diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 8d4e827..bbc02b5 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f2b037a..c67caad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.kapt' id 'dagger.hilt.android.plugin' + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' } android { @@ -55,13 +56,20 @@ dependencies { implementation 'androidx.activity:activity-compose:1.7.1' implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.compose.ui:ui' + implementation "androidx.compose.ui:ui-util" implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material:1.4.3' // compose implementation 'androidx.navigation:navigation-compose:2.5.3' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1' + implementation 'androidx.compose.foundation:foundation:1.4.3' + implementation 'com.google.accompanist:accompanist-navigation-animation:0.30.0' + implementation 'androidx.compose.animation:animation-graphics:1.4.3' + implementation 'androidx.compose.animation:animation:1.4.3' + implementation 'androidx.compose.animation:animation-core:1.4.3' //lifecycle implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' @@ -85,6 +93,7 @@ dependencies { // Dagger - Hilt implementation 'com.google.dagger:hilt-android:2.45' + implementation 'com.google.android.gms:play-services-location:21.0.1' kapt 'com.google.dagger:hilt-android-compiler:2.45' kapt 'androidx.hilt:hilt-compiler:1.0.0' implementation "androidx.hilt:hilt-navigation-compose:1.0.0" @@ -96,6 +105,25 @@ dependencies { implementation 'com.squareup.moshi:moshi:1.14.0' implementation 'com.squareup.moshi:moshi-kotlin:1.14.0' + // Pager + implementation "com.google.accompanist:accompanist-pager:0.23.1" + + // Maps + implementation 'com.google.maps.android:maps-compose:2.10.0' + implementation 'com.google.android.gms:play-services-maps:18.1.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2' + implementation 'com.google.android.gms:play-services-location:21.0.1' + + // CameraX + implementation "androidx.camera:camera-camera2:1.2.3" + implementation "androidx.camera:camera-lifecycle:1.2.3" + implementation "androidx.camera:camera-view:1.3.0-alpha07" + + // Room + implementation "androidx.room:room-ktx:2.5.1" + kapt "androidx.room:room-compiler:2.5.1" + implementation "androidx.room:room-paging:2.5.1" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a8a7842..3194540 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,12 @@ + + + + @@ -33,6 +39,10 @@ + + + AnimatedNavHost( + navController = navController, + startDestination = Splash.route, + modifier = Modifier + .padding(innerPadding), + + ) { + + composable( + route = Splash.route, + enterTransition = Splash.enterTransition, + exitTransition = Splash.exitTransition + ) { + SplashScreen(navController = navController) + } + + composable( + route = OnBoarding.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + }, + exitTransition = OnBoarding.exitTransition + ) { + OnBoardingScreen(navController = navController) + } + + composable(SignIn.route) { + SignInScreen(navController = navController) + } + + composable(SignUp.route) { + SignUpScreen(navController = navController) + } + + composable( + route = Main.route + "/{page}", + enterTransition = Main.enterTransition, + exitTransition = Main.exitTransition, + arguments = listOf( + navArgument("page") { + type = NavType.IntType + defaultValue = 0 + } + ) + ) { navBackStackEntry -> + val page = navBackStackEntry.arguments?.getInt("page") + if (page != null) { + MainScreen(navController = navController, page = page) + } + } + + composable( + route = Home.route, + enterTransition = Home.enterTransition, + exitTransition = Home.exitTransition + ) { + HomeScreen(navController = navController) + } + + composable( + route = Article.route, + enterTransition = Article.enterTransition, + exitTransition = Article.exitTransition + ) { + ArticleScreen(navController = navController) + } + + composable( + route = SingleArticle.route + "/{idArticle}", + arguments = listOf( + navArgument("idArticle") { + type = NavType.IntType + defaultValue = 1 + } + ) + ) { navBackStackEntry -> + val idArticle = navBackStackEntry.arguments?.getInt("idArticle") + + if(idArticle != null) { + SingleArticleScreen(idArticle = idArticle, navController = navController) + } + } + + composable(Forum.route) { + ForumScreen(navController = navController) + } + + composable( + route = SingleForum.route + "/{forumId}", + arguments = listOf( + navArgument("forumId") { + type = NavType.IntType + defaultValue = 1 + } + ) + ) { navBackStackEntry -> + val forumId = navBackStackEntry.arguments?.getInt("forumId") + if (forumId != null) { + ForumSingleScreen(navController = navController, forumId = forumId) + } + } + + composable( + route = CreateForum.route + ) { + ForumCreateScreen(navController = navController) + } + + composable(Profile.route) { + ProfileUserScreen(navController = navController) + } + + composable( + route = Setting.route, + enterTransition = Setting.enterTransition, + exitTransition = Setting.exitTransition + ) { + SettingScreen(navController = navController) + } + + composable(Catalog.route) { + CatalogScreen(navController = navController) + } + + composable( + route = SingleCatalog.route + "/{componentJson}", + arguments = listOf( + navArgument("componentJson") { + type = NavType.StringType + defaultValue = "U fucked up" + } + ) + ) { navBackStackEntry -> + val componentJson = navBackStackEntry.arguments?.getString("componentJson") + + if (componentJson != null) { + CatalogSingleComponentScreen(componentJson = componentJson, navController = navController) + } + } + + composable( + route = Camera.route, + enterTransition = Camera.enterTransition, + exitTransition = Camera.exitTransition + ) { + CameraScreen(navController = navController) + } + + composable( + route = DetectionResult.route + "/{uri}/{result}", + arguments = listOf( + navArgument("uri") { + type = NavType.StringType + defaultValue = "Image to sent doesn't exist" + }, + navArgument("result") { + type = NavType.StringType + defaultValue = "Nothing to predict" + } + ) + ) { navBackStackEntry -> + val uri = navBackStackEntry.arguments?.getString("uri") + val result = navBackStackEntry.arguments?.getString("result") + + if (uri != null && result != null) { + DetectionResultScreen(stringUri = uri, detectionResult = result, navController = navController) + } + + } + + composable(Maps.route) { + MapsScreen(navController = navController) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/local/database/FavoriteArticleDatabase.kt b/app/src/main/java/com/capstone/techwasmark02/data/local/database/FavoriteArticleDatabase.kt new file mode 100644 index 0000000..a50a4f4 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/local/database/FavoriteArticleDatabase.kt @@ -0,0 +1,14 @@ +package com.capstone.techwasmark02.data.local.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.capstone.techwasmark02.data.local.database.dao.FavoriteArticleEntityDao +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity + +@Database( + entities = [FavoriteArticleEntity::class], + version = 1 +) +abstract class FavoriteArticleDatabase: RoomDatabase() { + abstract val favoriteArticleDao: FavoriteArticleEntityDao +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/local/database/dao/FavoriteArticleEntityDao.kt b/app/src/main/java/com/capstone/techwasmark02/data/local/database/dao/FavoriteArticleEntityDao.kt new file mode 100644 index 0000000..4ec782d --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/local/database/dao/FavoriteArticleEntityDao.kt @@ -0,0 +1,28 @@ +package com.capstone.techwasmark02.data.local.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity +import kotlinx.coroutines.flow.Flow + + +@Dao +interface FavoriteArticleEntityDao { + + @Upsert + suspend fun upsertFavoriteArticle(favoriteArticle: FavoriteArticleEntity) + + @Delete + suspend fun deleteFavoriteArticle( + favoriteArticle: FavoriteArticleEntity + ) + + @Query("SELECT * FROM fav_article_entity") + fun getFavoriteArticles(): Flow> + + @Query("SELECT * FROM fav_article_entity WHERE id = :id") + fun getFavoriteArticleById(id: Int): Flow + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/local/database/entity/FavoriteArticleEntity.kt b/app/src/main/java/com/capstone/techwasmark02/data/local/database/entity/FavoriteArticleEntity.kt new file mode 100644 index 0000000..daa2776 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/local/database/entity/FavoriteArticleEntity.kt @@ -0,0 +1,14 @@ +package com.capstone.techwasmark02.data.local.database.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity( tableName = "fav_article_entity" ) +data class FavoriteArticleEntity( + @PrimaryKey + val id: Int, + val name: String, + val desc: String, + val articleImageURL: String, + val componentId: Int +) diff --git a/app/src/main/java/com/capstone/techwasmark02/data/mappers/Mappers.kt b/app/src/main/java/com/capstone/techwasmark02/data/mappers/Mappers.kt index 3f292d3..307683f 100644 --- a/app/src/main/java/com/capstone/techwasmark02/data/mappers/Mappers.kt +++ b/app/src/main/java/com/capstone/techwasmark02/data/mappers/Mappers.kt @@ -1,5 +1,7 @@ package com.capstone.techwasmark02.data.mappers +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity +import com.capstone.techwasmark02.data.model.FavoriteArticle import com.capstone.techwasmark02.data.model.UserSession import com.capstone.techwasmark02.data.remote.response.LoginResult @@ -8,4 +10,24 @@ fun LoginResult.toUserSession(): UserSession { userLoginToken = token, userNameId = userId ) -} \ No newline at end of file +} + +fun FavoriteArticleEntity.toFavoriteArticle(): FavoriteArticle { + return FavoriteArticle( + id = id, + name = name, + desc = desc, + imageURL = articleImageURL, + compId = componentId + ) +} + +fun FavoriteArticle.toFavoriteArticleEntity(): FavoriteArticleEntity { + return FavoriteArticleEntity( + id = id, + name = name, + componentId = compId, + articleImageURL = imageURL, + desc = desc + ) +} diff --git a/app/src/main/java/com/capstone/techwasmark02/data/model/FavoriteArticle.kt b/app/src/main/java/com/capstone/techwasmark02/data/model/FavoriteArticle.kt new file mode 100644 index 0000000..3ab5a23 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/model/FavoriteArticle.kt @@ -0,0 +1,9 @@ +package com.capstone.techwasmark02.data.model + +data class FavoriteArticle( + val id: Int, + val name: String, + val imageURL: String, + val compId: Int, + val desc: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/model/ForumCommentInfo.kt b/app/src/main/java/com/capstone/techwasmark02/data/model/ForumCommentInfo.kt new file mode 100644 index 0000000..b6704a9 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/model/ForumCommentInfo.kt @@ -0,0 +1,6 @@ +package com.capstone.techwasmark02.data.model + +data class ForumCommentInfo( + val comment: String, + val forumID: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/model/ForumToCreateInfo.kt b/app/src/main/java/com/capstone/techwasmark02/data/model/ForumToCreateInfo.kt new file mode 100644 index 0000000..e7f209a --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/model/ForumToCreateInfo.kt @@ -0,0 +1,9 @@ +package com.capstone.techwasmark02.data.model + +data class ForumToCreateInfo( + val category: String, + val content: String, + val imageUrl: String, + val location: String, + val title: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasArticleApi.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasArticleApi.kt new file mode 100644 index 0000000..047540e --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasArticleApi.kt @@ -0,0 +1,35 @@ +package com.capstone.techwasmark02.data.remote.apiService + +import com.capstone.techwasmark02.data.remote.response.AllArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.SingleArticleResponse +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path +import retrofit2.http.Query + +interface TechwasArticleApi { + + @GET("/allArticle") + suspend fun getAllArticle(): AllArticleResultResponse + + @GET("/article/{id}") + suspend fun getArticleByComponentId( + @Header("Authorization") token: String, + @Query("compid") compid: Int + ): ArticleResultResponse + + @GET("/article/id/{id}") + suspend fun getArticleById( + @Path("id") id: Int + ): SingleArticleResponse + + @GET("/article/name/{name}") + suspend fun getArticleByName( + @Path("name") name: String + ): ArticleResultResponse + + companion object { + const val BASE_URL = "https://backend-api-56g32wdmqa-uc.a.run.app/" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasComponentApi.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasComponentApi.kt new file mode 100644 index 0000000..ba3dc1e --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasComponentApi.kt @@ -0,0 +1,31 @@ +package com.capstone.techwasmark02.data.remote.apiService + +import com.capstone.techwasmark02.data.remote.response.ComponentResponse +import com.capstone.techwasmark02.data.remote.response.ComponentsResponse +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path + +interface TechwasComponentApi { + + @GET("components/") + suspend fun fetchComponents( + @Header("Authorization") token: String, + ) : ComponentsResponse + + @GET("components/{id}") + suspend fun fetchComponentById( + @Header("Authorization") token: String, + @Path("id") id: Int + ) : ComponentResponse + + @GET("smallparts/bycompid/{compid}") + suspend fun fetchUsableComponents( + @Path("compid") compid: Int + ) : UsableComponentsResponse + + companion object { + const val BASE_URL = "https://backend-api-56g32wdmqa-uc.a.run.app/" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasForumApi.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasForumApi.kt new file mode 100644 index 0000000..707dc36 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasForumApi.kt @@ -0,0 +1,63 @@ +package com.capstone.techwasmark02.data.remote.apiService + +import com.capstone.techwasmark02.data.model.ForumCommentInfo +import com.capstone.techwasmark02.data.model.ForumToCreateInfo +import com.capstone.techwasmark02.data.remote.response.CreateForumResponse +import com.capstone.techwasmark02.data.remote.response.ForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.ImageUrlResponse +import com.capstone.techwasmark02.data.remote.response.PostForumCommentResponse +import okhttp3.MultipartBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path + +interface TechwasForumApi { + + @GET("/forum/getall") + suspend fun fetchAllForum(): ForumResponse + + @GET("/forum/id/{id}") + suspend fun fetchForumById( + @Header("Authorization") token: String, + @Path("id") id: Int + ): ForumResponse + + @GET("/forum/category/{category}") + suspend fun fetchForumByCategory( + @Path("category") category: String + ): ForumResponse + + @POST("/forum/post") + suspend fun createNewForum( + @Header("Authorization") token: String, + @Body forumToCreateInfo: ForumToCreateInfo + ): CreateForumResponse + + @GET("/comments/byforumid/{forumid}") + suspend fun fetchForumComment( + @Path("forumid") forumid: Int + ): ForumCommentResponse + + @POST("/comments/post") + suspend fun postForumComment( + @Header("Authorization") token: String, + @Body forumCommentInfo: ForumCommentInfo + ): PostForumCommentResponse + + + @Multipart + @POST("/forum/upimagepost") + suspend fun uploadAndGetImage( + @Header("Authorization") token: String, + @Part imageFile: MultipartBody.Part + ): ImageUrlResponse + + companion object { + const val BASE_URL = "https://backend-api-56g32wdmqa-uc.a.run.app/" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasPredictionApi.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasPredictionApi.kt index e74f63b..4d139fc 100644 --- a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasPredictionApi.kt +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasPredictionApi.kt @@ -1,5 +1,7 @@ package com.capstone.techwasmark02.data.remote.apiService + +import com.capstone.techwasmark02.data.remote.response.DetectionsResultResponse import okhttp3.MultipartBody import retrofit2.http.Multipart import retrofit2.http.POST @@ -11,9 +13,10 @@ interface TechwasPredictionApi { @POST("predict/") suspend fun predict( @Part imageFile: MultipartBody.Part - ) : String + ) : DetectionsResultResponse companion object { - const val BASE_URL = "https://e-waste-model-deployment-1gb-fwd5gpydiq-uc.a.run.app/" + const val BASE_URL = "http://35.222.88.99/" +// const val BASE_URL = "https://e-waste-model-deployment-1gb-fwd5gpydiq-uc.a.run.app/" } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasUserApi.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasUserApi.kt index cc4f41d..0e9767e 100644 --- a/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasUserApi.kt +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/apiService/TechwasUserApi.kt @@ -3,6 +3,7 @@ package com.capstone.techwasmark02.data.remote.apiService import com.capstone.techwasmark02.data.model.UserLoginInfo import com.capstone.techwasmark02.data.model.UserRegisterInfo import com.capstone.techwasmark02.data.remote.response.UserLoginResponse +import com.capstone.techwasmark02.data.remote.response.UserRegisterResponse import retrofit2.http.Body import retrofit2.http.POST @@ -16,12 +17,10 @@ interface TechwasUserApi { @POST("user/signup") suspend fun register( @Body userRegisterInfo: UserRegisterInfo - ) : String - - + ) : UserRegisterResponse companion object { - const val BASE_URL = "https://the-prophecy-fwd5gpydiq-uc.a.run.app/" + const val BASE_URL = "https://backend-api-56g32wdmqa-uc.a.run.app/" } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ArticleResultResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ArticleResultResponse.kt new file mode 100644 index 0000000..b7a4b03 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ArticleResultResponse.kt @@ -0,0 +1,29 @@ +package com.capstone.techwasmark02.data.remote.response + +import com.google.gson.annotations.SerializedName + +data class ArticleResultResponse( + val articleList: List, + val error: String?, + val message: String?, +) + +data class AllArticleResultResponse( + val componentList: List, + val error: String?, + val message: String?, +) + +data class ArticleList( + @SerializedName("componentId") + val componentId: Int?, + @SerializedName("articleImageURL") + val articleImageURL: String?, + @SerializedName("name") + val name: String?, + @SerializedName("id") + val id: Int?, + @SerializedName("desc") + val desc: String?, +) + diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentResponse.kt new file mode 100644 index 0000000..6363750 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentResponse.kt @@ -0,0 +1,28 @@ +package com.capstone.techwasmark02.data.remote.response + +data class ComponentResponse( + val componentList: ComponentList, + val error: String, + val message: String +) + +data class ComponentList( + val desc: String, + val example: String, + val id: Int, + val name: String +) + +data class UsableComponentsResponse( + val error: String, + val message: String, + val smallParts: List +) + +data class SmallPart( + val compID: Int, + val description: String, + val id: Int, + val imageURL: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentsResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentsResponse.kt new file mode 100644 index 0000000..0833b79 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ComponentsResponse.kt @@ -0,0 +1,14 @@ +package com.capstone.techwasmark02.data.remote.response + +data class ComponentsResponse( + val components: List, + val error: String, + val message: String +) + +data class Component( + val desc: String, + val id: Int, + val imageExample: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/DetectionResultResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/DetectionResultResponse.kt new file mode 100644 index 0000000..c8a9ea4 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/DetectionResultResponse.kt @@ -0,0 +1,21 @@ +package com.capstone.techwasmark02.data.remote.response + +import com.google.gson.annotations.SerializedName + +data class DetectionsResultResponse( + val Image_Url: String, + val predictions: List, + val time_taken: String +) + +data class Prediction( + @SerializedName("Components ID") + val componentId: Int, + @SerializedName("Components Name") + val componentName: String, + @SerializedName("Components Value") + val componentValue: Double +) + + + diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ForumResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ForumResponse.kt new file mode 100644 index 0000000..74fcf28 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/ForumResponse.kt @@ -0,0 +1,60 @@ +package com.capstone.techwasmark02.data.remote.response + +import com.google.gson.annotations.SerializedName + +data class ForumResponse( + val error: String, + val forum: List, + val message: String +) + +data class Forum( + @SerializedName("Postedby") + val postedBy: String, + val category: String, + val content: String, + val id: Int, + val imageURL: String, + val likes: Any, + val location: String, + val title: String +) + +data class ForumCommentResponse( + val article: List
, + val error: String, + val message: String +) + +data class Article( + val comment: String, + val forumID: Int, + val id: Int, + val replyFrom: Int, + val userID: String, + val username: String +) + +data class PostForumCommentResponse( + val commentInfo: CommentInfo, + val error: String, + val message: String +) + +data class CommentInfo( + val Poster: String, + val PosterID: Int, + val comment: String, + val forumID: Int +) + +data class CreateForumResponse( + val error: String, + val message: String +) + +data class ImageUrlResponse( + val error: String, + val imgURL: String, + val message: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/SingleArticleResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/SingleArticleResponse.kt new file mode 100644 index 0000000..2ec2174 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/SingleArticleResponse.kt @@ -0,0 +1,36 @@ +package com.capstone.techwasmark02.data.remote.response + +import com.google.gson.annotations.SerializedName + +data class SingleArticleResponse( + + @field:SerializedName("error") + val error: String? = null, + + @field:SerializedName("message") + val message: String? = null, + + @field:SerializedName("article") + val article: List? = null +) + +data class ArticleItem( + + @field:SerializedName("componentID") + val componentID: Int? = null, + + @field:SerializedName("articleImageURL") + val articleImageURL: String? = null, + + @field:SerializedName("name") + val name: String? = null, + + @field:SerializedName("description") + val description: String? = null, + + @field:SerializedName("id") + val id: Int? = null, + + @field:SerializedName("componentName") + val componentName: String? = null +) diff --git a/app/src/main/java/com/capstone/techwasmark02/data/remote/response/UserRegisterResponse.kt b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/UserRegisterResponse.kt new file mode 100644 index 0000000..79aefb1 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/data/remote/response/UserRegisterResponse.kt @@ -0,0 +1,18 @@ +package com.capstone.techwasmark02.data.remote.response + +import com.google.gson.annotations.SerializedName + +data class UserRegisterResponse( + val error: String, + val message: String, + val registerResult: RegisterResult, +) + +data class RegisterResult( + val signupToken: SignUpToken, +) + +data class SignUpToken( + @SerializedName("access token") + val accessToken: String +) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/di/AppModule.kt b/app/src/main/java/com/capstone/techwasmark02/di/AppModule.kt index d9c151c..58585ec 100644 --- a/app/src/main/java/com/capstone/techwasmark02/di/AppModule.kt +++ b/app/src/main/java/com/capstone/techwasmark02/di/AppModule.kt @@ -4,8 +4,17 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import androidx.room.Room +import com.capstone.techwasmark02.data.local.database.FavoriteArticleDatabase +import com.capstone.techwasmark02.data.remote.apiService.TechwasArticleApi +import com.capstone.techwasmark02.data.remote.apiService.TechwasComponentApi +import com.capstone.techwasmark02.data.remote.apiService.TechwasForumApi import com.capstone.techwasmark02.data.remote.apiService.TechwasPredictionApi import com.capstone.techwasmark02.data.remote.apiService.TechwasUserApi +import com.capstone.techwasmark02.repository.FavoriteArticleRepository +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.repository.TechwasComponentApiRepository +import com.capstone.techwasmark02.repository.TechwasForumApiRepository import com.capstone.techwasmark02.repository.TechwasUserApiRepository import dagger.Module import dagger.Provides @@ -61,6 +70,57 @@ object AppModule { return retrofit.create(TechwasPredictionApi::class.java) } + @Provides + @Singleton + fun provideTechwasArticleApi(): TechwasArticleApi { + val loggingInterceptor = + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + val retrofit = Retrofit.Builder() + .baseUrl(TechwasArticleApi.BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + return retrofit.create(TechwasArticleApi::class.java) + } + + @Provides + @Singleton + fun provideTechwasComponentApi(): TechwasComponentApi { + val loggingInterceptor = + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + val retrofit = Retrofit.Builder() + .baseUrl(TechwasComponentApi.BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + return retrofit.create(TechwasComponentApi::class.java) + } + + @Provides + @Singleton + fun provideTechwasForumApi(): TechwasForumApi { + val loggingInterceptor = + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + val retrofit = Retrofit.Builder() + .baseUrl(TechwasForumApi.BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + return retrofit.create(TechwasForumApi::class.java) + } + @Provides @Singleton fun provideDataStorePreferences( @@ -74,4 +134,34 @@ object AppModule { api: TechwasUserApi ) = TechwasUserApiRepository(api) + @Provides + fun provideTechwasArticleRepository( + apiArticle: TechwasArticleApi + ) = TechwasArticleRepository(apiArticle) + + @Provides + fun provideTechwasComponentApiRepository( + api: TechwasComponentApi + ) = TechwasComponentApiRepository(api) + + @Provides + fun provideTechwasForumApiRepository( + api: TechwasForumApi + ) = TechwasForumApiRepository(api) + + @Provides + @Singleton + fun provideFavoriteArticleDatabase(@ApplicationContext context: Context): FavoriteArticleDatabase { + return Room.databaseBuilder( + context, + FavoriteArticleDatabase::class.java, + "fav_article_database" + ).build() + } + + @Provides + fun provideFavoriteArticleRepository( + favoriteArticleDatabase: FavoriteArticleDatabase + ) = FavoriteArticleRepository(favArticleDatabase = favoriteArticleDatabase) + } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/FavoriteArticleRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/FavoriteArticleRepository.kt new file mode 100644 index 0000000..6b8f83c --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/repository/FavoriteArticleRepository.kt @@ -0,0 +1,20 @@ +package com.capstone.techwasmark02.repository + +import com.capstone.techwasmark02.data.local.database.FavoriteArticleDatabase +import com.capstone.techwasmark02.data.mappers.toFavoriteArticleEntity +import com.capstone.techwasmark02.data.model.FavoriteArticle +import javax.inject.Inject + +class FavoriteArticleRepository @Inject constructor(private val favArticleDatabase: FavoriteArticleDatabase) { + + fun getFavArticles() = favArticleDatabase.favoriteArticleDao.getFavoriteArticles() + + fun getFavArticleById(id: Int) = favArticleDatabase.favoriteArticleDao.getFavoriteArticleById(id = id) + + suspend fun upsertFavoriteArticle(favoriteArticle: FavoriteArticle) = favArticleDatabase.favoriteArticleDao.upsertFavoriteArticle(favoriteArticle.toFavoriteArticleEntity()) + + suspend fun deleteFavoriteArticle( + favoriteArticle: FavoriteArticle + ) = favArticleDatabase.favoriteArticleDao.deleteFavoriteArticle(favoriteArticle.toFavoriteArticleEntity()) + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/PreferencesRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/PreferencesRepository.kt index e8e1800..d06f440 100644 --- a/app/src/main/java/com/capstone/techwasmark02/repository/PreferencesRepository.kt +++ b/app/src/main/java/com/capstone/techwasmark02/repository/PreferencesRepository.kt @@ -1,14 +1,16 @@ package com.capstone.techwasmark02.repository +import android.content.res.Resources +import androidx.compose.ui.res.stringResource import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import com.capstone.techwasmark02.R import com.capstone.techwasmark02.common.Resource import com.capstone.techwasmark02.data.model.UserSession import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasArticleRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasArticleRepository.kt new file mode 100644 index 0000000..76b98c9 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasArticleRepository.kt @@ -0,0 +1,53 @@ +package com.capstone.techwasmark02.repository + +import com.capstone.techwasmark02.data.remote.apiService.TechwasArticleApi +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.SingleArticleResponse +import com.capstone.techwasmark02.ui.common.UiState +import javax.inject.Inject + +class TechwasArticleRepository @Inject constructor( + private val articleApi: TechwasArticleApi +) { + suspend fun getAllArticle(): UiState { + val response = try { + articleApi.getAllArticle() + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch article, ${e.message}") + } + val articleResultResponse = ArticleResultResponse( + articleList = response.componentList, + error = response.error, + message = response.message + ) + + return UiState.Success(data = articleResultResponse, message = "Success to fetch article") + } + + suspend fun getArticleById(id: Int): UiState { + val response = try { + articleApi.getArticleById(id) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch article, ${e.message}") + } + return UiState.Success(data = response, message = "Success to fetch article") + } + + suspend fun getArticleByName(name: String): UiState { + val response = try { + articleApi.getArticleByName(name) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch article, ${e.message}") + } + return UiState.Success(data = response, message = "Success to fetch article") + } + + suspend fun getArticleByComponentId(userToken: String = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySUQiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwaXJ5IjoxNjg1ODcyMDkwLjMwNjU4ODJ9.cvaEjnnWe4Z3Hl-ImAIKyguTWeuntb6vOuwGCa1rr2w", id: Int): UiState { + val response = try { + articleApi.getArticleByComponentId(token = userToken, compid = id) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch article, ${e.message}") + } + return UiState.Success(data = response, message = "Success to fetch article") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasComponentApiRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasComponentApiRepository.kt new file mode 100644 index 0000000..f22ab12 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasComponentApiRepository.kt @@ -0,0 +1,43 @@ +package com.capstone.techwasmark02.repository + +import com.capstone.techwasmark02.data.remote.apiService.TechwasComponentApi +import com.capstone.techwasmark02.data.remote.response.ComponentResponse +import com.capstone.techwasmark02.data.remote.response.ComponentsResponse +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import com.capstone.techwasmark02.ui.common.UiState +import javax.inject.Inject + +class TechwasComponentApiRepository @Inject constructor( + private val componentApi: TechwasComponentApi +) { + + suspend fun fetchComponents(userToken: String = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySUQiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwaXJ5IjoxNjg1ODcyMDkwLjMwNjU4ODJ9.cvaEjnnWe4Z3Hl-ImAIKyguTWeuntb6vOuwGCa1rr2w") : UiState { + + val response = try { + componentApi.fetchComponents(token = userToken) + } catch (e: Exception) { + return UiState.Error(message = e.message ?: "Fail to fetch components") + } + return UiState.Success(data = response) + } + + suspend fun fetchComponentById(userToken: String = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySUQiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwaXJ5IjoxNjg1ODcyMDkwLjMwNjU4ODJ9.cvaEjnnWe4Z3Hl-ImAIKyguTWeuntb6vOuwGCa1rr2w", componentId: Int) : UiState { + + val response = try { + componentApi.fetchComponentById(token = userToken, id = componentId) + } catch (e: Exception) { + return UiState.Error(message = e.message ?: "Fail to fetch components") + } + return UiState.Success(data = response) + } + + suspend fun fetchUsableComponents(compId: Int): UiState { + val response = try { + componentApi.fetchUsableComponents(compid = compId) + } catch (e: Exception) { + return UiState.Error(message = e.message ?: "Fail to fetch usable components") + } + return UiState.Success(data = response) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasForumApiRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasForumApiRepository.kt new file mode 100644 index 0000000..080f55d --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasForumApiRepository.kt @@ -0,0 +1,110 @@ +package com.capstone.techwasmark02.repository + +import com.capstone.techwasmark02.data.model.ForumCommentInfo +import com.capstone.techwasmark02.data.model.ForumToCreateInfo +import com.capstone.techwasmark02.data.remote.apiService.TechwasForumApi +import com.capstone.techwasmark02.data.remote.response.CreateForumResponse +import com.capstone.techwasmark02.data.remote.response.ForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.ImageUrlResponse +import com.capstone.techwasmark02.data.remote.response.PostForumCommentResponse +import com.capstone.techwasmark02.ui.common.UiState +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import javax.inject.Inject + +class TechwasForumApiRepository @Inject constructor( + private val forumApi: TechwasForumApi +) { + + suspend fun fetchAllForum(): UiState { + val response = try { + forumApi.fetchAllForum() + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch forum, ${e.message}") + } + return UiState.Success(data = response, message = "Success to fetch forum") + } + + suspend fun fetchForumById(id: Int, userToken: String): UiState { + val token = "Bearer $userToken" + + val response = try { + forumApi.fetchForumById( + id = id, + token = token + ) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch forum, ${e.message}") + } + return UiState.Success(data = response, message = response.message) + } + + suspend fun fetchForumByCategory(category: String): UiState { + val response = try { + forumApi.fetchForumByCategory(category) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch forum, ${e.message}") + } + return UiState.Success(data = response, message = response.message) + } + + suspend fun fetchForumCommentByForumId(forumId: Int): UiState { + val response = try { + forumApi.fetchForumComment(forumid = forumId) + } catch (e: Exception) { + return UiState.Error(message = "fail to fetch comment, ${e.message}") + } + return UiState.Success(data = response, message = response.message) + } + + suspend fun postForumComment(forumCommentInfo: ForumCommentInfo, userToken: String): UiState { + val token = "Bearer $userToken" + + val response = try { + forumApi.postForumComment( + token, + forumCommentInfo, + ) + } catch (e: Exception) { + return UiState.Error(message = "fail to post comment, ${e.message}") + } + return UiState.Success(data = response, message = response.message) + } + + suspend fun createNewForum(forumToCreateInfo: ForumToCreateInfo, userToken: String): UiState { + val token = "Bearer $userToken" + + val response = try { + forumApi.createNewForum(forumToCreateInfo = forumToCreateInfo, token = token) + } catch (e: Exception) { + return UiState.Error(message = e.message ?: "Fail to create new forum") + } + return UiState.Success(data = response, message = response.message) + } + + suspend fun uploadAndGetImageUrl(file: File, userToken: String): UiState { + val token = "Bearer ${userToken}" + + val imageFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) + val imageMultiPart: MultipartBody.Part = MultipartBody.Part + .createFormData( + name = "file", + filename = file.name, + body = imageFile + ) + + val response = try { + forumApi.uploadAndGetImage( + token, + imageMultiPart + ) + } catch (e: Exception) { + return UiState.Error(message = "fail to get image url, ${e.message}") + } + return UiState.Success(data = response, message = response.message) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasPredictionApiRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasPredictionApiRepository.kt index a7736b3..5cd8623 100644 --- a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasPredictionApiRepository.kt +++ b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasPredictionApiRepository.kt @@ -1,6 +1,7 @@ package com.capstone.techwasmark02.repository import com.capstone.techwasmark02.data.remote.apiService.TechwasPredictionApi +import com.capstone.techwasmark02.data.remote.response.DetectionsResultResponse import com.capstone.techwasmark02.ui.common.UiState import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -12,7 +13,7 @@ class TechwasPredictionApiRepository @Inject constructor( private val predictionApi: TechwasPredictionApi ) { - suspend fun predictWaste(file: File): UiState { + suspend fun predictWaste(file: File): UiState { val imageFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) val imageMultiPart: MultipartBody.Part = MultipartBody.Part.createFormData( name = "file", diff --git a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasUserApiRepository.kt b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasUserApiRepository.kt index ca0821a..d943bff 100644 --- a/app/src/main/java/com/capstone/techwasmark02/repository/TechwasUserApiRepository.kt +++ b/app/src/main/java/com/capstone/techwasmark02/repository/TechwasUserApiRepository.kt @@ -4,6 +4,7 @@ import com.capstone.techwasmark02.data.model.UserLoginInfo import com.capstone.techwasmark02.data.model.UserRegisterInfo import com.capstone.techwasmark02.data.remote.apiService.TechwasUserApi import com.capstone.techwasmark02.data.remote.response.UserLoginResponse +import com.capstone.techwasmark02.data.remote.response.UserRegisterResponse import com.capstone.techwasmark02.ui.common.UiState import java.lang.Exception import javax.inject.Inject @@ -22,7 +23,7 @@ class TechwasUserApiRepository @Inject constructor( return UiState.Success(data = response) } - suspend fun userRegister(userRegisterInfo: UserRegisterInfo) : UiState { + suspend fun userRegister(userRegisterInfo: UserRegisterInfo) : UiState { val response = try { userApi.register(userRegisterInfo) diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/ArticleCard.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/ArticleCard.kt index 94943d4..72c930d 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/ArticleCard.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/ArticleCard.kt @@ -1,11 +1,13 @@ package com.capstone.techwasmark02.ui.component -import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -16,21 +18,89 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.ArticleList import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme -import kotlin.random.Random @Composable fun ArticleCardBig( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + article: ArticleList +) { + ElevatedCard( + modifier = modifier + .height(175.dp), + shape = MaterialTheme.shapes.large, + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(MaterialTheme.colorScheme.tertiary) + ) { + Box( + modifier = Modifier + .weight(1f) + .background(Color.LightGray) + ) { + AsyncImage( + model = article.articleImageURL, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .height(64.dp), + verticalArrangement = Arrangement.Center + ) { + article.name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + article.desc?.let { + HtmlText( + html = it, + textStyle = MaterialTheme.typography.bodySmall.copy( + color = Color.Black, + fontWeight = FontWeight.Normal, + fontSize = 7.sp + ), + maxLine = 1 + ) + } + } + } + } +} + +@Composable +fun ArticleCardSmall( + modifier: Modifier = Modifier, + imgUrl: String?, + title: String?, + description: String?, ) { ElevatedCard( modifier = modifier, @@ -42,13 +112,15 @@ fun ArticleCardBig( Box( modifier = Modifier .height(107.dp) + .fillMaxWidth() ) { Image( modifier = Modifier .fillMaxWidth() .height(107.dp), painter = rememberAsyncImagePainter( - model = "https://picsum.photos/seed/${Random.nextInt()}/320/120", +// model = "https://picsum.photos/seed/${Random.nextInt()}/320/120", + model = imgUrl, placeholder = painterResource(id = R.drawable.place_holder), ), contentScale = ContentScale.Crop, @@ -63,16 +135,25 @@ fun ArticleCardBig( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.Center ) { - Text( - text = "Do not throw electronic waste carelessly", - style = MaterialTheme.typography.titleSmall, - maxLines = 1 - ) - Text( - text = "Lorem ipsum dolor sit amet, consectetur...", - style = MaterialTheme.typography.bodySmall, - maxLines = 1 - ) + if (title != null) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (description != null) { + HtmlText( + html = description, + textStyle = MaterialTheme.typography.bodySmall.copy( + color = Color.Black, + fontWeight = FontWeight.Normal, + fontSize = 7.sp + ), + maxLine = 1 + ) + } } } } @@ -87,7 +168,23 @@ fun ArticleCardPreview() { .background(MaterialTheme.colorScheme.background) .padding(20.dp) ) { - ArticleCardBig(modifier = Modifier.width(240.dp)) + Column { + ArticleCardBig(modifier = Modifier.width(240.dp), article = ArticleList( + componentId = 2, + articleImageURL = null, + name = "Do not throw electronic was bg", + id = 2, + desc = "Lorem ipsum and the sum of the sum si sum for the sum" + ) + ) + Spacer(modifier = Modifier.height(20.dp)) + ArticleCardSmall( + modifier = Modifier.width(150.dp), + imgUrl = "", + title = "judul satu", + description = "deskripsi ajah", + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/Banner.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/Banner.kt index 793e532..26e7b94 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/Banner.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/Banner.kt @@ -1,14 +1,20 @@ package com.capstone.techwasmark02.ui.component +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,9 +24,22 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.Black20 +import com.capstone.techwasmark02.ui.theme.Green77 import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.capstone.techwasmark02.ui.theme.Yellow77 +import com.capstone.techwasmark02.ui.theme.poppins @Composable fun SignInBanner( @@ -39,24 +58,66 @@ fun SignInBanner( .background( MaterialTheme.colorScheme.primary, ), - contentAlignment = Alignment.Center ) { - Text( - text = "Welcome to TechWaste", - style = MaterialTheme.typography.headlineSmall.copy( - shadow = Shadow( - color = MaterialTheme.colorScheme.onTertiary.copy( - alpha = 0.8f + + Image( + painter = painterResource(id = R.drawable.img_bg_singin), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center + ) { + + Spacer(modifier = Modifier.height(10.dp)) + + Box( + modifier = Modifier + ) { + + Text( + text = "Welcome", + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Bold ), - offset = Offset( - x = 0f, - y = 14f + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .offset( + x = 0.dp, + y = -(18.dp) + ) + ) + + Text( + text = "to Techwaste!", + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Bold ), - blurRadius = 16f + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .offset( + x = 0.dp, + y = (10.dp) + ) ) - ), - color = MaterialTheme.colorScheme.onPrimary, - ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Let's manage your e-waste properly.", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.onPrimary + ) + } } } @@ -66,7 +127,7 @@ fun SignUpBanner( ) { Box( modifier = modifier - .height(100.dp) + .height(150.dp) .fillMaxWidth() .shadow( elevation = 12.dp, @@ -77,69 +138,259 @@ fun SignUpBanner( .background( MaterialTheme.colorScheme.primary, ), - contentAlignment = Alignment.Center ) { - Text( - text = "Nice to meet you", - style = MaterialTheme.typography.headlineSmall.copy( - shadow = Shadow( - color = MaterialTheme.colorScheme.onTertiary.copy( - alpha = 0.8f + + Image( + painter = painterResource(id = R.drawable.img_bg_signup), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + ) { + + Text( + text = "Let's start your", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold ), - offset = Offset( - x = 0f, - y = 14f + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .offset( + x = 0.dp, + y = -(28.dp) + ) + ) + + Text( + text = "journey to dispose of", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold ), - blurRadius = 16f + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .offset( + x = 0.dp, + y = (0.dp) + ) ) - ), - color = MaterialTheme.colorScheme.onPrimary, - ) + + Text( + text = "e-waste!", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .offset( + x = 0.dp, + y = (28.dp) + ) + ) + } + } } } @Composable fun DropPointBanner(modifier: Modifier = Modifier) { Box( - modifier = modifier - .height(175.dp) - .fillMaxWidth() - .shadow( - elevation = 12.dp, - shape = MaterialTheme.shapes.large, - clip = true, - ) - .clip(MaterialTheme.shapes.large) - .background( - MaterialTheme.colorScheme.primary, - ) - .padding(horizontal = 18.dp), - contentAlignment = Alignment.CenterEnd + modifier = modifier.height(190.dp), + contentAlignment = Alignment.BottomStart ) { - Column( + Box( modifier = Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.End + .height(175.dp) + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .background( + Green77.copy(alpha = 0.5f), + ), + contentAlignment = Alignment.Center ) { - Text( - text = "Don't know where to", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimary, - ) - Text( - text = "put your e-waste?", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimary, - ) - Text( - text = "We've got you covered", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimary, - ) - Spacer(modifier = Modifier.height(12.dp)) - SmallButton(contentText = "LOCATE NOW") + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .clip(RoundedCornerShape(44.dp)) + .background( + Green77.copy(alpha = 0.5f) + ) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxSize() + .clip(RoundedCornerShape(65.dp)) + .background( + Green77.copy(alpha = 0.7f) + ) + ) + } + Row( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.width(150.dp)) + Column( + modifier = Modifier.fillMaxWidth() + ) { + + val text = buildAnnotatedString { + append("Don't know where to put your ") + withStyle(style = SpanStyle(color = Yellow77)) { + append("e-waste?") + } + } + + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + shadow = Shadow( + color = Black20.copy( + alpha = 0.25f, + ), + offset = Offset(0f, 4.0f), + blurRadius = 4f + ), + fontSize = 22.sp, + lineHeight = 25.sp, + letterSpacing = 0.sp, + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = poppins + ) + ) + Spacer(modifier = Modifier.height(10.dp)) + Row(modifier = Modifier.align(alignment = Alignment.CenterHorizontally)) { + SmallButton(contentText = "LOCATE NOW", colorText = MaterialTheme.colorScheme.primary) + } + } + } } + Image( + painter = painterResource(id = R.drawable.trash_bucket), + contentDescription = "Image", + modifier = Modifier + .height(400.dp) + .width(200.dp) + .offset(y = 0.dp, x = 10.dp) + ) + } +} + +@Composable +fun ForumBanner(modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(190.dp), + contentAlignment = Alignment.BottomStart + ) { + Box( + modifier = Modifier + .height(175.dp) + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .background( + Green77.copy(alpha = 0.5f), + ), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .clip(RoundedCornerShape(39.dp)) + .background( + Green77.copy(alpha = 0.5f) + ) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxSize() + .clip(RoundedCornerShape(72.dp)) + .background( + Green77.copy(alpha = 0.7f) + ) + ) + } + Row( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.width(160.dp)) + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row(modifier = Modifier.align(alignment = Alignment.CenterHorizontally)) { + SmallButton(contentText = "LOCATE NOW", colorText = MaterialTheme.colorScheme.primary) + } + + Spacer(modifier = Modifier.height(10.dp)) + + val text = buildAnnotatedString { + append("Don't know where to put your ") + withStyle(style = SpanStyle(color = Yellow77)) { + append("e-waste?") + } + } + + Text( + text = "Join the discussions!", + modifier = Modifier.width(200.dp), + style = MaterialTheme.typography.labelSmall.copy( + shadow = Shadow( + color = Black20.copy( + alpha = 0.25f, + ), + offset = Offset(0f, 4.0f), + blurRadius = 4f, + ), + fontSize = 13.sp, + lineHeight = 25.sp, + letterSpacing = 0.sp, + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = poppins, + textAlign = TextAlign.End + ) + ) + + Text( + text = "Get involved in discussion with other users.", + style = MaterialTheme.typography.bodyMedium, + fontSize = 11.sp, + textAlign = TextAlign.End, + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = poppins + ) + + } + } + } + Image( + painter = painterResource(id = R.drawable.forum_chat), + contentDescription = "Image", + modifier = Modifier + .height(400.dp) + .width(170.dp) + .offset(y = 0.dp, x = 20.dp) + ) } } @@ -171,7 +422,11 @@ fun HomeBannerPreview() { .padding(16.dp) ) { DropPointBanner() + + Spacer(modifier = Modifier.height(16.dp)) + + ForumBanner() } } - + } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/BottomBar.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/BottomBar.kt index cd43d69..2daa4ca 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/BottomBar.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/BottomBar.kt @@ -4,13 +4,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -19,100 +23,159 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.capstone.techwasmark02.R import com.capstone.techwasmark02.ui.componentType.BottomBarItemType import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme -import com.capstone.techwasmark02.ui.theme.customShape.BottomNavShape -import com.capstone.techwasmark02.ui.theme.customShape.BottomNavShape2 @Composable fun DefaultBottomBar( modifier: Modifier = Modifier, - selectedType: BottomBarItemType + selectedType: BottomBarItemType, + onClickBottomNavType: (bottomBarType: BottomBarItemType) -> Unit, ) { - NavigationBar( - modifier = modifier - .graphicsLayer { - shape = BottomNavShape() - clip = true - } + Box( + modifier = Modifier .fillMaxWidth() - .height(60.dp), - containerColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface, - + .padding(horizontal = 8.dp) + .padding(bottom = 8.dp) ) { - Row( - modifier = Modifier + NavigationBar( + modifier = modifier + .shadow( + elevation = 8.dp, +// shape = BottomNavShape(), + clip = true + ) + .clip(RoundedCornerShape(10.dp)) .fillMaxWidth() - .fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically + .height(64.dp), + containerColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface, + tonalElevation = 8.dp ) { - Spacer(modifier = Modifier.weight(1f)) - - BottomBarItem(bottomBarItemType = BottomBarItemType.Home, selectedType = selectedType) - - Spacer(modifier = Modifier.weight(1f)) - - BottomBarItem(bottomBarItemType = BottomBarItemType.Forum, selectedType = selectedType) - - Spacer(modifier = Modifier.weight(1f)) - - Spacer(modifier = Modifier.width(80.dp)) - - Spacer(modifier = Modifier.weight(1f)) - - BottomBarItem(bottomBarItemType = BottomBarItemType.Article, selectedType = selectedType) - - Spacer(modifier = Modifier.weight(1f)) - - BottomBarItem(bottomBarItemType = BottomBarItemType.Profile, selectedType = selectedType) - - Spacer(modifier = Modifier.weight(1f)) - + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BottomBarItem( + bottomBarItemType = BottomBarItemType.Home, + selectedType = selectedType, + onClick = { + onClickBottomNavType(it) + }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + BottomBarItem( + bottomBarItemType = BottomBarItemType.Forum, + selectedType = selectedType, + onClick = { + onClickBottomNavType(it) + }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + BottomBarItem( + bottomBarItemType = BottomBarItemType.Article, + selectedType = selectedType, + onClick = { + onClickBottomNavType(it) + }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + BottomBarItem( + bottomBarItemType = BottomBarItemType.Profile, + selectedType = selectedType, + onClick = { + onClickBottomNavType(it) + }, + ) + } } } } + @Composable fun BottomBarItem( modifier: Modifier = Modifier, bottomBarItemType: BottomBarItemType, - selectedType: BottomBarItemType + selectedType: BottomBarItemType, + onClick: (BottomBarItemType) -> Unit, ) { - Column( + Box( modifier = modifier - .width(44.dp) - .height(44.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + .fillMaxHeight() + .width(IntrinsicSize.Max), + contentAlignment = Alignment.Center ) { - Icon( - painter = painterResource(id = bottomBarItemType.icon), - contentDescription = null, - tint = if (selectedType == bottomBarItemType) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.inverseOnSurface - } - ) - Text( - text = bottomBarItemType.title, - style = MaterialTheme.typography.labelSmall, - color = if (selectedType == bottomBarItemType) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.inverseOnSurface + if (selectedType == bottomBarItemType) { + Column( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + verticalArrangement = Arrangement.Bottom + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .background(MaterialTheme.colorScheme.primary) + ) } - ) + } + + IconButton( + onClick = { + onClick(bottomBarItemType) + }, + modifier = modifier + .size(44.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Icon( + painter = painterResource(id = bottomBarItemType.icon), + contentDescription = null, + tint = if (selectedType == bottomBarItemType) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.inverseOnSurface + } + + ) + Text( + text = bottomBarItemType.title, + style = MaterialTheme.typography.labelSmall, + color = if (selectedType == bottomBarItemType) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.inverseOnSurface + } + ) + } + } } } + @Preview(showBackground = true) @Composable fun DefaultBottomBarPreview() { @@ -122,37 +185,10 @@ fun DefaultBottomBarPreview() { .fillMaxWidth() .padding(20.dp) ) { - DefaultBottomBar(selectedType = BottomBarItemType.Home) + DefaultBottomBar( + selectedType = BottomBarItemType.Home, + onClickBottomNavType = {}, + ) } } -} - -@Composable -fun CheckBottomNav( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .graphicsLayer { - shape = BottomNavShape() - clip = true - } - .fillMaxWidth() - .height(58.dp) - .background(MaterialTheme.colorScheme.inverseSurface) - ) { - - } -} - -@Preview (showBackground = true) -@Composable -fun CheckBottomNavPreview() { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) - ) { - CheckBottomNav() - } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/Button.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/Button.kt index 6cffb8c..93d6896 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/Button.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/Button.kt @@ -1,22 +1,21 @@ package com.capstone.techwasmark02.ui.component import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -26,7 +25,8 @@ import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme fun DefaultButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, - contentText: String + contentText: String, + buttonColors: ButtonColors = ButtonDefaults.buttonColors() ) { Button( onClick = onClick, @@ -35,6 +35,7 @@ fun DefaultButton( elevation = ButtonDefaults.buttonElevation( defaultElevation = 4.dp ), + colors = buttonColors ) { Text( text = contentText, @@ -73,19 +74,16 @@ fun InverseButton( } @Composable -fun SmallButton(modifier: Modifier = Modifier, contentText: String, onClick: () -> Unit = {}) { +fun SmallButton(modifier: Modifier = Modifier, contentText: String, onClick: () -> Unit = {}, colorText: Color, containerColor: Color = MaterialTheme.colorScheme.tertiary) { Button( onClick = onClick, modifier = modifier .height(28.dp), colors = ButtonDefaults.buttonColors( contentColor = MaterialTheme.colorScheme.onTertiary, - containerColor = MaterialTheme.colorScheme.tertiary + containerColor = containerColor ), shape = MaterialTheme.shapes.large, - elevation = ButtonDefaults.buttonElevation( - defaultElevation = 6.dp - ), contentPadding = PaddingValues(horizontal = 12.dp), ) { Text( @@ -93,7 +91,7 @@ fun SmallButton(modifier: Modifier = Modifier, contentText: String, onClick: () style = MaterialTheme.typography.bodySmall.copy( fontWeight = FontWeight.Bold ), - color = MaterialTheme.colorScheme.primary + color = colorText ) } } @@ -115,7 +113,10 @@ fun ButtonPreview() { Spacer(modifier = Modifier.height(16.dp)) - SmallButton(contentText = "LOCATE NOW") + SmallButton( + contentText = "LOCATE NOW", + colorText = MaterialTheme.colorScheme.primary + ) } } } diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/CameraView.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/CameraView.kt new file mode 100644 index 0000000..61656fc --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/CameraView.kt @@ -0,0 +1,341 @@ +package com.capstone.techwasmark02.ui.component + +import android.net.Uri +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.Executor + +@Composable +fun CameraView( + outputDirectory: File, + executor: Executor, + onImageCaptured: (Uri) -> Unit, + onError: (ImageCaptureException) -> Unit +) { + val lensFacing = CameraSelector.LENS_FACING_BACK + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val preview = Preview.Builder().build() + val previewView = remember { + PreviewView(context) + } + + var photoPreviewUri by remember { + mutableStateOf(Uri.EMPTY) + } + val imageCapture: ImageCapture = remember { + ImageCapture.Builder().build() + } + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(lensFacing) + .build() + + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { galleryImageUri -> + if(galleryImageUri != null) { + photoPreviewUri = galleryImageUri + onImageCaptured(galleryImageUri) + } + } + ) + + LaunchedEffect(key1 = lensFacing) { + val cameraProvider = withContext(Dispatchers.IO) { + withContext(Dispatchers.Main) { + ProcessCameraProvider.getInstance(context) + }.get() + } + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture + ) + + preview.setSurfaceProvider(previewView.surfaceProvider) + } + + Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.fillMaxSize()) { + TechwasMark02Theme() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primary) + .padding(top = 16.dp) + ) { + Box(modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .background(Color.White) + ) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp, vertical = 8.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.LightGray), + contentAlignment = Alignment.BottomCenter, + ) { + + if (photoPreviewUri != Uri.EMPTY) { + AsyncImage( + modifier = Modifier + .fillMaxSize() + .blur(16.dp), + model = photoPreviewUri, + contentDescription = null, + contentScale = ContentScale.Crop, + alpha = 0.7f + ) + AsyncImage( + modifier = Modifier + .fillMaxSize(), + model = photoPreviewUri, + contentDescription = "Selected image", + contentScale = ContentScale.Fit + ) + } else { + AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Max) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(start = 20.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { galleryLauncher.launch("image/*") }, + modifier = Modifier + .clip(CircleShape) + .background(Color.White) + ) { + Icon(painter = painterResource(id = R.drawable.ic_gallery), contentDescription = null) + } + } + + Box( + Modifier + .clip(CircleShape) + ) { + Box( + modifier = Modifier + .size(62.dp) + .clip(CircleShape) + .background(Color.Transparent) + .border(2.dp, Color.White, CircleShape) + .clickable { + Log.i("kilo", "ON CLICK") + takePhoto( + filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS", + imageCapture = imageCapture, + outputDirectory = outputDirectory, + executor = executor, + onImageCaptured = { + photoPreviewUri = it + onImageCaptured(it) + }, + onError = onError + ) + }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .background(Color.White) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } +} + +private fun takePhoto( + filenameFormat: String, + imageCapture: ImageCapture, + outputDirectory: File, + executor: Executor, + onImageCaptured: (Uri) -> Unit, + onError: (ImageCaptureException) -> Unit +) { + + val photoFile = File( + outputDirectory, + SimpleDateFormat(filenameFormat, Locale.US).format(System.currentTimeMillis()) + ".jpg" + ) + + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + imageCapture.takePicture(outputOptions, executor, object: ImageCapture.OnImageSavedCallback { + override fun onError(exception: ImageCaptureException) { + Log.e("kilo", "Take photo error:", exception) + onError(exception) + } + + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val savedUri = Uri.fromFile(photoFile) + onImageCaptured(savedUri) + } + }) +} + +@androidx.compose.ui.tooling.preview.Preview (showBackground = true) +@Composable +fun CameraViewPreview() { + TechwasMark02Theme() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primary) + ) { + + Box(modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .background(Color.White) + ) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 20.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.LightGray), + contentAlignment = Alignment.BottomCenter, + ) { +// AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Max) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(start = 20.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { /*TODO*/ }, + modifier = Modifier + .clip(CircleShape) + .background(Color.White) + ) { + Icon(painter = painterResource(id = R.drawable.ic_gallery), contentDescription = null) + } + } + + Box( + Modifier + .clip(CircleShape) + ) { + Box( + modifier = Modifier + .size(62.dp) + .clip(CircleShape) + .background(Color.Transparent) + .border(2.dp, Color.White, CircleShape) + .clickable { +// Log.i("kilo", "ON CLICK") +// takePhoto( +// filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS", +// imageCapture = imageCapture, +// outputDirectory = outputDirectory, +// executor = executor, +// onImageCaptured = onImageCaptured, +// onError = onError +// ) + }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(52.dp) + .clip(CircleShape) + .background(Color.White) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/CatalogCard.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/CatalogCard.kt new file mode 100644 index 0000000..fb20ae5 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/CatalogCard.kt @@ -0,0 +1,144 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.Component +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlin.random.Random + +@Composable +fun CatalogCard( + modifier: Modifier = Modifier, + component: Component, +) { + + Box( + modifier = modifier + .width(300.dp) + .height(100.dp) + .shadow( + elevation = 6.dp, + shape = RoundedCornerShape(20.dp) + ) +// .clip(RoundedCornerShape(20.dp)) + ) { + Image( + painter = painterResource(id = R.drawable.img_bg_catalog_card), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + Row( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(vertical = 8.dp) + .padding(start = 16.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + text = component.name, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onPrimary + ) + + Text( + text = component.desc, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + + Box( + modifier = Modifier + .width(150.dp) + .fillMaxHeight() + .clip(RoundedCornerShape(topEnd = 20.dp, bottomEnd = 20.dp)) + .background(Color.LightGray) + ) { + val greenColor = Color(0xff8bcd54) + + AsyncImage( + model = component.imageExample, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + + Box( + + modifier = Modifier + .fillMaxSize() + .background( + Brush.horizontalGradient( + .0f to greenColor, + .4F to greenColor.copy(alpha = 0.4f), + .6F to Color.Transparent, + ) + ) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun CatalogCardPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + contentAlignment = Alignment.Center + ) { + CatalogCard( + component = Component( + desc = "A small portable personal computer that not very reliable and not very versatile.", + id = 1, + name = "Laptop", + imageExample = "https://picsum.photos/seed/${Random.nextInt()}/320/120" + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectBox.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectBox.kt new file mode 100644 index 0000000..3e51644 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectBox.kt @@ -0,0 +1,246 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.componentType.FeatureBoxType +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.capstone.techwasmark02.ui.theme.gray + +@Composable +fun FeatureBox( + modifier: Modifier = Modifier, + featureBoxType: FeatureBoxType, + onClick: () -> Unit +) { + Box( + modifier = modifier + .width(150.dp) + .height(160.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(10.dp) + ) + .clickable { onClick() } + ) { + Image( + painter = painterResource(id = featureBoxType.backGround), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = featureBoxType.title, + style = MaterialTheme.typography.titleLarge, + color = Color.White + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(34.dp), + painter = painterResource(id = featureBoxType.icon), + contentDescription = null, + tint = Color.White + ) + Spacer(modifier = Modifier.weight(1f)) + SmallButton( + contentText = featureBoxType.buttonTitle, + onClick = onClick, + colorText = featureBoxType.buttonColor + ) + } + } + } +} + +@Composable +fun FeatureBoxLarge( + modifier: Modifier = Modifier, + featureBoxType: FeatureBoxType, + onClick: () -> Unit +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(160.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(10.dp) + ) + .clickable { onClick() } + ) { + Image( + painter = painterResource(id = featureBoxType.backGround), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = featureBoxType.title, + style = MaterialTheme.typography.titleLarge.copy( + fontSize = 28.sp, + fontWeight = FontWeight.SemiBold + ), + color = Color.White, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(44.dp), + painter = painterResource(id = featureBoxType.icon), + contentDescription = null, + tint = Color.White + ) + Spacer(modifier = Modifier.weight(1f)) + SmallButton( + contentText = featureBoxType.buttonTitle, + onClick = onClick, + colorText = featureBoxType.buttonColor + ) + } + } + } +} + +@Composable +fun AboutUsBox(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .width(150.dp) + .height(160.dp) + .clip( + RoundedCornerShape( + 10.dp + ) + ) + .background(gray) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .border( + BorderStroke( + width = 2.dp, + color = Color.Black + ), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_question_mark), + contentDescription = null + ) + } + Text( + text = "About Us", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium + ) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun DetectBoxPreview() { + TechwasMark02Theme { + Box(modifier = Modifier.padding(20.dp)) { + Column(modifier = Modifier.fillMaxWidth()) { + + FeatureBoxLarge(featureBoxType = FeatureBoxType.Detection, onClick = {}) + + Spacer(modifier = Modifier.height(20.dp)) + + FeatureBox( + featureBoxType = FeatureBoxType.Detection, + onClick = {} + ) + + Spacer(modifier = Modifier.height(20.dp)) + + FeatureBox(featureBoxType = FeatureBoxType.Catalog, onClick = {}) + + Spacer(modifier = Modifier.height(20.dp)) + + FeatureBox(featureBoxType = FeatureBoxType.DropPoint, onClick = {}) + + Spacer(modifier = Modifier.height(20.dp)) + + AboutUsBox() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultBox.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultBox.kt new file mode 100644 index 0000000..fc9847e --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultBox.kt @@ -0,0 +1,291 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.lerp +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.Prediction +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DetectionsResultBox( + modifier: Modifier = Modifier, + predictionList: List, + updateSelectedPrediction: (Int) -> Unit +) { + val pagerState = rememberPagerState() + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + updateSelectedPrediction(page) + } + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(120.dp) + .shadow( + elevation = 6.dp, + shape = MaterialTheme.shapes.large + ) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 24.dp), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Box( + modifier = Modifier + .size(84.dp) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.primaryContainer) + ) + } + + VerticalPager( + pageCount = predictionList.size, + modifier = Modifier + .height(84.dp), + pageSpacing = (-50).dp, + state = pagerState, + ) { page -> + ResultListItem( + detectionResult = predictionList[page], + pagerState = pagerState, + page = page, + modifier = Modifier + ) + } + + Column( + modifier = Modifier + .fillMaxHeight() + ) { + val prevButtonVisible by remember { + derivedStateOf { + pagerState.currentPage > 0 + } + } + + val nextButtonVisible by remember { + derivedStateOf { + pagerState.currentPage < dummyDetectionResultList.size - 1 + } + } + + IconButton(onClick = { + val prevPageIndex = pagerState.currentPage - 1 + coroutineScope.launch { pagerState.animateScrollToPage(prevPageIndex) } + }, + enabled = prevButtonVisible, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_up), + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiary, + modifier = Modifier + .alpha(if (prevButtonVisible) 1f else 0.3f), + ) + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { + val nextPageIndex = pagerState.currentPage + 1 + coroutineScope.launch { pagerState.animateScrollToPage(nextPageIndex) } + }, + enabled = nextButtonVisible + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_down), + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiary, + modifier = Modifier + .alpha(if (nextButtonVisible) 1f else 0.3f) + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ResultListItem( + detectionResult: Prediction, + pagerState: PagerState, + page: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = detectionResult.componentName, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .graphicsLayer { + val pageOffset = ( + (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue + alpha = lerp( + start = 0.2f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + scaleX = lerp( + start = 0.6f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + scaleY = lerp( + start = 0.6f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + transformOrigin = TransformOrigin( + pivotFractionY = 0.4f, + pivotFractionX = 0.1f + ) + } + ) + Spacer(modifier = Modifier.weight(1f)) + + Box( + modifier = Modifier + .size(84.dp) + .background(Color.Transparent), + contentAlignment = Alignment.Center + ) { + Text( + text = "${detectionResult.componentValue.toInt()}%", + style = MaterialTheme.typography.headlineSmall.copy( + fontSize = 28.sp + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .graphicsLayer { + val pageOffset = ( + (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue + alpha = lerp( + start = 0.5f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + scaleX = lerp( + start = 0.7f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + scaleY = lerp( + start = 0.7f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + transformOrigin = TransformOrigin( + pivotFractionY = 0.4f, + pivotFractionX = 0.5f + ) + } + ) + } + } +} + + +@Preview (showBackground = false) +@Composable +fun DetectionsResultBoxPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + DetectionsResultBox( + predictionList = dummyDetectionResultList, + updateSelectedPrediction = {} + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview (showBackground = true) +@Composable +fun ResultListItemPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(20.dp) + ) { + ResultListItem( + detectionResult = Prediction( + componentId = 1, + componentName = "PCB", + componentValue = 80.988 + ), + pagerState = PagerState( + initialPage = 0, + initialPageOffsetFraction = 0f, + ), + page = 0 + ) + } + } +} + +private val dummyDetectionResultList: List = listOf( + Prediction( + componentId = 1, + componentName = "PCB", + componentValue = 80.00 + ), + Prediction( + componentId = 2, + componentName = "Monitor", + componentValue = 15.00 + ), + Prediction( + componentId = 3, + componentName = "Mouse", + componentValue = 5.00 + ) +) diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultSaveBottomSheet.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultSaveBottomSheet.kt new file mode 100644 index 0000000..7a3f657 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/DetectionsResultSaveBottomSheet.kt @@ -0,0 +1,57 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun DetectionsResultSaveBottomSheet(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .height(172.dp) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 20.dp) + .padding(top = 24.dp, bottom = 42.dp) + ) { + Text( + text = "Save your result", + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.height(36.dp)) + + DefaultButton( + contentText = "Save", + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) + } +} + +@Preview +@Composable +fun DetectionsResultSaveBottomSheetPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(vertical = 20.dp) + ) { + DetectionsResultSaveBottomSheet() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/ForumBox.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/ForumBox.kt new file mode 100644 index 0000000..3e87a7a --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/ForumBox.kt @@ -0,0 +1,135 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlin.random.Random + +@Composable +fun ForumBox( + modifier: Modifier = Modifier, + photoUrl: String? = null, + onClick: () -> Unit = {}, + title: String, + place: String, + desc: String + +) { + Box( + modifier = modifier + .height(80.dp) + .width(328.dp) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(20.dp) + ) + .background(Color.White) + .clip(RoundedCornerShape(20.dp)) + .clickable { + onClick() + } + ) { + Row( + modifier + .fillMaxSize() + .padding(end = 27.5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier + .width(100.dp) + .fillMaxHeight() + .clip(RoundedCornerShape( + topStart= 20.dp, + bottomStart= 20.dp + )), + painter = rememberAsyncImagePainter( + model = photoUrl ?: "https://picsum.photos/seed/${Random.nextInt()}/320/120", + placeholder = painterResource(id = R.drawable.place_holder), + ), + contentScale = ContentScale.Crop, + contentDescription = null + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 8.2.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) + Text( + text = desc, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Text( + text = place, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.SemiBold + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + overflow = TextOverflow.Ellipsis + ) + } + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun ForumBoxPreview() { + TechwasMark02Theme() { + Box(modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(20.dp)) { + ForumBox( + title = "Laptop Rusak", + desc = "Ini adalah akhir dunia", + place = "Bandung" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/HtmlText.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/HtmlText.kt new file mode 100644 index 0000000..da6a2d0 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/HtmlText.kt @@ -0,0 +1,69 @@ +package com.capstone.techwasmark02.ui.component + +import android.content.Context +import android.text.TextUtils +import android.util.TypedValue +import android.view.Gravity +import android.widget.TextView +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.res.ResourcesCompat +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.utils.html.fromHtml +import kotlin.math.max + +private const val SPACING_FIX = 3f + +@Composable +fun HtmlText( + modifier: Modifier = Modifier, + html: String, + textStyle: TextStyle = MaterialTheme.typography.bodyMedium, + maxLine: Int? = null +) { + AndroidView( + modifier = modifier, + update = { it.text = fromHtml(it.context, html) }, + factory = { context -> + val spacingReady = + max(textStyle.lineHeight.value - textStyle.fontSize.value - SPACING_FIX, 0f) + val extraSpacing = spToPx(spacingReady.toInt(), context) + val gravity = when (textStyle.textAlign) { + TextAlign.Center -> Gravity.CENTER + TextAlign.End -> Gravity.END + else -> Gravity.START + } + val fontResId = when (textStyle.fontWeight) { + FontWeight.Medium -> R.font.poppins_medium + else -> R.font.poppins_regular + } + val font = ResourcesCompat.getFont(context, fontResId) + + TextView(context).apply { + // general style + textSize = textStyle.fontSize.value + setLineSpacing(extraSpacing, 1f) + setTextColor(textStyle.color.toArgb()) + setGravity(gravity) + typeface = font + if (maxLine != null) { + maxLines = maxLine + ellipsize = TextUtils.TruncateAt.END + } + } + } + ) +} + +fun spToPx(sp: Int, context: Context): Float = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp.toFloat(), + context.resources.displayMetrics + ) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/MainTextField.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/MainTextField.kt index b42a779..37cfe76 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/MainTextField.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/MainTextField.kt @@ -1,24 +1,32 @@ package com.capstone.techwasmark02.ui.component +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.capstone.techwasmark02.R import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme @@ -31,33 +39,66 @@ fun DefaultTextField( labelText: String, placeHolderText: String ) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - label = { - Text( - text = labelText, - style = MaterialTheme.typography.bodyMedium - ) - }, - placeholder = { - Text( - text = placeHolderText, - style = MaterialTheme.typography.bodyMedium - ) - }, - singleLine = true, - shape = MaterialTheme.shapes.large, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = MaterialTheme.colorScheme.onTertiary, - containerColor = MaterialTheme.colorScheme.tertiary, - focusedLabelColor = MaterialTheme.colorScheme.onTertiary, - focusedBorderColor = MaterialTheme.colorScheme.onTertiary, - cursorColor = MaterialTheme.colorScheme.onTertiary, - ), - textStyle = MaterialTheme.typography.bodyMedium, + var hasFocus by remember { + mutableStateOf(false) + } + + val focusColor = if (hasFocus) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f) + + Box( modifier = modifier - ) + ) { + TextField( + value = value, + onValueChange = onValueChange, + placeholder = { + Text( + text = placeHolderText, + style = MaterialTheme.typography.bodyMedium + ) + }, + singleLine = true, + shape = MaterialTheme.shapes.large, + colors = TextFieldDefaults.textFieldColors( + textColor = if (hasFocus) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onTertiary, + containerColor = Color.Transparent, + focusedLabelColor = MaterialTheme.colorScheme.primary, + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + placeholderColor = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f), + focusedTrailingIconColor = focusColor + ), + textStyle = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.SemiBold + ), + modifier = modifier + .onFocusChanged { focusState -> hasFocus = focusState.hasFocus } + .border( + width = if (hasFocus) 3.dp else 1.5.dp, + color = if (hasFocus) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onTertiary.copy( + alpha = 0.6f + ), + shape = MaterialTheme.shapes.large + ), + ) + + Text( + text = labelText, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.sp + ), + modifier = Modifier + .offset( + x = 16.dp, + y = -(10.dp) + ) + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 4.dp), + color = focusColor + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -69,53 +110,85 @@ fun PasswordTextField( toggleShowPassword: () -> Unit, modifier: Modifier = Modifier ) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier, - label = { - Text( - text = "Password", - style = MaterialTheme.typography.bodyMedium - ) - }, - placeholder = { - Text( - text = "user password", - style = MaterialTheme.typography.bodyMedium - ) - }, - singleLine = true, - shape = MaterialTheme.shapes.large, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = MaterialTheme.colorScheme.onTertiary, - containerColor = MaterialTheme.colorScheme.tertiary, - focusedLabelColor = MaterialTheme.colorScheme.onTertiary, - focusedBorderColor = MaterialTheme.colorScheme.onTertiary, - cursorColor = MaterialTheme.colorScheme.onTertiary, - ), - textStyle = MaterialTheme.typography.bodyMedium, - visualTransformation = if (showPassword) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - trailingIcon = { - IconButton(onClick = toggleShowPassword ) { - if (showPassword) { - Icon( - painter = painterResource(id = R.drawable.ic_visibility_on), - contentDescription = null - ) - } else { - Icon( - painter = painterResource(id = R.drawable.ic_visibility_off), - contentDescription = null - ) + + var hasFocus by remember { + mutableStateOf(false) + } + + val focusColor = if (hasFocus) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f) + + Box( + modifier = Modifier + ) { + TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .onFocusChanged { focusState -> hasFocus = focusState.hasFocus } + .border( + width = if (hasFocus) 3.dp else 1.5.dp, + color = focusColor, + shape = MaterialTheme.shapes.large + ), + placeholder = { + Text( + text = "user password", + style = MaterialTheme.typography.bodyMedium + ) + }, + singleLine = true, + shape = MaterialTheme.shapes.large, + colors = TextFieldDefaults.textFieldColors( + textColor = focusColor, + containerColor = Color.Transparent, + focusedLabelColor = MaterialTheme.colorScheme.primary, + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + placeholderColor = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f), + focusedTrailingIconColor = focusColor, + unfocusedLabelColor = focusColor + ), + textStyle = MaterialTheme.typography.bodyMedium, + visualTransformation = if (showPassword) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = toggleShowPassword ) { + if (showPassword) { + Icon( + painter = painterResource(id = R.drawable.ic_visibility_on), + contentDescription = null + ) + } else { + Icon( + painter = painterResource(id = R.drawable.ic_visibility_off), + contentDescription = null + ) + } } } - } - ) + ) + + + Text( + text = "Password", + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.sp + ), + modifier = Modifier + .offset( + x = 16.dp, + y = -(10.dp) + ) + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 4.dp), + color = focusColor + ) + } } @Preview (showBackground = true) @@ -134,6 +207,7 @@ fun TextFieldPreview() { Column( modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .padding(20.dp) ) { DefaultTextField( diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/ProfileBox.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/ProfileBox.kt new file mode 100644 index 0000000..e1a8f2b --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/ProfileBox.kt @@ -0,0 +1,95 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ProfileBox( + modifier: Modifier = Modifier, + navigateToSetting: () -> Unit +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(120.dp) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 24.dp), + contentAlignment = Alignment.Center + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_language), + contentDescription = "Language", + tint = MaterialTheme.colorScheme.onTertiary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Language", + style = MaterialTheme.typography.labelMedium + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { navigateToSetting() }, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_settings), + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onTertiary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Settings", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onTertiary + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun ProfileBoxPreview() { + TechwasMark02Theme { + Box(modifier = Modifier.padding(20.dp)) { + ProfileBox( + navigateToSetting = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/SearchBox.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/SearchBox.kt new file mode 100644 index 0000000..50613f9 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/SearchBox.kt @@ -0,0 +1,108 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBox( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, +) { + + Box( + modifier = modifier + .fillMaxWidth() + .height(50.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(20.dp) + ) + .background(Color.White) + .clip(RoundedCornerShape(20.dp)), + contentAlignment = Alignment.Center + ) { + TextField( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + value = value, + onValueChange = onValueChange, + placeholder = { + Text( + text = "Search", + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray + ) + }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.White, + textColor = Color.Gray, + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + trailingIcon = { + IconButton(onClick = { /*TODO*/ }) { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(end = 12.dp) + ) + } + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SearchBoxPreview() { + TechwasMark02Theme { + Box(modifier = Modifier.padding(20.dp)) { + + var value by remember { + mutableStateOf("") + } + + SearchBox( + value = value, + onValueChange = { newValue -> + value = newValue + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/SelectableText.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/SelectableText.kt new file mode 100644 index 0000000..bceaa2d --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/SelectableText.kt @@ -0,0 +1,71 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capstone.techwasmark02.ui.componentType.ArticleFilterType +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun SelectableText( + modifier: Modifier = Modifier, + filterType: ArticleFilterType, + selected: Boolean, + onClick: () -> Unit +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .clickable { + onClick() + } + .background( + if (selected) MaterialTheme.colorScheme.primary else Color.LightGray.copy(alpha = 0.6f) + ) + .padding(vertical = 4.dp, horizontal = 8.dp), + ) { + Text( + maxLines = 1, + text = filterType.type, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier, + color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onBackground + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SelectableTextPreview() { + TechwasMark02Theme { + Row(modifier = Modifier.padding(20.dp), verticalAlignment = Alignment.CenterVertically){ + SelectableText( + filterType = ArticleFilterType.WashingMachine, + selected = true, + onClick = {} + ) + + Spacer(modifier = Modifier.width(20.dp)) + + SelectableText( + filterType = ArticleFilterType.General, + selected = false, + onClick = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/SettingItem.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/SettingItem.kt new file mode 100644 index 0000000..3bab856 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/SettingItem.kt @@ -0,0 +1,88 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capstone.techwasmark02.ui.componentType.SettingItemType +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun SettingItem( + modifier: Modifier = Modifier, + icon: Int, + title: String +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(10.dp) + ) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = icon), + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = title + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onTertiary + ) + } + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun SettingItemPreview() { + TechwasMark02Theme { + Box(modifier = Modifier.padding(20.dp)) { + SettingItem( + modifier = Modifier.fillMaxWidth(), + icon = SettingItemType.Password.icon, + title = SettingItemType.Password.title + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/TopBar.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/TopBar.kt index b566d7b..ca0d008 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/TopBar.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/TopBar.kt @@ -1,10 +1,9 @@ package com.capstone.techwasmark02.ui.component +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api @@ -18,17 +17,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.capstone.techwasmark02.R import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun InverseTopBar(onClickNavigationIcon: () -> Unit, modifier: Modifier = Modifier) { +fun InverseTopBar(onClickNavigationIcon: () -> Unit, modifier: Modifier = Modifier, pageTitle: String = "") { + BackHandler(true) { + onClickNavigationIcon() + } + TopAppBar( navigationIcon = { IconButton( @@ -38,12 +39,22 @@ fun InverseTopBar(onClickNavigationIcon: () -> Unit, modifier: Modifier = Modifi painter = painterResource(id = R.drawable.ic_arrow_back), contentDescription = null, tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(24.dp) ) } }, title = {}, - actions = {}, + actions = { + Text( + text = pageTitle, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding(end = 20.dp), + ) + }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = Color.Transparent, ), @@ -63,21 +74,21 @@ fun DefaultTopBar(pageTitle: String = "", onClickNavigationIcon: () -> Unit, mod painter = painterResource(id = R.drawable.ic_arrow_back), contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(24.dp) ) } }, title = {}, actions = { - Text( - text = pageTitle, - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal - ), - modifier = Modifier - .padding(end = 20.dp) - ) + Text( + text = pageTitle, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding(end = 20.dp) + ) }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, @@ -86,6 +97,41 @@ fun DefaultTopBar(pageTitle: String = "", onClickNavigationIcon: () -> Unit, mod ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TransparentTopBar(pageTitle: String = "", onClickNavigationIcon: () -> Unit, modifier: Modifier = Modifier) { + TopAppBar( + navigationIcon = { + IconButton( + onClick = onClickNavigationIcon, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(24.dp) + ) + } + }, + title = {}, + actions = { + Text( + text = pageTitle, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding(end = 20.dp) + ) + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = Color.Transparent, + ), + modifier = modifier + ) +} + @Preview (showBackground = true) @Composable fun InverseTopBarPreview() { @@ -116,4 +162,22 @@ fun DefaultTopBarPreview() { ) } } +} + +@Preview (showBackground = true) +@Composable +fun TransparantTopBarPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.onBackground) + .padding(20.dp) + ) { + TransparentTopBar( + pageTitle = "Detail", + onClickNavigationIcon = {} + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentBottomSheet.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentBottomSheet.kt new file mode 100644 index 0000000..d6da43c --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentBottomSheet.kt @@ -0,0 +1,93 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun UsableComponentBottomSheet( + modifier: Modifier = Modifier, + smallPart: SmallPart +) { + Column( + modifier = modifier + .fillMaxWidth() + .height(360.dp) + .background(MaterialTheme.colorScheme.tertiary) + .padding(top = 32.dp, bottom = 42.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Text( + text = smallPart.name, + style = MaterialTheme.typography.labelLarge + ) + + Text( + text = smallPart.description, + style = MaterialTheme.typography.bodySmall + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.large) + .background(Color.LightGray) + ) { + AsyncImage( + model = smallPart.imageURL, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + } + } + } + +} + +@Preview (showBackground = true) +@Composable +fun UsableComponentBottomSheetPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(vertical = 20.dp) + ) { + UsableComponentBottomSheet( + smallPart = SmallPart( + compID = 0, + description = "jkdfau adkjfaku aksdufka ufausd f", + id = 0, + imageURL = "", + name = "RAM" + ) + ) + } + } +} diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentItem.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentItem.kt new file mode 100644 index 0000000..7c338c9 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/UsableComponentItem.kt @@ -0,0 +1,98 @@ +package com.capstone.techwasmark02.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun UsableComponentItem( + modifier: Modifier = Modifier, + onClick: () -> Unit, + usableComponent: SmallPart +) { + Row( + modifier = modifier + .width(152.dp) + .height(60.dp) + .shadow( + elevation = 6.dp, + shape = MaterialTheme.shapes.large + ) + .background(MaterialTheme.colorScheme.tertiary) + .clickable { onClick() } + ) { + AsyncImage( + model = usableComponent.imageURL, + contentDescription = null, + placeholder = painterResource(id = R.drawable.place_holder), + contentScale = ContentScale.Crop, + modifier = Modifier + .width(55.dp) + .fillMaxHeight() + ) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = usableComponent.name, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + } +} + +@Preview (showBackground = true) +@Composable +fun UsableCompoenentItemPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(20.dp), + contentAlignment = Alignment.Center + ) { + UsableComponentItem( + onClick = {}, + usableComponent = SmallPart( + compID = 3, + description = "", + id = 2, + imageURL = "", + name = "RAM" + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/component/UserGreet.kt b/app/src/main/java/com/capstone/techwasmark02/ui/component/UserGreet.kt index 38255be..389f798 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/component/UserGreet.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/component/UserGreet.kt @@ -19,6 +19,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.capstone.techwasmark02.R @@ -40,16 +43,18 @@ fun UserGreet( Row( modifier = Modifier ) { + val text = buildAnnotatedString { + append("Hello,\n") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append("$userName.") + } + } + Text( - text = "Hello, ", + text = text, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.inverseSurface, ) - Text( - text = "$userName.", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.primary - ) } Box( diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/componentType/ArticleFilterType.kt b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/ArticleFilterType.kt new file mode 100644 index 0000000..adf78bc --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/ArticleFilterType.kt @@ -0,0 +1,37 @@ +package com.capstone.techwasmark02.ui.componentType + +sealed class ArticleFilterType(val type: String, val id: Int) { + + object General: ArticleFilterType(type = "General", id = 0) + + object Battery: ArticleFilterType(type = "Battery", id = 1) + + object Cable: ArticleFilterType(type = "Cable", id = 2) + + object CrtTv: ArticleFilterType(type = "CRT TV", id = 3) + + object EKettle: ArticleFilterType(type = "E-kettle", id = 4) + + object Refrigerator: ArticleFilterType(type = "Refrigerator", id = 5) + + object Keyboard: ArticleFilterType(type = "Keyboard", id = 6) + + object Laptop: ArticleFilterType(type = "Laptop", id = 7) + + object LightBulb: ArticleFilterType(type = "Light Bulb", id = 8) + + object Monitor: ArticleFilterType(type = "Monitor", id = 9) + + object Mouse: ArticleFilterType(type = "Mouse", id = 10) + + object PCB: ArticleFilterType(type = "PCB", id = 11) + + object Printer: ArticleFilterType(type = "Printer", id = 12) + + object RiceCooker: ArticleFilterType(type = "Rice Cooker", id = 13) + + object WashingMachine: ArticleFilterType(type = "Washing Machine", id = 14) + + object Phone: ArticleFilterType(type = "Phone", id = 15) + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/componentType/BottomBarItemType.kt b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/BottomBarItemType.kt index 8faca17..551b166 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/componentType/BottomBarItemType.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/BottomBarItemType.kt @@ -2,9 +2,9 @@ package com.capstone.techwasmark02.ui.componentType import com.capstone.techwasmark02.R -sealed class BottomBarItemType(val title: String, val icon: Int) { - object Home: BottomBarItemType(title = "Home", icon = R.drawable.ic_home) - object Forum: BottomBarItemType(title = "Forum", icon = R.drawable.ic_forum) - object Article: BottomBarItemType(title = "Article", icon = R.drawable.ic_article) - object Profile: BottomBarItemType(title = "Profile", icon = R.drawable.ic_profile) +sealed class BottomBarItemType(val title: String, val icon: Int, val pageIndex: Int) { + object Home: BottomBarItemType(title = "Home", icon = R.drawable.ic_home, pageIndex = 0) + object Forum: BottomBarItemType(title = "Forum", icon = R.drawable.ic_forum, pageIndex = 1) + object Article: BottomBarItemType(title = "Article", icon = R.drawable.ic_article, pageIndex = 2) + object Profile: BottomBarItemType(title = "Profile", icon = R.drawable.ic_profile, pageIndex = 3) } diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/componentType/FeatureBoxType.kt b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/FeatureBoxType.kt new file mode 100644 index 0000000..7eeef60 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/FeatureBoxType.kt @@ -0,0 +1,13 @@ +package com.capstone.techwasmark02.ui.componentType + +import androidx.compose.ui.graphics.Color +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.theme.Green35 +import com.capstone.techwasmark02.ui.theme.purple +import com.capstone.techwasmark02.ui.theme.sakura + +sealed class FeatureBoxType(val title: String, val buttonTitle: String, val icon: Int, val backGround: Int, val buttonColor: Color) { + object Detection: FeatureBoxType(title = "Detect\ne-waste", buttonTitle = "Detect", icon = R.drawable.ic_center_focus, backGround = R.drawable.img_bg_green_large, buttonColor = Green35) + object Catalog: FeatureBoxType(title = "E-waste catalog", buttonTitle = "Open", icon = R.drawable.ic_menu_book, backGround = R.drawable.img_bg_purple, buttonColor = purple) + object DropPoint: FeatureBoxType(title = "Nearby drop point", buttonTitle = "Locate", icon = R.drawable.ic_location_on, backGround = R.drawable.img_bg_sakura, buttonColor = sakura) +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/componentType/SettingItemType.kt b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/SettingItemType.kt new file mode 100644 index 0000000..2473e93 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/componentType/SettingItemType.kt @@ -0,0 +1,14 @@ +package com.capstone.techwasmark02.ui.componentType + +import com.capstone.techwasmark02.R + +sealed class SettingItemType(val title: String, val icon: Int) { + + object Password: SettingItemType(title = "Security", icon = R.drawable.ic_shield) + + object Comment: SettingItemType(title = "Comments", icon = R.drawable.ic_chat_buble) + + object Notification: SettingItemType(title = "Notifications", icon = R.drawable.ic_fill_notification) + + object Language: SettingItemType(title = "Language", icon = R.drawable.ic_language) +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/navigation/Screen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/navigation/Screen.kt new file mode 100644 index 0000000..9312814 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/navigation/Screen.kt @@ -0,0 +1,292 @@ +package com.capstone.techwasmark02.ui.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.navigation.NavBackStackEntry + +@OptIn(ExperimentalAnimationApi::class) +sealed class Screen constructor(val route: String, val enterTransition: (AnimatedContentScope.() -> EnterTransition?), val exitTransition: (AnimatedContentScope.() -> ExitTransition?)) { + object OnBoarding: Screen( + route = "OnBoarding", + enterTransition = { + fadeIn(animationSpec = tween(700)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object SignIn: Screen( + route = "SignIn", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Left, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(700)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object SignUp: Screen( + route = "SignUp", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Left, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(700)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object Main: Screen( + route = "Main", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Left, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(300)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Up, animationSpec = tween(700)) + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object Home: Screen( + route = "home", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { +// slideOutOfContainer( +// AnimatedContentScope.SlideDirection.Left, animationSpec = tween( +// durationMillis = 700, +// ) +// ) + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object Forum: Screen( + route = "forum", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Left, animationSpec = tween( + durationMillis = 700, + ) + ) + } + ) + + object SingleForum: Screen( + route = "SingleForum", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Left, animationSpec = tween( + durationMillis = 700, + ) + ) + } + ) + + object CreateForum: Screen( + route = "CreateForum", + enterTransition = { + fadeIn( + animationSpec = tween(700) + ) + }, + exitTransition = { + fadeOut( + animationSpec = tween(700) + ) + } + ) + + object Profile: Screen( + route = "profile", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Left, animationSpec = tween( + durationMillis = 700, + ) + ) + } + ) + + object Setting: Screen( + route = "setting", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Left, animationSpec = tween( + durationMillis = 700, + ) + ) + } + ) + + object Camera: Screen( + route = "camera", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Up, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(1000)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Down, animationSpec = tween(700)) + fadeOut(animationSpec = tween(1000)) + } + ) + + object Catalog: Screen( + route = "catalog", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Left, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(300)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + fadeOut(animationSpec = tween(300)) + } + ) + + object SingleCatalog: Screen( + route = "singleCatalog", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + } + ) + + object DetectionResult: Screen( + route = "detectionResult", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + } + ) + + object Article: Screen( + route = "article", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + } + ) + + object SingleArticle: Screen( + route = "singleArticle", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + } + ) + + object Splash: Screen( + route = "splash", + enterTransition = { +// slideIntoContainer( +// AnimatedContentScope.SlideDirection.Left, +// animationSpec = tween(700) +// ) + fadeIn(animationSpec = tween(300)) + }, + exitTransition = { +// slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700)) + fadeOut(animationSpec = tween(300)) + } + ) + + object Maps: Screen( + route = "maps", + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = tween(700) + ) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween( + transitionDuration)) + } + ) +} + +private const val transitionDuration = 300 \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreen.kt new file mode 100644 index 0000000..66f9b5a --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreen.kt @@ -0,0 +1,261 @@ +package com.capstone.techwasmark02.ui.screen.article + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.ArticleCardSmall +import com.capstone.techwasmark02.ui.component.SearchBox +import com.capstone.techwasmark02.ui.component.SelectableText +import com.capstone.techwasmark02.ui.componentType.ArticleFilterType +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ArticleScreen( + viewModel: ArticleScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + val articleList by viewModel.articleList.collectAsState() + + ArticleContent( + viewModel = viewModel, + articleList = articleList, + navigateToSingleArticle = { navController.navigate("${Screen.SingleArticle.route}/$it") }, + ) +} + +@Composable +fun ArticleContent( + viewModel: ArticleScreenViewModel, + articleList: UiState?, + navigateToSingleArticle: (idArticle: Int) -> Unit, +) { + + var inputValue by remember { + mutableStateOf("") + } + + val filterTypeList = listOf( + ArticleFilterType.General, + ArticleFilterType.Battery, + ArticleFilterType.Cable, + ArticleFilterType.CrtTv, + ArticleFilterType.EKettle, + ArticleFilterType.Refrigerator, + ArticleFilterType.Keyboard, + ArticleFilterType.Laptop, + ArticleFilterType.LightBulb, + ArticleFilterType.Monitor, + ArticleFilterType.Mouse, + ArticleFilterType.PCB, + ArticleFilterType.Printer, + ArticleFilterType.RiceCooker, + ArticleFilterType.WashingMachine, + ArticleFilterType.Phone + ) + + var selectedFilter by remember { + mutableStateOf(filterTypeList.firstOrNull()) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp, bottom = 80.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + Text( + text = "Articles", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Check out these articles!", + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SearchBox( + value = inputValue, + onValueChange = { newValue -> + inputValue = newValue + if(inputValue.isEmpty()) { + viewModel.getAllFilterArticle(selectedFilter?.id ?: 0) + } else { + viewModel.getArticleByName(inputValue, selectedFilter ?: ArticleFilterType.General) + } + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + + LazyRow( + modifier = Modifier.height(48.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items( + items = filterTypeList, + ) { item -> + SelectableText( + filterType = item, + selected = item == selectedFilter, + onClick = { + selectedFilter = item + viewModel.getAllFilterArticle(item.id) + } + ) + } + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + if (articleList != null) { + when (articleList) { + is UiState.Loading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + articleList.message?.let { + Text(text = it) + } + } + is UiState.Success -> { + val componentListArticle = articleList.data?.articleList + if(!componentListArticle.isNullOrEmpty()) { + LazyVerticalGrid( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(bottom = 20.dp), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(componentListArticle) { item -> + ArticleCardSmall( + modifier = Modifier + .width(150.dp) + .clickable { + item?.id?.let { navigateToSingleArticle(it) } + }, + imgUrl = item?.articleImageURL, + title = item?.name, + description = item?.desc, + ) + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "There's no related article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + + } + } + } + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + ) { + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Preview(showBackground = true) +@Composable +fun ArticleScreenPreview() { + TechwasMark02Theme { +// ArticleContent( +// articleList = UiState.Loading(), +// navigateToHome = {}, +// navigateToArticle = {}, +// navigateToForum = {}, +// navigateToProfile = {} +// ) + } +} diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreenViewModel.kt new file mode 100644 index 0000000..e9ca610 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/article/ArticleScreenViewModel.kt @@ -0,0 +1,90 @@ +package com.capstone.techwasmark02.ui.screen.article + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.componentType.ArticleFilterType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ArticleScreenViewModel @Inject constructor( + private val articleRepository: TechwasArticleRepository +): ViewModel() { + + private val _articleList: MutableStateFlow?> = MutableStateFlow(null) + val articleList = _articleList.asStateFlow() + + init { + viewModelScope.launch { + getAllFilterArticle(0) + } + } + + fun getAllFilterArticle(id: Int) { + _articleList.value = UiState.Loading() + viewModelScope.launch { + val result = if(id == 0) { + articleRepository.getAllArticle() + } else { + articleRepository.getArticleByComponentId(userToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySUQiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwaXJ5IjoxNjg2MDQ5NDcwLjc1NjgyMTR9.eJfMxHb3UsQu-kyzyzN_3PdV8OvvwTmD8vOyoTRENyQ'", id = id) + } + when(result) { + is UiState.Success -> { + _articleList.value = result + } + is UiState.Error -> { + _articleList.value = result + } + else -> { + // do nothing + } + } + } + } + + fun getArticleByName(name: String, filterType: ArticleFilterType) { + _articleList.value = UiState.Loading() + viewModelScope.launch { + if (name.isEmpty()) { // input kosong fetch article by filter + getAllFilterArticle(filterType.id) + } else { + val result = if(filterType.id == 0) { + articleRepository.getAllArticle() + } else { + articleRepository.getArticleByComponentId(userToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySUQiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwaXJ5IjoxNjg2MDQ5NDcwLjc1NjgyMTR9.eJfMxHb3UsQu-kyzyzN_3PdV8OvvwTmD8vOyoTRENyQ'", id = filterType.id) + } + when(result) { + is UiState.Success -> { + val filteredArticles = if (filterType.id == 0) { // general + result.data?.articleList?.filter { + it?.name?.contains(name, ignoreCase = true) == true + } + } else { + result.data?.articleList?.filter { + it?.name?.contains(name, ignoreCase = true) == true && it.componentId == filterType.id + } + } + val filteredResult = ArticleResultResponse( + articleList = filteredArticles.orEmpty(), + error = result.message, + message = result.message + ) + _articleList.value = UiState.Success(filteredResult) + } + is UiState.Error -> { + _articleList.value = result + } + else -> { + // do nothing + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreen.kt new file mode 100644 index 0000000..1be1cba --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreen.kt @@ -0,0 +1,195 @@ +package com.capstone.techwasmark02.ui.screen.camera + +import android.content.Context +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.DetectionsResultResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.CameraView +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import java.io.File +import java.util.concurrent.Executors + +@Composable +fun CameraScreen( + viewModel: CameraScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + + val photoUri by viewModel.photoUri.collectAsState() + val predictImageState by viewModel.predictImageState.collectAsState() + + CameraContent( + photoUri = photoUri, + predictImageState = predictImageState, + updatePhotoUri = { viewModel.updatePhotoUri(it) }, + predictImage = { viewModel.predictImage(it)}, + navigateToResult = { uri, result -> + navController.navigate("detectionResult/$uri/$result") + } + ) { viewModel.onSuccessPredictImage() } +} + +@Composable +fun CameraContent( + photoUri: Uri?, + predictImageState: UiState?, + updatePhotoUri: (Uri) -> Unit, + predictImage: (Context) -> Unit, + navigateToResult: (uri: String, result: String) -> Unit, + onSuccessPredictImage:() -> Unit +) { + + val context = LocalContext.current + val outputDirectory = getOutputDirectory(context) + val cameraExecutor = Executors.newSingleThreadExecutor() + + var imageIsTaken by remember { + mutableStateOf(false) + } + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val adapter = moshi.adapter(DetectionsResultResponse::class.java) + + LaunchedEffect(key1 = imageIsTaken) { + if (imageIsTaken) { + predictImage(context) + } + } + + LaunchedEffect(key1 = predictImageState) { + if (predictImageState != null) { + when (predictImageState) { + is UiState.Success -> { + val predictResult = Uri.encode(adapter.toJson(predictImageState.data)) +// Toast.makeText(context,"Result: $predictResult", Toast.LENGTH_SHORT).show() +// val predictResult = predictImageState.data?.predictions?.let { +// getAndSortResult( +// it +// ) +// } + + val stringUri = Uri.encode(photoUri.toString()) + onSuccessPredictImage() + if (predictResult != null) { + navigateToResult(stringUri, predictResult) + } +// navigateToResult(stringUri, "alo") + } + is UiState.Error -> { + val predictResult = predictImageState.message + Toast.makeText(context,"$predictResult", Toast.LENGTH_SHORT).show() + imageIsTaken = false + } + is UiState.Loading -> { + Toast.makeText(context,"Your e-waste is being predicted", Toast.LENGTH_SHORT).show() + } + } + } + } + Box( + modifier = Modifier + .fillMaxSize() + ) { + CameraView( + outputDirectory = outputDirectory, + executor = cameraExecutor, + onImageCaptured = { uri -> + updatePhotoUri(uri) + imageIsTaken = true + }, + onError = { Log.e("zhahrany", "View error:", it)} + ) + + if (predictImageState != null) { + when (predictImageState) { + is UiState.Success, is UiState.Error -> { + // do nothing + } + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + + CircularProgressIndicator( + color = Color.White + ) + } + } + } + } + } + +} + +private fun getOutputDirectory(context: Context): File { + val cacheDir = context.externalCacheDirs.firstOrNull()?.let { + File(it, context.resources.getString(R.string.app_name)).apply { mkdirs() } + } + + return if (cacheDir != null && cacheDir.exists()) cacheDir else context.filesDir +} + +//private fun getAndSortResult(detectionResult: Predictions): String { +// val resultComponentList = emptyList().toMutableList() +// for (property in Predictions::class.memberProperties) { +// if (property.get(detectionResult) != null) { +// val resultComponentItem = ResultComponentType( +// name = property.name, +// percentage = property.get(detectionResult).toString().toDouble() +// ) +// resultComponentList.add(resultComponentItem) +// } +// } +// +// val sortedResultComponentList = resultComponentList.sortedWith(compareBy({it.percentage})).reversed() +// +// val stringResultList = emptyList().toMutableList() +// +// sortedResultComponentList.forEach { componentItem -> +// val stringResultItem = "${componentItem.name}: ${componentItem.percentage}" +// stringResultList.add(stringResultItem) +// } +// +// return stringResultList.joinToString(separator = ",") +// +//} + +@Preview(showBackground = true) +@Composable +fun CameraScreenPreview() { + TechwasMark02Theme() { + CameraContent( + photoUri = null, + updatePhotoUri = {}, + navigateToResult = { uri, result -> }, + predictImage = {}, + predictImageState = null + ) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreenViewModel.kt new file mode 100644 index 0000000..814b773 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/camera/CameraScreenViewModel.kt @@ -0,0 +1,72 @@ +package com.capstone.techwasmark02.ui.screen.camera + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.DetectionsResultResponse +import com.capstone.techwasmark02.repository.TechwasPredictionApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +@HiltViewModel +class CameraScreenViewModel @Inject constructor( + private val predictionApiRepository: TechwasPredictionApiRepository +): ViewModel() { + + private val _photoUri: MutableStateFlow = MutableStateFlow(null) + val photoUri = _photoUri.asStateFlow() + + private val _predictImageState: MutableStateFlow?> = MutableStateFlow(null) + val predictImageState = _predictImageState.asStateFlow() + + fun updatePhotoUri(newUri: Uri) { + _photoUri.value = newUri + } + + fun predictImage(context: Context) { + _predictImageState.value = UiState.Loading() + + val imageFileToUpload = _photoUri.value?.let { convertUriToFile(context = context, uri = it) } + viewModelScope.launch { + _predictImageState.value = imageFileToUpload?.let { + predictionApiRepository.predictWaste(it) + } + } + } + + fun onSuccessPredictImage() { + _predictImageState.value = null + } + +} + +private fun convertUriToFile(context: Context, uri: Uri): File? { + val inputStream = context.contentResolver.openInputStream(uri) + inputStream?.let { + val file = createTempFile(context, getFileExtension(uri)) + val outputStream = FileOutputStream(file) + inputStream.copyTo(outputStream) + outputStream.close() + inputStream.close() + return file + } + return null +} + +private fun createTempFile(context: Context, fileExtension: String): File { + val directory = context.cacheDir + return File.createTempFile("temp", ".$fileExtension", directory) +} + +private fun getFileExtension(uri: Uri): String { + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(uri.toString()) + return extension ?: "jpg" // Default to "jpg" if extension is null +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreen.kt new file mode 100644 index 0000000..b9be8ee --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreen.kt @@ -0,0 +1,192 @@ +package com.capstone.techwasmark02.ui.screen.catalog + +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.data.remote.response.Component +import com.capstone.techwasmark02.data.remote.response.ComponentsResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.CatalogCard +import com.capstone.techwasmark02.ui.component.InverseTopBar +import com.capstone.techwasmark02.ui.component.SearchBox +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +@Composable +fun CatalogScreen( + viewModel: CatalogScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + val componentsState by viewModel.componentState.collectAsState() + val searchBoxValue by viewModel.searchBoxValue.collectAsState() + + CatalogContent( + componentsState = componentsState, + navigateToSingleComponent = { navController.navigate("${Screen.SingleCatalog.route}/$it") }, + searchBoxValue = searchBoxValue, + onSearchBoxValueChange = { viewModel.updateSearchBoxValue(it) }, + navigateBackToMain = { navController.navigate("${Screen.Main.route}/0") } + ) +} + +@Composable +fun CatalogContent( + componentsState: UiState?, + navigateToSingleComponent: (component: String) -> Unit, + searchBoxValue: String, + onSearchBoxValueChange: (String) -> Unit, + navigateBackToMain: () -> Unit +) { + + BackHandler(true) { + navigateBackToMain() + } + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val adapter = moshi.adapter(Component::class.java) + + Box( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .padding(top = 60.dp) + ) { + Text( + text = "Catalog", + style = MaterialTheme.typography.headlineSmall + ) + Text( + text = "Find your e-waste component here", + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.height(10.dp)) + + SearchBox(onValueChange = onSearchBoxValueChange, value = searchBoxValue) + + Spacer(modifier = Modifier.height(16.dp)) + + if (componentsState != null) { + when(componentsState) { + is UiState.Success -> { + val components = componentsState.data?.components + + var filteredComponents by remember { + mutableStateOf(components) + } + + LaunchedEffect(key1 = searchBoxValue) { + if (components != null) { + filteredComponents = searchComponent( + componentList = components, + searchBoxValue = searchBoxValue + ) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + filteredComponents?.size?.let { + items( + count = it, + ) {index -> + CatalogCard( + component = filteredComponents!![index], + modifier = Modifier + .fillMaxWidth() + .clickable { + val componentJson = + Uri.encode(adapter.toJson(filteredComponents!![index])) + navigateToSingleComponent(componentJson) + }, + ) + } + } + } + } + is UiState.Error -> { + + } + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + } + + InverseTopBar( + onClickNavigationIcon = navigateBackToMain, + pageTitle = "Catalog", + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) + } +} + +private fun searchComponent(componentList: List, searchBoxValue: String) : List { + if (searchBoxValue == "") { + return componentList + } + return componentList.filter { component -> + component.name.contains(searchBoxValue, ignoreCase = true) + } +} + +@Preview (showBackground = true) +@Composable +fun CatalogScreenPreview() { + TechwasMark02Theme { + CatalogContent( + componentsState = UiState.Loading(), + navigateToSingleComponent = {}, + searchBoxValue = "", + onSearchBoxValueChange = {}, + navigateBackToMain = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreenViewModel.kt new file mode 100644 index 0000000..3175b1c --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalog/CatalogScreenViewModel.kt @@ -0,0 +1,37 @@ +package com.capstone.techwasmark02.ui.screen.catalog + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ComponentsResponse +import com.capstone.techwasmark02.repository.TechwasComponentApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CatalogScreenViewModel @Inject constructor( + private val componentApiRepository: TechwasComponentApiRepository +): ViewModel() { + + private val _componentsState: MutableStateFlow?> = MutableStateFlow(null) + val componentState = _componentsState.asStateFlow() + + private val _searchBoxValue: MutableStateFlow = MutableStateFlow("") + val searchBoxValue = _searchBoxValue.asStateFlow() + + fun updateSearchBoxValue(newValue: String) { + _searchBoxValue.value = newValue + } + + init { + _componentsState.value = UiState.Loading() + + viewModelScope.launch { + _componentsState.value = componentApiRepository.fetchComponents() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreen.kt new file mode 100644 index 0000000..103ea24 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreen.kt @@ -0,0 +1,462 @@ +package com.capstone.techwasmark02.ui.screen.catalogSingleComponent + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.AsyncImage +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.Component +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.ArticleCardBig +import com.capstone.techwasmark02.ui.component.DefaultTopBar +import com.capstone.techwasmark02.ui.component.UsableComponentBottomSheet +import com.capstone.techwasmark02.ui.component.UsableComponentItem +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.launch + +@Composable +fun CatalogSingleComponentScreen( + viewModel: CatalogSingleComponentScreenViewModel = hiltViewModel(), + componentJson: String, + navController: NavHostController +) { + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val adapter = moshi.adapter(Component::class.java) + val component = adapter.fromJson(componentJson) + + val usableComponentsState by viewModel.usableComponentsState.collectAsState() + val usableComponentList by viewModel.usableComponentList.collectAsState() + val relatedArticleListState by viewModel.relatedArticleListState.collectAsState() + + if (component != null) { + CatalogSingleComponentContent( + component = component, + usableComponentsState = usableComponentsState, + updateUsableComponentsState = { viewModel.updateUsableComponentsState(it) }, + usableComponentList = usableComponentList, + updateUsableComponentsList = { viewModel.updateUsableComponentList(it) }, + relatedArticleListState = relatedArticleListState, + updateRelatedArticleListState = { viewModel.updateRelatedArticleListState(it) }, + navigateToSingleArticle = { navController.navigate("${Screen.SingleArticle.route}/$it") }, + navigateBackToCatalog = { navController.popBackStack() } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun CatalogSingleComponentContent( + component: Component, + usableComponentsState: UiState?, + updateUsableComponentsState: (Int) -> Unit, + usableComponentList: List, + updateUsableComponentsList: (List) -> Unit, + relatedArticleListState: UiState?, + updateRelatedArticleListState: (Int) -> Unit, + navigateToSingleArticle: (idArticle: Int) -> Unit, + navigateBackToCatalog: () -> Unit + ) { + + val modalSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded}, + skipHalfExpanded = false + ) + + val coroutineScope = rememberCoroutineScope() + + var currentlyClickedUsableComponent by remember { + mutableStateOf(0) + } + + var isBottomSheetOpen by remember { + mutableStateOf(false) + } + + LaunchedEffect(Unit) { + updateUsableComponentsState(component.id) + updateRelatedArticleListState(component.id) + } + + BackHandler(isBottomSheetOpen) { + if (isBottomSheetOpen) { + coroutineScope.launch { + modalSheetState.hide() + isBottomSheetOpen = false + } + } + } + + ModalBottomSheetLayout( + sheetState = modalSheetState, + sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + sheetContent = { + if (usableComponentList.isNotEmpty()) { + UsableComponentBottomSheet( + smallPart = usableComponentList[currentlyClickedUsableComponent] + ) + } + } + ) { + Scaffold( + topBar = { + DefaultTopBar( + pageTitle = "Result", + onClickNavigationIcon = { navigateBackToCatalog() } + ) + } + ) { innerPadding -> + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(innerPadding) + .padding(bottom = 20.dp) + ) { + + var componentWidth by remember { + mutableStateOf(0.dp) + } + val density = LocalDensity.current + + + Box( + modifier = Modifier + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(440.dp) + .clip( + RoundedCornerShape( + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .background(Color(0xff656565)) + .onGloballyPositioned { + componentWidth = with(density) { + it.size.width.toDp() + } + } + .offset( + x = -(componentWidth * 15 / 88) + ), + ) { +// AsyncImage( +// model = component.imageExample, +// contentDescription = null, +// contentScale = ContentScale.Crop, +// alpha = 0.8f, +// modifier = Modifier +// .fillMaxSize() +// .blur(16.dp) +// ) + AsyncImage( + model = component.imageExample, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxSize() + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 402.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .height(64.dp) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(20.dp) + ) + .background(MaterialTheme.colorScheme.tertiary) + .padding(start = 20.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = component.name, + style = MaterialTheme.typography.headlineSmall + ) + } + +// if (detectionsResultObj != null) { +// DetectionsResultBox( +// modifier = Modifier +// .padding(horizontal = 20.dp), +// predictionList = detectionsResultObj.predictions +// ) +// } + + } + } + + Column( + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 20.dp) + ) { + + Text( + text = component.desc, + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Usable Components", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (usableComponentsState != null) { + when(usableComponentsState) { + is UiState.Success -> { + usableComponentsState.data?.smallParts?.size?.let { + LazyVerticalGrid( + columns = GridCells.Fixed( + 2 + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .height((it * 72).dp), + ) { + items(usableComponentsState.data.smallParts.size) { index -> + currentlyClickedUsableComponent = index + updateUsableComponentsList(usableComponentsState.data.smallParts) + + UsableComponentItem( + onClick = { + isBottomSheetOpen = true + coroutineScope.launch { + modalSheetState.show() + } + }, + usableComponent = usableComponentsState.data.smallParts[index] + ) + } + + } + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Fail to fetch usable component", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + is UiState.Loading -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Related Articles", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + + if (relatedArticleListState != null) { + when(relatedArticleListState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Fail to fetch article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + is UiState.Success -> { + val articleList = relatedArticleListState.data?.articleList + + if (articleList?.isNotEmpty() == true) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + relatedArticleListState.data.articleList.let { + if (it.isNotEmpty()) { + items( + count = it.size, + ) { index -> + articleList.get(index)?.let { it1 -> + ArticleCardBig( + modifier = Modifier + .width(150.dp) + .clickable { + it1.id?.let { it2 -> + navigateToSingleArticle( + it2 + ) + } + }, + article = it1 + ) + } + } + } + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "There's no related article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(28.dp)) + + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun CatalogSingleComponentScreenPreview() { + TechwasMark02Theme { + CatalogSingleComponentContent( + component = Component( + desc = "kdfiasu kadfiau kaud fiajdfkua dfja ufiauj dfkaud ifaju", + id = 2, + imageExample = "", + name = "Laptop" + ), + usableComponentsState = UiState.Loading(), + updateUsableComponentsState = {}, + usableComponentList = emptyList(), + updateUsableComponentsList = {}, + relatedArticleListState = UiState.Loading(), + updateRelatedArticleListState = {}, + navigateToSingleArticle = {}, + navigateBackToCatalog = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreenViewModel.kt new file mode 100644 index 0000000..1050175 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/catalogSingleComponent/CatalogSingleComponentScreenViewModel.kt @@ -0,0 +1,50 @@ +package com.capstone.techwasmark02.ui.screen.catalogSingleComponent + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.repository.TechwasComponentApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CatalogSingleComponentScreenViewModel @Inject constructor( + private val componentApiRepository: TechwasComponentApiRepository, + private val articleRepository: TechwasArticleRepository +) : ViewModel() { + + private val _usableComponentsState: MutableStateFlow?> = MutableStateFlow(null) + val usableComponentsState = _usableComponentsState.asStateFlow() + + private val _usableComponentList: MutableStateFlow> = MutableStateFlow(emptyList()) + val usableComponentList = _usableComponentList.asStateFlow() + + private val _relatedArticleListState: MutableStateFlow?> = MutableStateFlow(null) + val relatedArticleListState = _relatedArticleListState.asStateFlow() + + fun updateUsableComponentsState(compId: Int) { + _usableComponentsState.value = UiState.Loading() + viewModelScope.launch { + _usableComponentsState.value = componentApiRepository.fetchUsableComponents(compId) + } + } + + fun updateUsableComponentList(newList: List) { + _usableComponentList.value = newList + } + + fun updateRelatedArticleListState(compId: Int) { + _relatedArticleListState.value = UiState.Loading() + viewModelScope.launch { + _relatedArticleListState.value = articleRepository.getArticleByComponentId(id = compId) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreen.kt new file mode 100644 index 0000000..84248d3 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreen.kt @@ -0,0 +1,606 @@ +package com.capstone.techwasmark02.ui.screen.detectionResult + +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.DetectionsResultResponse +import com.capstone.techwasmark02.data.remote.response.Prediction +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.ArticleCardBig +import com.capstone.techwasmark02.ui.component.DefaultTopBar +import com.capstone.techwasmark02.ui.component.DetectionsResultBox +import com.capstone.techwasmark02.ui.component.UsableComponentBottomSheet +import com.capstone.techwasmark02.ui.component.UsableComponentItem +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.launch + + +@Composable +fun DetectionResultScreen( + stringUri: String, + detectionResult: String, + viewModel: DetectionResultScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + + val selectedPrediction by viewModel.selectedPrediction.collectAsState() + val usableComponentsListState by viewModel.usableComponentsListState.collectAsState() + val currentlySelectedUsableComponentList by viewModel.currentlySelectedUsableComponentList.collectAsState() + val relatedArticleListState by viewModel.relatedArticleListState.collectAsState() + + DetectionResultContent( + stringUri = stringUri, + detectionResult = detectionResult, + selectedPrediction = selectedPrediction, + updateSelectedPrediction = { viewModel.updateSelectedPrediction(it) }, + usableComponentsListState = usableComponentsListState, + fetchAllUsableComponents = { viewModel.fetchAllUsableComponents(it) }, + currentlySelectedUsableComponentList = currentlySelectedUsableComponentList, + updateCurrentlySelectedUsableComponentList = { viewModel.updateCurrentlySelectedUsableComponentList(it) }, + relatedArticleListState = relatedArticleListState, + navigateToSingleArticle = { navController.navigate("${Screen.SingleArticle.route}/$it") }, + navigateToMain = { navController.navigate("${Screen.Main.route}/0") }, + navigateToForum = { navController.navigate("${Screen.Main.route}/1")}, + navigateToMaps = { navController.navigate(Screen.Maps.route) } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun DetectionResultContent( + stringUri: String, + detectionResult: String, + selectedPrediction: Int, + updateSelectedPrediction: (Int) -> Unit, + usableComponentsListState: List>?, + fetchAllUsableComponents: (List) -> Unit, + currentlySelectedUsableComponentList: List, + updateCurrentlySelectedUsableComponentList: (List) -> Unit, + relatedArticleListState: List>?, + navigateToSingleArticle: (idArticle: Int) -> Unit, + navigateToMain: () -> Unit, + navigateToForum: () -> Unit, + navigateToMaps: () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val modalSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded}, + skipHalfExpanded = false + ) + + val photoUri = Uri.parse(stringUri) + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val adapter = moshi.adapter(DetectionsResultResponse::class.java) + val detectionsResultObj = adapter.fromJson(detectionResult) + + LaunchedEffect(Unit) { + val componentIdList = detectionsResultObj?.predictions + + if (componentIdList != null) { + fetchAllUsableComponents(componentIdList) + } + } + + var currentlyClickedUsableComponent by remember { + mutableStateOf(0) + } + + var isBottomSheetOpen by remember { + mutableStateOf(false) + } + + BackHandler( + enabled = true + ) { + if (isBottomSheetOpen) { + coroutineScope.launch { + modalSheetState.hide() + isBottomSheetOpen = false + } + } else { + navigateToMain() + } + } + + ModalBottomSheetLayout( + sheetState = modalSheetState, + sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + sheetContent = { + if (currentlySelectedUsableComponentList.isNotEmpty()) { + val currentUsableComponents = currentlySelectedUsableComponentList[currentlyClickedUsableComponent] + UsableComponentBottomSheet( + smallPart = currentUsableComponents + ) + } + } + ) { + + Scaffold( + topBar = { + DefaultTopBar( + pageTitle = "Result", + onClickNavigationIcon = { + navigateToMain() + } + ) + } + ) { innerPadding -> + val scrollState = rememberScrollState() + + Box( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(innerPadding) + .padding(bottom = 20.dp) + ) { + Box( + modifier = Modifier + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + .clip( + RoundedCornerShape( + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .background(MaterialTheme.colorScheme.primary), + ) { + AsyncImage( + model = photoUri, + contentDescription = null, + contentScale = ContentScale.Crop, + alpha = 0.8f, + modifier = Modifier + .fillMaxSize() + .blur(16.dp) + ) + AsyncImage( + model = photoUri, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxSize() + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 402.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Detected as", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimary + ) + + if (detectionsResultObj != null) { + DetectionsResultBox( + modifier = Modifier + .padding(horizontal = 20.dp), + predictionList = detectionsResultObj.predictions, + updateSelectedPrediction = updateSelectedPrediction + ) + } + + } + } + + Column( + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 28.dp) + ) { + Text( + text = "Usable Components", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (usableComponentsListState != null) { + val currentlySelectedPrediction = usableComponentsListState[selectedPrediction] + when(currentlySelectedPrediction) { + is UiState.Loading -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Success -> { + val usableComponentList = currentlySelectedPrediction.data?.smallParts + + usableComponentList?.size?.let { + LazyVerticalGrid( + columns = GridCells.Fixed( + 2 + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.height((it * 72).dp) + ) { + items(usableComponentList.size) {index -> + currentlyClickedUsableComponent = index + updateCurrentlySelectedUsableComponentList(usableComponentList) + + UsableComponentItem( + onClick = { + isBottomSheetOpen = true + coroutineScope.launch { + modalSheetState.show() + } + }, + usableComponent = usableComponentList[index] + ) + } + } + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Fail to fetch usable component", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + } else { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = "Related Articles", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + if (relatedArticleListState != null) { + val currentSelectedArticleListState = relatedArticleListState[selectedPrediction] + when(currentSelectedArticleListState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .height(175.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .height(175.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Fail to fetch article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + is UiState.Success -> { + val articleList = relatedArticleListState[selectedPrediction].data?.articleList + + if (articleList?.isNotEmpty() == true) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + relatedArticleListState[selectedPrediction].data?.articleList?.let { + items( + count = it.size, + ) { index -> + articleList[index]?.let { it1 -> + ArticleCardBig( + modifier = Modifier + .width(150.dp) + .clickable { + it1.id?.let { it2 -> + navigateToSingleArticle( + it2 + ) + } + }, + article = it1 + ) + } + } + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(175.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "There's no related article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + } + } + } else { + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + Spacer(modifier = Modifier.height(80.dp)) + + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + + ThrowNSellButton( + navigateToMaps = navigateToMaps, + navigateToForum = navigateToForum + ) + } + } + } + + } +} + +@Composable +fun ThrowNSellButton( + navigateToMaps: () -> Unit, + navigateToForum: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Button( + onClick = navigateToMaps, + modifier = Modifier + .weight(1f) + .padding(vertical = 18.dp) +// .border( +// width = 2.dp, +// color = MaterialTheme.colorScheme.primary, +// shape = RoundedCornerShape(20.dp) +// ) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(20.dp) + ), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + painter = painterResource(id = R.drawable.ic_trash_outline), + contentDescription = null, + modifier = Modifier + .size(26.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "Throw it", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .padding(top = 4.dp) + ) + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + Button( + onClick = navigateToForum, + modifier = Modifier + .weight(1f) + .padding(vertical = 18.dp) +// .border( +// width = 2.dp, +// color = MaterialTheme.colorScheme.primary, +// shape = RoundedCornerShape(20.dp) +// ) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(20.dp) + ), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + painter = painterResource(id = R.drawable.ic_sell), + contentDescription = null, + modifier = Modifier + .size(26.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "Sell it", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .padding(top = 4.dp) + ) + } + } + + } +} + +//@Preview +//@Composable +//fun DetectionResultContentPreview() { +// TechwasMark02Theme { +// DetectionResultContent( +// stringUri = "", +// detectionResult = "", +// selectedPrediction = 0, +// updateSelectedPrediction = {}, +// usableComponentsListState = emptyList(), +// fetchAllUsableComponents = {}, +// currentlySelectedUsableComponentList = emptyList(), +// updateCurrentlySelectedUsableComponentList = {}, +// relatedArticleListState = emptyList(), +// navigateToSingleArticle = {}, +// navigateToMain = {} +// ) +// } +//} + +@Preview(showBackground = true) +@Composable +fun ThrowNSellButtonPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .padding(20.dp) + ) { + ThrowNSellButton( + navigateToForum = {}, + navigateToMaps = {} + ) + } + } +} diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreenViewModel.kt new file mode 100644 index 0000000..e7060b7 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/detectionResult/DetectionResultScreenViewModel.kt @@ -0,0 +1,71 @@ +package com.capstone.techwasmark02.ui.screen.detectionResult + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.Prediction +import com.capstone.techwasmark02.data.remote.response.SmallPart +import com.capstone.techwasmark02.data.remote.response.UsableComponentsResponse +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.repository.TechwasComponentApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DetectionResultScreenViewModel @Inject constructor( + private val componentApiRepository: TechwasComponentApiRepository, + private val articleRepository: TechwasArticleRepository +) : ViewModel() { + + private val _usableComponentsListState: MutableStateFlow>?> = MutableStateFlow(null) + val usableComponentsListState = _usableComponentsListState.asStateFlow() + + private val _selectedPrediction: MutableStateFlow = MutableStateFlow(0) + val selectedPrediction = _selectedPrediction.asStateFlow() + + private val _currentlySelectedUsableComponentList: MutableStateFlow> = MutableStateFlow( + emptyList() + ) + val currentlySelectedUsableComponentList = _currentlySelectedUsableComponentList.asStateFlow() + + private val _relatedArticleListState: MutableStateFlow>?> = MutableStateFlow(null) + val relatedArticleListState = _relatedArticleListState.asStateFlow() + + fun fetchAllUsableComponents(componentIdList: List) { + viewModelScope.launch { + val usableComponentsList = mutableListOf>() + val articlesList = mutableListOf>() + componentIdList.forEach { prediction -> + + prediction.componentId.let { + val usableComponents = componentApiRepository.fetchUsableComponents(compId = it) + usableComponentsList.add(usableComponents) + + val relatedArticleList = articleRepository.getArticleByComponentId(id = it) + articlesList.add(relatedArticleList) + } + } + _usableComponentsListState.value = usableComponentsList + _relatedArticleListState.value = articlesList + } + } + + fun updateSelectedPrediction(currentlySelectedPrediction: Int) { + _selectedPrediction.value = currentlySelectedPrediction + } + + fun updateCurrentlySelectedUsableComponentList(newList: List) { + _currentlySelectedUsableComponentList.value = newList + } + +// fun updateRelatedArticleListState(compId: Int) { +// _relatedArticleListState.value = UiState.Loading() +// viewModelScope.launch { +// _relatedArticleListState.value = articleRepository.getArticleByComponentId(id = compId) +// } +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreen.kt new file mode 100644 index 0000000..ce0abda --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreen.kt @@ -0,0 +1,331 @@ +package com.capstone.techwasmark02.ui.screen.forum + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.Forum +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.ForumBox +import com.capstone.techwasmark02.ui.component.SearchBox +import com.capstone.techwasmark02.ui.component.SelectableText +import com.capstone.techwasmark02.ui.componentType.ArticleFilterType +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ForumScreen( + navController: NavHostController, + viewModel: ForumScreenViewModel = hiltViewModel() +) { + val forumListState by viewModel.forumList.collectAsState() + val searchBoxValue by viewModel.searchBoxValue.collectAsState() + + ForumContent( + navigateToSingleForum = { navController.navigate("${Screen.SingleForum.route}/$it")}, + forumListState = forumListState, + navigateToCreateForum = { navController.navigate(Screen.CreateForum.route)}, + searchBoxValue = searchBoxValue, + onSearchBoxValueChange = { viewModel.updateSearchBoxValue(it)} + ) +} + +@Composable +fun ForumContent( + navigateToSingleForum: (Int) -> Unit, + forumListState: UiState?, + navigateToCreateForum: () -> Unit, + searchBoxValue: String, + onSearchBoxValueChange: (String) -> Unit +) { + + val filterTypeList = listOf( + ArticleFilterType.General, + ArticleFilterType.Battery, + ArticleFilterType.Cable, + ArticleFilterType.CrtTv, + ArticleFilterType.EKettle, + ArticleFilterType.Refrigerator, + ArticleFilterType.Keyboard, + ArticleFilterType.Laptop, + ArticleFilterType.LightBulb, + ArticleFilterType.Monitor, + ArticleFilterType.Mouse, + ArticleFilterType.PCB, + ArticleFilterType.Printer, + ArticleFilterType.RiceCooker, + ArticleFilterType.WashingMachine, + ArticleFilterType.Phone + ) + + var selectedFilter by remember { + mutableStateOf(filterTypeList.firstOrNull()) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp, bottom = 80.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + Column( + modifier = Modifier + .width(280.dp) + ) { + Text( + text = "Forum", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Share your thoughts and connect with others", + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium + ) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Box( + modifier = Modifier + .padding(top = 8.dp, end = 8.dp) + ) { + IconButton( + onClick = navigateToCreateForum, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_create), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + SearchBox( + value = searchBoxValue, + onValueChange = onSearchBoxValueChange, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + + LazyRow( + modifier = Modifier.height(48.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items( + items = filterTypeList, + ) { item -> + SelectableText( + filterType = item, + selected = item == selectedFilter, + modifier = Modifier, + onClick = { + selectedFilter = item + } + ) + } + } + + if (forumListState != null) { + when(forumListState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(175.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Fail to fetch forum", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Success -> { + val currentForumList = forumListState.data?.forum + + var filteredForumList by remember { + mutableStateOf(currentForumList) + } + + LaunchedEffect(key1 = searchBoxValue, key2 = selectedFilter) { + if (currentForumList != null) { + filteredForumList = selectedFilter?.type?.let { + searchForum( + forumList = currentForumList, + searchBoxValue = searchBoxValue, + selectedFilter = it + ) + } + } + } + + if (!filteredForumList.isNullOrEmpty() && filteredForumList!!.isNotEmpty()) { + LazyColumn( + modifier = Modifier, + contentPadding = PaddingValues(vertical = 20.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(filteredForumList!!) { forum -> + ForumBox( + modifier = Modifier + .fillMaxWidth(), + title = forum.title, + place = forum.location, + desc = forum.content, + onClick = { navigateToSingleForum(forum.id) }, + photoUrl = forum.imageURL + ) + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(175.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "No forum to view", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + } + } + } + + } + } +} + +private fun searchForum(forumList: List, searchBoxValue: String, selectedFilter: String): List { + if (searchBoxValue == "" && selectedFilter == "General") { + return forumList + } + + val searchBoxFilter = forumList.filter { forum -> + forum.title.contains(searchBoxValue, ignoreCase = true) + } + + val selectedFilterList = searchBoxFilter.filter { forum -> + forum.category == selectedFilter + } + + return selectedFilterList +} + +@Preview +@Composable +fun ForumScreenPreview() { + TechwasMark02Theme { + ForumContent( + navigateToSingleForum = {}, + forumListState = null, + navigateToCreateForum = {}, + searchBoxValue = "", + onSearchBoxValueChange = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreenViewModel.kt new file mode 100644 index 0000000..6afb7dc --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forum/ForumScreenViewModel.kt @@ -0,0 +1,36 @@ +package com.capstone.techwasmark02.ui.screen.forum + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.repository.TechwasForumApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ForumScreenViewModel @Inject constructor( + private val forumApiRepository: TechwasForumApiRepository +): ViewModel() { + + private val _forumList: MutableStateFlow?> = MutableStateFlow(null) + val forumList = _forumList.asStateFlow() + + private val _searchBoxValue: MutableStateFlow = MutableStateFlow("") + val searchBoxValue = _searchBoxValue.asStateFlow() + + fun updateSearchBoxValue(newValue: String) { + _searchBoxValue.value = newValue + } + + init { + _forumList.value = UiState.Loading() + viewModelScope.launch { + _forumList.value = forumApiRepository.fetchAllForum() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreen.kt new file mode 100644 index 0000000..4594bb7 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreen.kt @@ -0,0 +1,451 @@ +package com.capstone.techwasmark02.ui.screen.forumCreate + +import android.content.Context +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.model.ForumToCreateInfo +import com.capstone.techwasmark02.data.remote.response.CreateForumResponse +import com.capstone.techwasmark02.data.remote.response.ImageUrlResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.DefaultTopBar +import com.capstone.techwasmark02.ui.component.SelectableText +import com.capstone.techwasmark02.ui.componentType.ArticleFilterType +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ForumCreateScreen( + navController: NavHostController, + viewModel: ForumCreateScreenViewModel = hiltViewModel() +) { + + val forumToCreateInfo by viewModel.forumToCreateInfo.collectAsState() + val createForumState by viewModel.createForumState.collectAsState() + val imageUri by viewModel.imageUri.collectAsState() + val uploadAndGetImageUrlState by viewModel.uploadAndGetImageUrlState.collectAsState() + + ForumCreateContent( + navigateBackToForum = {navController.navigate("${Screen.Main.route}/1")}, + forumToCreateInfo = forumToCreateInfo, + updateForumToCreateInfo = { viewModel.updateForumToCreateInfo(it) }, + createForumState = createForumState, + createNewForum = { viewModel.createNewForum() }, + imageUri = imageUri, + updateImageUri = { viewModel.updateImageUri(it) }, + uploadAndGetImageUrl = { viewModel.uploadAndGetImageUrl(it)}, + uploadAndGetImageUrlState = uploadAndGetImageUrlState + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForumCreateContent( + navigateBackToForum: () -> Unit, + forumToCreateInfo: ForumToCreateInfo, + updateForumToCreateInfo: (ForumToCreateInfo) -> Unit, + createForumState: UiState?, + createNewForum: () -> Unit, + imageUri: Uri?, + updateImageUri: (Uri) -> Unit, + uploadAndGetImageUrl: (Context) -> Unit, + uploadAndGetImageUrlState: UiState? +) { + + val context = LocalContext.current + + val filterTypeList = listOf( + ArticleFilterType.Battery, + ArticleFilterType.Cable, + ArticleFilterType.CrtTv, + ArticleFilterType.EKettle, + ArticleFilterType.Refrigerator, + ArticleFilterType.Keyboard, + ArticleFilterType.Laptop, + ArticleFilterType.LightBulb, + ArticleFilterType.Monitor, + ArticleFilterType.Mouse, + ArticleFilterType.PCB, + ArticleFilterType.Printer, + ArticleFilterType.RiceCooker, + ArticleFilterType.WashingMachine, + ArticleFilterType.Phone + ) + + var selectedFilter by remember { + mutableStateOf(filterTypeList.first()) + } + + val scrollState = rememberScrollState() + + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { storageImageUri -> + if (storageImageUri != null) { + updateImageUri(storageImageUri) + } + } + ) + + + LaunchedEffect(key1 = selectedFilter) { + selectedFilter.type.let { + forumToCreateInfo.copy( + category = it + ) + }.let { updateForumToCreateInfo(it) } + } + + LaunchedEffect(key1 = imageUri) { + if (imageUri != null) { + uploadAndGetImageUrl(context) + } + } + + + Box( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + Column( + modifier = Modifier + .padding(top = 88.dp) + .padding(horizontal = 16.dp) + ) { + Text( + text = "E-waste picture", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(240.dp) + .padding(8.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.LightGray), + contentAlignment = Alignment.Center + ) { + if (uploadAndGetImageUrlState != null) { + when(uploadAndGetImageUrlState) { + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Fail to fetch forum", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Success -> { + if (imageUri != null) { + AsyncImage( + model = imageUri, + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + uploadAndGetImageUrlState.data?.imgURL?.let { + forumToCreateInfo.copy( + imageUrl = it + ) + }?.let { updateForumToCreateInfo(it) } + } + } + } + Box( + modifier = Modifier + .fillMaxSize() + .padding(end = 10.dp, bottom = 10.dp), + contentAlignment = Alignment.BottomEnd + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.tertiary) + .clickable { + galleryLauncher.launch("image/*") + } + .padding(8.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_gallery_big), + contentDescription = null, + tint = Color.Black.copy(alpha = 0.7f) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "E-waste category", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + ) + } + + LazyRow( + modifier = Modifier.height(48.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items( + items = filterTypeList, + ) { item -> + SelectableText( + filterType = item, + selected = item == selectedFilter, + modifier = Modifier, + onClick = { + selectedFilter = item + } + ) + } + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 80.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Forum title", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + + TextField( + value = forumToCreateInfo.title, + onValueChange = { newValue -> + updateForumToCreateInfo(forumToCreateInfo.copy( + title = newValue + )) + }, + modifier = Modifier + .fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Location", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + + TextField( + value = forumToCreateInfo.location, + onValueChange = { newValue -> + updateForumToCreateInfo(forumToCreateInfo.copy( + location = newValue + )) + }, + modifier = Modifier + .fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Forum Description", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + + TextField( + value = forumToCreateInfo.content, + onValueChange = { newValue -> + updateForumToCreateInfo(forumToCreateInfo.copy( + content = newValue + )) + }, + modifier = Modifier + .fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + ) + ) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + DefaultTopBar(onClickNavigationIcon = navigateBackToForum, pageTitle = "Create New Forum") + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) { + val context = LocalContext.current + + if (createForumState != null) { + + when(createForumState) { + is UiState.Error -> { + Toast.makeText(context, "Fail to create new forum", Toast.LENGTH_SHORT).show() + } + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Success -> { + navigateBackToForum() + } + } + } else { + ElevatedButton( + onClick = createNewForum , + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + ) { + Text( + text = "Create Forum", + style = MaterialTheme.typography.labelLarge + ) + } + } + } + } + } +} + +@Preview (showBackground = true) +@Composable +fun ForumCreateScreenPreview() { + TechwasMark02Theme { + ForumCreateContent( + navigateBackToForum = {}, + forumToCreateInfo = ForumToCreateInfo( + category = "", + content = "", + imageUrl = "", + location = "", + title = "" + ), + updateForumToCreateInfo = {}, + createForumState = null, + createNewForum = {}, + imageUri = null, + updateImageUri = {}, + uploadAndGetImageUrlState = null, + uploadAndGetImageUrl = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreenViewModel.kt new file mode 100644 index 0000000..dcb797e --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumCreate/ForumCreateScreenViewModel.kt @@ -0,0 +1,131 @@ +package com.capstone.techwasmark02.ui.screen.forumCreate + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.common.Resource +import com.capstone.techwasmark02.data.model.ForumToCreateInfo +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.CreateForumResponse +import com.capstone.techwasmark02.data.remote.response.ImageUrlResponse +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.repository.PreferencesRepository +import com.capstone.techwasmark02.repository.TechwasForumApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +@HiltViewModel +class ForumCreateScreenViewModel @Inject constructor( + private val forumApiRepository: TechwasForumApiRepository, + private val preferencesRepository: PreferencesRepository +): ViewModel() { + + private val _userSessionState: MutableStateFlow = MutableStateFlow(null) + val userSessionState = _userSessionState.asStateFlow() + + private val _createForumState: MutableStateFlow?> = MutableStateFlow(null) + val createForumState = _createForumState.asStateFlow() + + private val _forumToCreateInfo: MutableStateFlow = MutableStateFlow( + ForumToCreateInfo( + category = "Mouse", + content = "", + imageUrl = "", + location = "", + title = "" + ) + ) + val forumToCreateInfo = _forumToCreateInfo.asStateFlow() + + private val _imageUri: MutableStateFlow = MutableStateFlow(null) + val imageUri = _imageUri.asStateFlow() + + private val _uploadAndGetImageUrlState: MutableStateFlow?> = MutableStateFlow(null) + val uploadAndGetImageUrlState = _uploadAndGetImageUrlState.asStateFlow() + + fun updateForumToCreateInfo(forumToCreateInfo: ForumToCreateInfo) { + _forumToCreateInfo.value = forumToCreateInfo + } + + fun updateImageUri(newUri: Uri) { + _imageUri.value = newUri + } + + fun createNewForum() { + _createForumState.value = UiState.Loading() + viewModelScope.launch { + _createForumState.value = _userSessionState.value?.userLoginToken?.accessToken?.let { + forumApiRepository.createNewForum( + forumToCreateInfo = _forumToCreateInfo.value, + userToken = it + ) + } + } + } + + fun uploadAndGetImageUrl(context: Context) { + val fileToUpload = _imageUri.value?.let { convertUriToFile(context, it) } + + _uploadAndGetImageUrlState.value = UiState.Loading() + viewModelScope.launch { + _uploadAndGetImageUrlState.value = fileToUpload?.let { + _userSessionState.value?.userLoginToken?.accessToken?.let { it1 -> + forumApiRepository.uploadAndGetImageUrl( + file = it, + userToken = it1 + ) + } + } + } + } + + init { + viewModelScope.launch { + val result = preferencesRepository.getActiveSession() + when(result) { + is Resource.Error -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "", id = 0) + ) + } + is Resource.Success -> { + _userSessionState.value = result.data + } + } + } + } + + private fun convertUriToFile(context: Context, uri: Uri): File? { + val inputStream = context.contentResolver.openInputStream(uri) + inputStream?.let { + val file = createTempFile(context, getFileExtension(uri)) + val outputStream = FileOutputStream(file) + inputStream.copyTo(outputStream) + outputStream.close() + inputStream.close() + return file + } + return null + } + + private fun createTempFile(context: Context, fileExtension: String): File { + val directory = context.cacheDir + return File.createTempFile("temp", ".$fileExtension", directory) + } + + private fun getFileExtension(uri: Uri): String { + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(uri.toString()) + return extension ?: "jpg" // Default to "jpg" if extension is null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreen.kt new file mode 100644 index 0000000..d43fd99 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreen.kt @@ -0,0 +1,658 @@ +package com.capstone.techwasmark02.ui.screen.forumSingle + +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.AsyncImage +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.ForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.PostForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.TransparentTopBar +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.Mist97 +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ForumSingleScreen( + viewModel: ForumSingleScreenViewModel = hiltViewModel(), + navController: NavHostController, + forumId: Int +) { + val userSession by viewModel.userSessionState.collectAsState() + val forumState by viewModel.forumState.collectAsState() + val forumCommentState by viewModel.forumCommentState.collectAsState() + val postingCommentState by viewModel.postingCommentState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.fetchForumById(id = forumId,) + viewModel.fetchForumCommentByForumId(forumId) + } + + BackHandler(true) { + navController.navigate("${Screen.Main.route}/1") + } + + ForumSingleContent( + userSession = userSession, + navigateBackToForum = { navController.navigate("${Screen.Main.route}/1") }, + forumState = forumState, + forumCommentState = forumCommentState, + fetchForumComment = { viewModel.fetchForumCommentByForumId(it)}, + forumId = forumId, + postForumComment = { comment, currentForumId -> viewModel.postForumComment(comment, currentForumId)}, + postingCommentState = postingCommentState, + clearPostingCommentState = { viewModel.clearPostingCommentState()} + ) +} + +@Composable +fun ForumSingleContent( + userSession: UserSession?, + navigateBackToForum: () -> Unit, + forumState: UiState?, + forumCommentState: UiState?, + fetchForumComment: (Int) -> Unit, + forumId: Int, + postForumComment: (comment: String, currentForumId: Int) -> Unit, + postingCommentState: UiState?, + clearPostingCommentState: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Mist97) + ) { + + val scrollState = rememberScrollState() + + val coroutineScope = rememberCoroutineScope() + + var commentText by remember { + mutableStateOf("") + } + + var firstRender by remember { + mutableStateOf(true) + } + + + LaunchedEffect(key1 = forumCommentState) { + if (!firstRender) { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + + if (forumState != null) { + when(forumState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(360.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(360.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Failed to fetch forum", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Success -> { + val currentForum = forumState.data?.forum?.get(0) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(360.dp) +// .height(100.dp) + .clip(RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp)) + .background(MaterialTheme.colorScheme.primary) + ) { + AsyncImage( + model = currentForum?.imageURL, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray) + ) +// Image( +// painter = painterResource(id = R.drawable.img_forum_laptop_bekas), +// contentDescription = null, +// contentScale = ContentScale.Crop, +// modifier = Modifier +// .fillMaxSize() +// ) + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 24.dp, bottom = 8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + currentForum?.location?.let { + Text( + text = it, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f) + ) + } + currentForum?.category?.let { + Text( + text = it, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f) + ) + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + currentForum?.title?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + currentForum?.content?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium + ) + ) + } + + } + + if (forumCommentState != null) { + when(forumCommentState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Failed to fetch comment", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Success -> { + val commentList = forumCommentState.data?.article + + if (commentList != null) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.tertiary) + .padding(top = 8.dp, bottom = 72.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + text = "Comment", + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.height(10.dp)) + + if (commentList != null) { + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .widthIn(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (commentList.isNotEmpty()) { + commentList.forEach { comment -> + UserInComment( + username = comment.username, + comment = comment.comment, + photoUrl = R.drawable.img_user_2 + ) + + Text( + text = "Reply", + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(start = 30.dp) + ) + } + } + } + } + } + } + } else { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(MaterialTheme.colorScheme.tertiary) + ) { + Text( + text = "Comment", + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + ) + } + } + } + } + } + + + } + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Bottom + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.tertiary) + .padding(top = 10.dp, bottom = 10.dp, start = 16.dp, end = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + if (postingCommentState != null) { + when(postingCommentState) { + is UiState.Success -> { + clearPostingCommentState() + firstRender = false + LaunchedEffect(Unit) { + fetchForumComment(forumId) + } + } + is UiState.Loading -> { + Box( + modifier = Modifier + .weight(1f) + .height(56.dp) + .border( + width = 2.dp, + color = MaterialTheme.colorScheme.onTertiary.copy( + alpha = 0.6f + ), + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp) + ) + } + } + is UiState.Error -> { + val context = LocalContext.current + Toast.makeText(context, "Fail to post comment", Toast.LENGTH_SHORT).show() + clearPostingCommentState() + } + } + } else { + CommentTextField( + value = commentText, + onValueChange = { newValue -> + commentText = newValue + }, + modifier = Modifier + .weight(1f) + ) + } + +// if (commentPosted) { +// Box( +// modifier = Modifier +// .weight(1f) +// .height(56.dp) +// .border( +// width = 2.dp, +// color = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f), +// shape = RoundedCornerShape(20.dp) +// ), +// contentAlignment = Alignment.Center +// ) { +// CircularProgressIndicator( +// modifier = Modifier +// .size(24.dp) +// ) +// } +// } else { +// CommentTextField( +// value = commentText, +// onValueChange = { newValue -> +// commentText = newValue +// }, +// modifier = Modifier +// .weight(1f) +// ) +// } + + Spacer(modifier = Modifier.width(10.dp)) + + IconButton( + onClick = { + postForumComment( commentText, forumId) + commentText = "" + }, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_create), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + } + } + } + + Box( + modifier = Modifier + .matchParentSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color.Black.copy(alpha = 0.7f), + Color.Transparent + ), + startY = 0f, + endY = 600f + ) + ) + ) { + TransparentTopBar(onClickNavigationIcon = { navigateBackToForum() }, pageTitle = "Detail Forum") + } + } +} + +@Preview (showBackground = true) +@Composable +fun ForumSingleScreenPreview() { + TechwasMark02Theme { + ForumSingleContent( + userSession = UserSession( + userNameId = UserId( + username = "Ghina", + id = 1 + ), + userLoginToken = Token( + accessToken = "" + ) + ), + navigateBackToForum = {}, + forumState = null, + forumCommentState = null, + fetchForumComment = {}, + forumId = 0, + postingCommentState = null, + postForumComment = {comment: String, currentForumId: Int -> {}}, + clearPostingCommentState = {} + ) + } +} + +@Composable +fun UserInComment( + username: String, + comment: String, + photoUrl: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .height(IntrinsicSize.Max), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = photoUrl), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .size(60.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.Center + ) { + Text( + text = username, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f) + ) + + Text( + text = comment, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.9f) + ) + } + } +} + +@Preview (showBackground = true) +@Composable +fun UserInCommentPreview() { + TechwasMark02Theme { + Box( + modifier = Modifier + .padding(10.dp) + ) { + UserInComment(username = "Ghori", comment = "Woow such a great insight", photoUrl = R.drawable.img_user_1) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CommentTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + + var hasFocus by remember { + mutableStateOf(false) + } + + val focusColor = if (hasFocus) MaterialTheme.colorScheme.onTertiary else MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.6f) + + TextField( + value = value, + onValueChange = onValueChange, + colors = TextFieldDefaults.textFieldColors( + textColor = MaterialTheme.colorScheme.onTertiary, + containerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.onTertiary, + placeholderColor = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.7f) + ), + shape = MaterialTheme.shapes.large, + placeholder = { + Text( + text = "Comments...", + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold + ) + ) + }, + modifier = modifier + .border( + width = if (hasFocus) 2.dp else 1.dp, + color = focusColor, + shape = MaterialTheme.shapes.large + ) + .height(56.dp) + .onFocusChanged { focusState -> hasFocus = focusState.hasFocus }, + textStyle = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium + ) + ) +} + +@Preview (showBackground = true) +@Composable +fun CommentTextFieldPreview() { + TechwasMark02Theme { + var value by remember { + mutableStateOf("") + } + + Box( + modifier = Modifier + .padding(10.dp) + ) { + CommentTextField( + value = value, + onValueChange = { newValue -> value = newValue} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreenViewModel.kt new file mode 100644 index 0000000..2c79914 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/forumSingle/ForumSingleScreenViewModel.kt @@ -0,0 +1,96 @@ +package com.capstone.techwasmark02.ui.screen.forumSingle + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.common.Resource +import com.capstone.techwasmark02.data.model.ForumCommentInfo +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.ForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.PostForumCommentResponse +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.repository.PreferencesRepository +import com.capstone.techwasmark02.repository.TechwasForumApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ForumSingleScreenViewModel @Inject constructor( + private val preferencesRepository: PreferencesRepository, + private val forumApiRepository: TechwasForumApiRepository +): ViewModel() { + + private val _userSessionState: MutableStateFlow = MutableStateFlow(null) + val userSessionState = _userSessionState.asStateFlow() + + private val _forumState: MutableStateFlow?> = MutableStateFlow(null) + val forumState = _forumState.asStateFlow() + + private val _forumCommentState: MutableStateFlow?> = MutableStateFlow(null) + val forumCommentState = _forumCommentState.asStateFlow() + + private val _postingCommentState: MutableStateFlow?> = MutableStateFlow(null) + val postingCommentState = _postingCommentState.asStateFlow() + + fun fetchForumById(id: Int) { + _forumState.value = UiState.Loading() + viewModelScope.launch { + _forumState.value = _userSessionState.value?.userLoginToken?.accessToken?.let { + forumApiRepository.fetchForumById( + id = id, + userToken = it + ) + } + } + } + + fun fetchForumCommentByForumId(forumId: Int) { + _forumCommentState.value = UiState.Loading() + viewModelScope.launch { + _forumCommentState.value = forumApiRepository.fetchForumCommentByForumId(forumId) + } + } + + fun postForumComment(comment: String, forumId: Int) { + _postingCommentState.value = UiState.Loading() + val forumCommentInfo = ForumCommentInfo( + comment = comment, + forumID = forumId + ) + viewModelScope.launch { + _userSessionState.value?.userLoginToken?.accessToken?.let { + _postingCommentState.value = forumApiRepository.postForumComment( + forumCommentInfo = forumCommentInfo, + userToken = it + ) + } + } + } + + fun clearPostingCommentState() { + _postingCommentState.value = null + } + + init { + viewModelScope.launch { + val result = preferencesRepository.getActiveSession() + when(result) { + is Resource.Error -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "", id = 0) + ) + } + is Resource.Success -> { + _userSessionState.value = result.data + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreen.kt index b980c26..8037249 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreen.kt @@ -1,84 +1,187 @@ package com.capstone.techwasmark02.ui.screen.home +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.ui.common.UiState import com.capstone.techwasmark02.ui.component.ArticleCardBig -import com.capstone.techwasmark02.ui.component.DefaultBottomBar -import com.capstone.techwasmark02.ui.component.DetectionsFab -import com.capstone.techwasmark02.ui.component.DropPointBanner -import com.capstone.techwasmark02.ui.component.UserGreet -import com.capstone.techwasmark02.ui.componentType.BottomBarItemType +import com.capstone.techwasmark02.ui.component.FeatureBox +import com.capstone.techwasmark02.ui.component.FeatureBoxLarge +import com.capstone.techwasmark02.ui.component.ForumBox +import com.capstone.techwasmark02.ui.componentType.FeatureBoxType +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.navigation.Screen.Camera +import com.capstone.techwasmark02.ui.navigation.Screen.Catalog +import com.capstone.techwasmark02.ui.navigation.Screen.Forum +import com.capstone.techwasmark02.ui.navigation.Screen.Maps import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.capstone.techwasmark02.ui.theme.yellow +import kotlin.math.absoluteValue @Composable -fun HomeScreen() { - HomeContent() +fun HomeScreen( + viewModel: HomeScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + + val latestArticleState by viewModel.latestArticleState.collectAsState() + val forumList by viewModel.forumList.collectAsState() + + HomeContent( + navigateToCamera = { navController.navigate(Camera.route) }, + navigateToArticle = { navController.navigate("${Screen.Main.route}/2") }, + navigateToForum = { navController.navigate(Forum.route) }, + navigateToCatalog = { navController.navigate(Catalog.route) }, + navigateToMaps = { navController.navigate(Maps.route) }, + navigateToSingleArticle = { navController.navigate("${Screen.SingleArticle.route}/$it") }, + latestArticleState = latestArticleState, + forumList = forumList, + navigateToSingleForum = { navController.navigate("${Screen.SingleForum.route}/$it")}, + ) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -fun HomeContent() { - Scaffold( +fun HomeContent( + navigateToCamera: () -> Unit, + navigateToForum: () -> Unit, + navigateToArticle: () -> Unit, + navigateToCatalog: () -> Unit, + navigateToMaps: () -> Unit, + navigateToSingleArticle: (idArticle: Int) -> Unit, + latestArticleState: UiState?, + forumList: UiState?, + navigateToSingleForum: (Int) -> Unit, +) { + + val context = LocalContext.current + + val cameraPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { granted -> + if(granted) { + navigateToCamera() + } + } + ) + + Scaffold( ) { innerPadding -> + val scrollState = rememberScrollState() + + val titlePaddingBottom = 10.dp + val titlePaddingTop = 24.dp + Box( modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding), - contentAlignment = Alignment.BottomCenter + .fillMaxSize(), ) { Column( modifier = Modifier - .fillMaxSize() - .padding(bottom = 60.dp, top = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally + .verticalScroll(scrollState) + .padding(innerPadding) + .background(MaterialTheme.colorScheme.primary) + .padding(top = 20.dp) ) { - Column( + Row( modifier = Modifier - .padding(horizontal = 16.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - UserGreet(userName = "Zhahrany") - - Spacer(modifier = Modifier.height(20.dp)) + Row( + modifier = Modifier, + ) { + Column( + modifier = Modifier + .fillMaxHeight(), - DropPointBanner() + ) { + Image( + modifier = Modifier.size(24.dp, 27.dp), + painter = painterResource(id = R.drawable.img_logo_onboarding_nooutline), + contentDescription = null + ) + } + Text( + text = "Techwaste", + modifier = Modifier + .padding(start = 4.dp) + .offset(y = 5.dp), + style = MaterialTheme.typography.titleMedium, + color = Color.White + ) + } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.weight(1f)) - Text( - text = "What's New?", - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(start = 8.dp) - ) - } - LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - items( - count = 10, + IconButton( + onClick = { }, +// modifier = Modifier.size(21.dp, 24.dp) ) { - ArticleCardBig( - modifier = Modifier.width(240.dp) + Icon( + painter = painterResource(id = R.drawable.ic_nofitications), + contentDescription = null, + tint = Color.White ) } } @@ -87,19 +190,300 @@ fun HomeContent() { Column( modifier = Modifier - .padding(horizontal = 16.dp) + .fillMaxSize() + .clip( + RoundedCornerShape( + topStart = 20.dp, + topEnd = 20.dp + ) + ) + .background(Color.White) + .padding(bottom = 80.dp) ) { - DropPointBanner() + Spacer(modifier = Modifier.height(18.dp)) + + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = "Features", + style = MaterialTheme.typography.titleMedium, + ) + } + + Spacer(modifier = Modifier.height(titlePaddingBottom)) + + Row( + modifier = Modifier + .fillMaxWidth(), + ) { + + FeatureBoxLarge( + featureBoxType = FeatureBoxType.Detection, + onClick = { + checkAndRequestCameraPermission( + context = context, + cameraPermissionLauncher = cameraPermissionLauncher, + onAlreadyGranted = navigateToCamera + ) + } + ) + +// DetectBox1() +// DetectBox2() + } + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .fillMaxWidth(), + ) { + FeatureBox(featureBoxType = FeatureBoxType.DropPoint, onClick = navigateToMaps, modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(24.dp)) + FeatureBox(featureBoxType = FeatureBoxType.Catalog, onClick = navigateToCatalog, modifier = Modifier.weight(1f)) + } + Spacer(modifier = Modifier.height(titlePaddingTop)) + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "Articles", + style = MaterialTheme.typography.titleMedium, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "See all", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = yellow, + modifier = Modifier + .clickable { + navigateToArticle() + } + ) + } + } + + Spacer(modifier = Modifier.height(titlePaddingBottom)) + + if (latestArticleState != null) { + + when(latestArticleState) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp), + contentAlignment = Alignment.Center + ) { + Text(text = "No article to view") + } + } + is UiState.Success -> { + latestArticleState.data?.articleList?.size?.let { + + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f, + ) + + HorizontalPager( + pageCount = it, + state = pagerState, + contentPadding = PaddingValues(horizontal = 48.dp), + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + ) { page -> + val articleList = latestArticleState.data.articleList + + articleList[page]?.let { it1 -> + ArticleCardBig( + modifier = Modifier + .width(340.dp) + .height(180.dp) + .graphicsLayer { + val pageOffset = ( + (pagerState.currentPage - page) + pagerState + .currentPageOffsetFraction + ).absoluteValue + + lerp( + start = 0.85f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn( + 0f, + 1f + ) + ).also { scale -> + scaleX = scale + scaleY = scale + } + alpha = lerp( + start = 0.8f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn( + 0f, + 1f + ) + ) + } + .clickable { + it1.id?.let { it2 -> + navigateToSingleArticle( + it2 + ) + } + }, + article = it1 + ) + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(titlePaddingTop)) + + Column( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .padding(horizontal = 20.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "Forums", + style = MaterialTheme.typography.titleMedium, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "See all", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = yellow, + modifier = Modifier + .clickable { + navigateToForum() + } + ) + } + + Spacer(modifier = Modifier.height(titlePaddingBottom)) + + if (forumList != null) { + when(forumList) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Fail to fetch forum", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Success -> { + val latestForumList = forumList.data?.forum + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .height(400.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues( bottom = 16.dp) + ) { + if (latestForumList != null) { + items(latestForumList) { forum -> + ForumBox( + modifier = Modifier.fillMaxWidth(), + title = forum.title, + desc = forum.content, + place = forum.location, + photoUrl = forum.imageURL, + onClick = { navigateToSingleForum(forum.id) } + ) + } + } + } + } + } + } + } } } + } + } +} - DetectionsFab( - modifier = Modifier.padding(bottom = 17.dp) - ) - - DefaultBottomBar(selectedType = BottomBarItemType.Home) +private fun checkAndRequestCameraPermission(context: Context, cameraPermissionLauncher: ManagedActivityResultLauncher, onAlreadyGranted: () -> Unit) { + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) -> { + Log.d("Zhahrany", "Permission has already granted") + onAlreadyGranted() + } + else -> { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) } } } @@ -108,6 +492,16 @@ fun HomeContent() { @Composable fun HomeContentPreview() { TechwasMark02Theme { - HomeContent() + HomeContent( + navigateToCamera = {}, + navigateToArticle = {}, + navigateToForum = {}, + navigateToCatalog = {}, + navigateToMaps = {}, + navigateToSingleArticle = {}, + latestArticleState = UiState.Loading(), + forumList = null, + navigateToSingleForum = {}, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreenViewModel.kt new file mode 100644 index 0000000..9fe4eac --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/home/HomeScreenViewModel.kt @@ -0,0 +1,38 @@ +package com.capstone.techwasmark02.ui.screen.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.repository.TechwasForumApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HomeScreenViewModel @Inject constructor( + private val articleApiRepository: TechwasArticleRepository, + private val forumApiRepository: TechwasForumApiRepository +): ViewModel() { + + private val _latestArticleState: MutableStateFlow?> = MutableStateFlow(null) + val latestArticleState = _latestArticleState.asStateFlow() + + private val _forumList: MutableStateFlow?> = MutableStateFlow(null) + val forumList = _forumList.asStateFlow() + + init { + _latestArticleState.value = UiState.Loading() + _forumList.value = UiState.Loading() + + viewModelScope.launch { + _latestArticleState.value = articleApiRepository.getAllArticle() + _forumList.value = forumApiRepository.fetchAllForum() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreen.kt index 589f85d..1a191b1 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreen.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.capstone.techwasmark02.R import com.capstone.techwasmark02.ui.common.UiState @@ -71,7 +70,7 @@ fun ImageDetectionScreen(viewModel: ImageDetectionScreenViewModel = hiltViewMode imageUri = imageUri, predictImageState = predictImageState, updateImageUri = { viewModel.updateImageUri(it) }, - predictImage = { viewModel.predictImage(it) } + predictImage = { } ) } diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreenViewModel.kt index 828c13f..ace2263 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreenViewModel.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/imageDetection/ImageDetectionScreenViewModel.kt @@ -65,16 +65,16 @@ class ImageDetectionScreenViewModel @Inject constructor( _imageUri.value = newUri } - fun predictImage(context: Context) { - _predictImageState.value = UiState.Loading() - - val imageFileToUpload = _imageUri.value?.let { convertUriToFile(context = context, uri = it) } - viewModelScope.launch { - _predictImageState.value = imageFileToUpload?.let { - predictionApiRepository.predictWaste(it) - } - } - } +// fun predictImage(context: Context) { +// _predictImageState.value = UiState.Loading() +// +// val imageFileToUpload = _imageUri.value?.let { convertUriToFile(context = context, uri = it) } +// viewModelScope.launch { +// _predictImageState.value = imageFileToUpload?.let { +// predictionApiRepository.predictWaste(it) +// } +// } +// } // private fun convertUriToFile(context: Context, uri: Uri): File? { // val inputStream = context.contentResolver.openInputStream(uri) diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreen.kt new file mode 100644 index 0000000..9814eeb --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreen.kt @@ -0,0 +1,90 @@ +package com.capstone.techwasmark02.ui.screen.main + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.ui.component.DefaultBottomBar +import com.capstone.techwasmark02.ui.componentType.BottomBarItemType +import com.capstone.techwasmark02.ui.screen.article.ArticleScreen +import com.capstone.techwasmark02.ui.screen.forum.ForumScreen +import com.capstone.techwasmark02.ui.screen.home.HomeScreen +import com.capstone.techwasmark02.ui.screen.profileUser.ProfileUserScreen + +@Composable +fun MainScreen( + navController: NavHostController, + viewModel: MainScreenViewModel = viewModel(), + page: Int +) { + + val selectedBottomBarType by viewModel.selectedBottomBarType.collectAsState() + val activity = LocalContext.current as Activity + + + BackHandler( + enabled = true, + onBack = { + if(selectedBottomBarType.pageIndex != 0) { + viewModel.updateSelectedBottomBartype(BottomBarItemType.Home) + } else { + activity.finish() + } + } + ) + + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + + LaunchedEffect(Unit) { + when(page) { + 0 -> { + viewModel.updateSelectedBottomBartype(BottomBarItemType.Home) + } + 1 -> { + viewModel.updateSelectedBottomBartype(BottomBarItemType.Forum) + } + 2 -> { + viewModel.updateSelectedBottomBartype(BottomBarItemType.Article) + } + 3 -> { + viewModel.updateSelectedBottomBartype(BottomBarItemType.Profile) + } + } + } + + when(selectedBottomBarType) { + is BottomBarItemType.Home -> { + HomeScreen(navController = navController) + } + is BottomBarItemType.Forum -> { + ForumScreen(navController = navController) + } + is BottomBarItemType.Article -> { + ArticleScreen(navController = navController) + } + is BottomBarItemType.Profile -> { + ProfileUserScreen(navController = navController) + } + } + + DefaultBottomBar( + selectedType = selectedBottomBarType, + onClickBottomNavType = { + viewModel.updateSelectedBottomBartype(it) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreenViewModel.kt new file mode 100644 index 0000000..6691882 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/main/MainScreenViewModel.kt @@ -0,0 +1,19 @@ +package com.capstone.techwasmark02.ui.screen.main + +import androidx.lifecycle.ViewModel +import com.capstone.techwasmark02.ui.componentType.BottomBarItemType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class MainScreenViewModel: ViewModel() { + + private val _selectedBottomBarType: MutableStateFlow = MutableStateFlow( + BottomBarItemType.Home + ) + val selectedBottomBarType = _selectedBottomBarType.asStateFlow() + + fun updateSelectedBottomBartype(newItemType: BottomBarItemType) { + _selectedBottomBarType.value = newItemType + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/maps/MapsScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/maps/MapsScreen.kt new file mode 100644 index 0000000..b630213 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/maps/MapsScreen.kt @@ -0,0 +1,195 @@ +package com.capstone.techwasmark02.ui.screen.maps + +import android.Manifest +import android.content.pm.PackageManager +import android.location.Location +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.ui.navigation.Screen +import com.google.android.gms.location.LocationServices +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.rememberCameraPositionState +import kotlinx.coroutines.tasks.await + +data class MapMarkerInfo( + val position: LatLng, + val title: String, + val snippet: String +) +@Composable +fun MapsScreen(navController: NavHostController) { + + BackHandler(true) { + navController.navigate("${Screen.Main.route}/0") + } + + val diy = LatLng(-7.782275587997325, 110.36709993087182) + + // posisi camera + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(diy, 12f) + } + + // dummy marker + val markerList = listOf( + MapMarkerInfo( + LatLng(-7.782589124314163, 110.38006889168956), + "Drop Point SKE 1", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.8041473764088085, 110.39441386554735), + "Drop Point SKE 2", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.790733483350418, 110.35766494375434), + "Drop Point SKE 3", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.767737238844195, 110.35476371308648), + "Drop Point SKE 4", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.774923700677788, 110.41182124955456), + "Drop Point SKE 5", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.7798743027989685, 110.33864576493167), + "Drop Point SKE 6", + "throw your e-waste here" + ), + MapMarkerInfo( + LatLng(-7.783068208639076, 110.3805524301342), + "Drop Point SKE 7", + "throw your e-waste here" + ) + ) + + val context = LocalContext.current + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) + + var userLatLng by remember { mutableStateOf(null) } + var permissionGranted by remember { mutableStateOf(false) } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + permissionGranted = isGranted + } + + LaunchedEffect(permissionGranted) { + if (permissionGranted) { + val location = fusedLocationClient.lastLocation.await() + if (location != null) { + userLatLng = LatLng(location.latitude, location.longitude) + } + } + } + + LaunchedEffect(Unit) { + when { + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED -> { + // Permission already granted + permissionGranted = true + } + else -> { + // Request permission + launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + } + + GoogleMap( + // posisi camera + cameraPositionState = cameraPositionState, + ) { + // cari marker dengan jarak terdekat + var closestMarker: MapMarkerInfo? = null + var closestDistance = Float.MAX_VALUE + userLatLng?.let { userLocation -> + for (marker in markerList) { + val distanceResults = FloatArray(1) + Location.distanceBetween(userLocation.latitude, userLocation.longitude, marker.position.latitude, marker.position.longitude, distanceResults) + val distance = distanceResults[0] + if (distance < closestDistance) { + closestDistance = distance + closestMarker = marker + } + } + } + + // kasih cat marker nya bg + markerList.forEach { marker -> + val markerIcon = if (marker == closestMarker) { + BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN) + } else { + BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE) + } + + Marker( + state = MarkerState( + position = marker.position, + ), + icon = markerIcon, + title = marker.title, + snippet = marker.snippet + ) + } + + userLatLng?.let { latLng -> + val maxDistance = 5000f // jarak maksimum user ke lokasi terdekat (meter) + if (closestDistance > maxDistance) { + AlertDialog( + onDismissRequest = { }, + title = { + Text( + text = "oh no, your location is too far", + style = MaterialTheme.typography.labelLarge + ) }, + text = { + Text( + text = "you are too far from the drop point", + style = MaterialTheme.typography.bodyMedium + ) }, + confirmButton = { + Button( + onClick = { + navController.popBackStack() + } + ) { + Text("Back") + } + }, + ) + } + + Marker( + state = MarkerState(position = latLng), + title = "Your Location", + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/onBoarding/OnBoardingScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/onBoarding/OnBoardingScreen.kt new file mode 100644 index 0000000..8bfb589 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/onBoarding/OnBoardingScreen.kt @@ -0,0 +1,766 @@ +package com.capstone.techwasmark02.ui.screen.onBoarding + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.component.DefaultButton +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.launch + +@Composable +fun OnBoardingScreen( + navController: NavHostController +) { + OnBoardingContent( + navigateToHome = { navController.navigate(Screen.Home.route) }, + navigateToSignUp = { navController.navigate(Screen.SignUp.route) }, + navigateToSignIn = { navController.navigate(Screen.SignIn.route) }, + navigateToMain = { navController.navigate("${Screen.Main.route}/0") } + ) +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun OnBoardingContent( + navigateToHome: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSignUp: () -> Unit, + navigateToMain: () -> Unit +) { + val pageCount = 3 + val pagerState = rememberPagerState() + val coroutineScope = rememberCoroutineScope() + + + + Box( + modifier = Modifier + .fillMaxSize() + ) { + HorizontalPager( + count = pageCount, + state = pagerState + ) { page -> + when(page) { + 0 -> { + OnBoardingContentFirst() + } + 1 -> { + OnBoardingContentSecond() + } + 2 -> { + OnBoardingContentThird( + navigateToHome = navigateToHome, + navigateToSignIn = navigateToSignIn, + navigateToSignUp = navigateToSignUp, + navigateToMain = navigateToMain + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + ) { + AnimatedVisibility( + visible = pagerState.currentPage != 2, + enter = fadeIn(), + exit = fadeOut() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .padding(top = 28.dp) + .padding(horizontal = 28.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + Button( + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(2) + } + }, + modifier = Modifier + .height(28.dp) + .width(58.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.LightGray + ) + ) { + Text( + text = "", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Default, + fontSize = 10.sp, + fontWeight = FontWeight.Medium + ) + ) + } + + Text( + text = "Skip", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + fontWeight = FontWeight.Medium + ), + color = Color.Black.copy(alpha = 0.6f), + modifier = Modifier + .padding(top = 2.dp) + .clickable { + coroutineScope.launch { + pagerState.animateScrollToPage(2) + } + } + ) + } + + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .padding(bottom = 28.dp) + .padding(horizontal = 28.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .width(170.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val bulletColor = Color.LightGray.copy(alpha = 0.8f) + + Row( + modifier = Modifier + .weight(1f), + horizontalArrangement = Arrangement.Start + ) { + + AnimatedVisibility( + visible = pagerState.currentPage != 0, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton( + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + modifier = Modifier + .background(Color.Transparent) + ) { + Box( + modifier = Modifier + .size(30.dp) + .clip(CircleShape) + .background(bulletColor), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_left), + contentDescription = null, + tint = Color.Black.copy(alpha = 0.4f) + ) + } + } + } + } + + repeat(pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) Color(0xffFFDE59) else bulletColor + +// val size = if (pagerState.currentPage == iteration) 14.dp else 10.dp + + val sidDp: Dp by animateDpAsState( + targetValue = if (pagerState.currentPage == iteration) 14.dp else 10.dp, + animationSpec = tween(durationMillis = 500) + ) + + Box( + modifier = Modifier + .padding(4.dp) + .clip(CircleShape) + .background(color) + .size(sidDp) + ) + } + + Row( + modifier = Modifier + .weight(1f), + horizontalArrangement = Arrangement.End + ) { + AnimatedVisibility( + visible = pagerState.currentPage != 2, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton( + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + modifier = Modifier + .background(Color.Transparent) + ) { + Box( + modifier = Modifier + .size(30.dp) + .clip(CircleShape) + .background(bulletColor), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = null, + tint = Color.Black.copy(alpha = 0.4f) + ) + } + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + + } + } + } +} + +@Composable +fun OnBoardingContentFirst() { + Box( + modifier = Modifier + .fillMaxSize() + + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Image( + painter = painterResource( id = R.drawable.img_onboarding_ripple_peach_left, ), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Image( + painter = painterResource(id = R.drawable.img_onboarding_ripple_purple_right), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 120.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Spacer(modifier = Modifier.height(40.dp)) + + Image( + painter = painterResource(id = R.drawable.img_logo_onboarding), + contentDescription = null, + modifier = Modifier + .size(182.dp) + ) + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = "Techwaste", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "It's an app developed by a\nteam of six students, aiming to\nenhance e-waste disposal!", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.7f + ) + ) + + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Composable +fun OnBoardingContentSecond() { + + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Image( + painter = painterResource(id = R.drawable.img_onboarding_ripple_peach_right), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.img_onboarding_ripple_purple_left), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .height(IntrinsicSize.Max) + .padding(top = 80.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + val boxHeight = 98.dp + + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.img_feature_detect_box), + contentDescription = null, + modifier = Modifier + .width(92.dp) + .height(boxHeight) + ) + Image( + painter = painterResource(id = R.drawable.img_feature_detect_illustration), + contentDescription = null, + modifier = Modifier + .width(134.dp) + .height(127.dp) + ) + Image( + painter = painterResource(id = R.drawable.img_feature_detect_frame), + contentDescription = null, + modifier = Modifier + .size(100.dp) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = (boxHeight + 40.dp)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "E-waste Classification", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + } + } + + Text( + text = "Upload an image of your e-waste and\nTechwaste will determine where it belongs", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.75f + ) + ), + textAlign = TextAlign.Center + ) + +// Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Max) + .padding(horizontal = 20.dp) + ) { + Column( + modifier = Modifier + .width(170.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .height(180.dp) + .padding(bottom = 8.dp), + contentAlignment = Alignment.BottomCenter, + ) { + Image( + painter = painterResource(id = R.drawable.img_feature_article_box), + contentDescription = null, + modifier = Modifier + .height(98.dp) + .width(92.dp) + ) + + Image( + painter = painterResource(id = R.drawable.img_feature_article_illustration), + contentDescription = null, + modifier = Modifier + .size(145.dp) + ) + } + + Text( + text = "Articles", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + ) + + Text( + text = "Know more about your\ne-waste and find out how to\nget fid of it properly", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.75f + ) + ), + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Box( + modifier = Modifier + .fillMaxHeight() + .width(170.dp), + contentAlignment = Alignment.TopCenter + ) { + Box( + modifier = Modifier + .height(180.dp) + .fillMaxWidth() + .padding(bottom = 8.dp), + contentAlignment = Alignment.BottomCenter, + ) { + Image( + painter = painterResource(id = R.drawable.img_feature_forum_box), + contentDescription = null, + modifier = Modifier + .height(98.dp) + .width(92.dp) + ) + + } + + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(48.dp)) + Image( + painter = painterResource(id = R.drawable.img_feature_forum_illustration), + contentDescription = null, + modifier = Modifier + .height(166.dp) + .width(166.dp) + ) + } + + + Column( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "Forum", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ), + ) + + Text( + text = "Ask questions, share your\nthoughts, drop any interesting\nfacts about e-waste", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.75f + ) + ), + textAlign = TextAlign.Center + ) + } + } + } + } + } +} + +@Composable +fun OnBoardingContentThird( + navigateToHome: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSignUp: () -> Unit, + navigateToMain: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(id = R.drawable.img_onboarding_ripple_peach_left), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Image( + painter = painterResource(id = R.drawable.img_onboarding_ripple_purple_right), + contentDescription = null, + modifier = Modifier + .height(200.dp) + .width(240.dp) + ) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 120.dp, bottom = 100.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.img_logo_onboarding_nooutline_green), + contentDescription = null, + modifier = Modifier + .height(164.dp) + .width(170.dp) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Text( + text = "Help us take care of\nelectronics waste.", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center + ) + + Text( + text = "What would you like to do?", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.7f + ), + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(48.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DefaultButton( + contentText = "Let's get started", + modifier = Modifier + .height(50.dp) + .fillMaxWidth(), + buttonColors = ButtonDefaults.buttonColors( + containerColor = Color(0xffF7B595) + ), + onClick = navigateToSignUp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DefaultButton( + contentText = "Already have account", + modifier = Modifier + .height(50.dp) + .fillMaxWidth(), + buttonColors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = Color.Black.copy( + alpha = 0.5f + ) + ), + onClick = navigateToSignIn + ) + + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun OnBoardingScreenPreview() { + TechwasMark02Theme { + OnBoardingContent( + navigateToSignIn = {}, + navigateToSignUp = {}, + navigateToHome = {}, + navigateToMain = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun OnBoardingContentFirstPreview() { + TechwasMark02Theme { + OnBoardingContentFirst() + } +} + +@Preview( showBackground = true) +@Composable +fun OnBoardingContentSecondPreview() { + TechwasMark02Theme { + OnBoardingContentSecond() + } +} + +@Preview(showBackground = true) +@Composable +fun OnBoardingContentThirdPreview() { + TechwasMark02Theme { + OnBoardingContentThird( + navigateToSignUp = {}, + navigateToHome = {}, + navigateToSignIn = {}, + navigateToMain = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreen.kt new file mode 100644 index 0000000..a4b3365 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreen.kt @@ -0,0 +1,408 @@ +package com.capstone.techwasmark02.ui.screen.profileUser + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.rememberAsyncImagePainter +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.ArticleCardSmall +import com.capstone.techwasmark02.ui.component.ForumBox +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme + +@Composable +fun ProfileUserScreen( + viewModel: ProfileUserScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + val userSession by viewModel.userSessionState.collectAsState() + val bookmarkedArticleState by viewModel.bookmarkedArticleState.collectAsState() + val favoriteArticlesList by viewModel.favoriteArticlesFlow.collectAsState(initial = null) + val forumList by viewModel.forumList.collectAsState() + + ProfileUserContent( + navigateToSetting = { navController.navigate(Screen.Setting.route) }, + userSession = userSession, + bookmarkedArticleState = bookmarkedArticleState, + favoriteArticleList = favoriteArticlesList, + navigateToSingleArticle = { navController.navigate("${Screen.SingleArticle.route}/$it") }, + forumList = forumList, + navigateToSingleForum = { navController.navigate("${Screen.SingleForum.route}/$it")} + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileUserContent( + navigateToSetting: () -> Unit, + userSession: UserSession?, + bookmarkedArticleState: UiState?, + favoriteArticleList: List?, + navigateToSingleArticle: (idArticle: Int) -> Unit, + forumList: UiState?, + navigateToSingleForum: (Int) -> Unit +) { + + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + title = {}, + actions = { + IconButton(onClick = { navigateToSetting() }) { + Icon( + painter = painterResource(id = R.drawable.ic_settings), + contentDescription = null, + tint = Color.White + ) + } + } + ) + }, + ) { innerPadding -> + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(innerPadding) + .padding(bottom = 80.dp) + ) { + Box( + modifier = Modifier + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(244.dp) + .clip( + RoundedCornerShape( + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .background(MaterialTheme.colorScheme.primary) + .padding(top = 20.dp), + ) { + Column( + modifier = Modifier.align(Alignment.TopCenter), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier + .size(120.dp) + .clip(CircleShape), + painter = rememberAsyncImagePainter( + model = R.drawable.img_user_1, + placeholder = painterResource(id = R.drawable.place_holder), + ), + contentScale = ContentScale.FillHeight, + contentDescription = null + ) + + Spacer(modifier = Modifier.height(24.dp)) + + if (userSession != null) { + Text( + text = userSession.userNameId.username, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold + ), + color = Color.White + ) + } else { + Text( + text = "User Full Name", + style = MaterialTheme.typography.labelLarge, + color = Color.White + ) + } +// Text( +// text = "user@gmail.com", +// style = MaterialTheme.typography.bodyMedium, +// color = Color.White +// ) + } + } + +// Column( +// modifier = Modifier +// .fillMaxWidth() +// .padding(top = 200.dp), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// ProfileBox( +// modifier = Modifier +// .padding(horizontal = 20.dp), +// navigateToSetting = navigateToSetting +// ) +// } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .height(700.dp) + ) { + Text( + text = "Bookmarks", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (!favoriteArticleList.isNullOrEmpty() && favoriteArticleList.isNotEmpty()) { + favoriteArticleList.let { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + count = favoriteArticleList.size, + ) { index -> + val article = favoriteArticleList[index] + + ArticleCardSmall( + modifier = Modifier + .width(150.dp) + .clickable { + navigateToSingleArticle(article.id) + }, + imgUrl = article.articleImageURL, + title = article.name, + description = article.desc + ) + } + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(175.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "There's no related article", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + +// if (bookmarkedArticleState != null) { +// when(bookmarkedArticleState) { +// is UiState.Loading -> { +// Box( +// modifier = Modifier +// .fillMaxWidth() +// .height(100.dp), +// contentAlignment = Alignment.Center +// ) { +// CircularProgressIndicator() +// } +// } +// is UiState.Error -> { +// Box( +// modifier = Modifier +// .fillMaxWidth() +// .height(100.dp), +// contentAlignment = Alignment.Center +// ) { +// Text(text = "No article to view") +// } +// } +// is UiState.Success -> { +// bookmarkedArticleState.data?.articleList?.size?.let { +// LazyRow( +// contentPadding = PaddingValues(horizontal = 16.dp), +// horizontalArrangement = Arrangement.spacedBy(16.dp) +// ) { +// items( +// count = it, +// ) { page -> +// val article = bookmarkedArticleState.data.articleList[page] +// +// ArticleCardSmall( +// modifier = Modifier.width(150.dp), +// imgUrl = article?.articleImageURL, +// title = article?.name, +// description = article?.desc +// ) +// } +// } +// } +// } +// } +// } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Forum Histories", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (forumList != null) { + when(forumList) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Color.Red, + shape = RoundedCornerShape(20.dp) + ) + .padding(8.dp) + + ) { + Text( + text = "Fail to fetch forum", + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + } + } + } + is UiState.Success -> { + val latestForumList = forumList.data?.forum + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .height(400.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues( vertical = 10.dp) + ) { + if (latestForumList != null) { + items(latestForumList) { forum -> + ForumBox( + modifier = Modifier.fillMaxWidth(), + title = forum.title, + desc = forum.content, + place = forum.location, + photoUrl = forum.imageURL, + onClick = { navigateToSingleForum(forum.id) } + ) + + } + } + } + } + } + } + } + } + } +} + +@Preview +@Composable +fun ProfileUserScreenPreview() { + TechwasMark02Theme { + ProfileUserContent( + navigateToSetting = {}, + userSession = UserSession( + userNameId = UserId( + username = "Ghina", + id = 1 + ), + userLoginToken = Token( + accessToken = "" + ) + ), + bookmarkedArticleState = null, + favoriteArticleList = null, + navigateToSingleArticle = {}, + forumList = null, + navigateToSingleForum = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreenViewModel.kt new file mode 100644 index 0000000..f67791f --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/profileUser/ProfileUserScreenViewModel.kt @@ -0,0 +1,64 @@ +package com.capstone.techwasmark02.ui.screen.profileUser + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.common.Resource +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.ArticleResultResponse +import com.capstone.techwasmark02.data.remote.response.ForumResponse +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.repository.FavoriteArticleRepository +import com.capstone.techwasmark02.repository.PreferencesRepository +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.repository.TechwasForumApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProfileUserScreenViewModel @Inject constructor( + private val preferencesRepository: PreferencesRepository, + private val articleApiRepository: TechwasArticleRepository, + private val favoriteArticleRepository: FavoriteArticleRepository, + private val forumApiRepository: TechwasForumApiRepository +): ViewModel() { + + private val _userSessionState: MutableStateFlow = MutableStateFlow(null) + val userSessionState = _userSessionState.asStateFlow() + + private val _bookmarkedArticleState: MutableStateFlow?> = MutableStateFlow(null) + val bookmarkedArticleState = _bookmarkedArticleState.asStateFlow() + + val favoriteArticlesFlow = favoriteArticleRepository.getFavArticles() + + private val _forumList: MutableStateFlow?> = MutableStateFlow(null) + val forumList = _forumList.asStateFlow() + + init { + _bookmarkedArticleState.value = UiState.Loading() + _forumList.value = UiState.Loading() + + viewModelScope.launch { + _bookmarkedArticleState.value = articleApiRepository.getAllArticle() + _forumList.value = forumApiRepository.fetchAllForum() + + val result = preferencesRepository.getActiveSession() + when(result) { + is Resource.Error -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "", id = 0) + ) + } + is Resource.Success -> { + _userSessionState.value = result.data + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreen.kt new file mode 100644 index 0000000..a546c35 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreen.kt @@ -0,0 +1,298 @@ +package com.capstone.techwasmark02.ui.screen.setting + +import android.content.Intent +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import coil.compose.rememberAsyncImagePainter +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.ui.component.InverseTopBar +import com.capstone.techwasmark02.ui.component.SettingItem +import com.capstone.techwasmark02.ui.componentType.SettingItemType +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import com.capstone.techwasmark02.ui.theme.red +import kotlinx.coroutines.launch + +@Composable +fun SettingScreen( + navController: NavHostController, + viewModel: SettingScreenViewModel = hiltViewModel() +) { + + val userSession by viewModel.userSessionState.collectAsState() + + SettingContent( + navigateToProfile = { navController.popBackStack() }, + userSession = userSession, + navigateToOnBoarding = { navController.navigate(Screen.OnBoarding.route)}, + logOutUser = { viewModel.clearUserSession() }, + navigateBackToMain = { navController.navigate("${Screen.Main.route}/3") } + ) +} + +@Composable +fun SettingContent( + navigateToProfile: () -> Unit, + userSession: UserSession?, + navigateToOnBoarding: () -> Unit, + logOutUser: () -> Unit, + navigateBackToMain: () -> Unit +) { + BackHandler(true) { + navigateBackToMain() + } + + val settingItemList = listOf( + SettingItemType.Password, + SettingItemType.Comment, + SettingItemType.Notification, + SettingItemType.Language + ) + + LaunchedEffect(userSession) { + if (userSession?.userNameId?.username == "-") { + navigateToOnBoarding() + } + } + + Box( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Image( + painter = painterResource(id = R.drawable.img_profile_ripple_green_reverse), + contentDescription = null + ) + } + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(id = R.drawable.img_profile_ripple_green), + contentDescription = null + ) + } + } + + + Column( + modifier = Modifier + .padding(top = 100.dp) + ) { + Box( + modifier = Modifier.fillMaxHeight() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + + ) { + Column( + modifier = Modifier + .fillMaxSize() + .align(Alignment.TopCenter), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box { + Image( + modifier = Modifier + .size(120.dp) + .clip(CircleShape), + painter = rememberAsyncImagePainter( + model = R.drawable.img_user_1, + placeholder = painterResource(id = R.drawable.place_holder), + ), + contentScale = ContentScale.FillHeight, + contentDescription = null + ) + Box( + modifier = Modifier + .matchParentSize() + .clip(CircleShape) + .background(Color.Black.copy(alpha = 0.3f)) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_camera_fill), + contentDescription = "Camera", + tint = Color.White, + modifier = Modifier + .align(Alignment.Center) + .size(32.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = userSession?.userNameId?.username ?: "User Full Name", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onTertiary + ) + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit", + tint = MaterialTheme.colorScheme.onTertiary, + modifier = Modifier + .padding(start = 8.dp) + .size(24.dp) + ) + } + +// Text( +// text = "user@gmail.com", +// style = MaterialTheme.typography.bodyMedium, +// color = Color.White +// ) + + Spacer(modifier = Modifier.height(40.dp)) + + Box(modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SettingItem( + icon = settingItemList[0].icon, + title = settingItemList[0].title, + ) + + SettingItem( + icon = settingItemList[1].icon, + title = settingItemList[1].title, + ) + + SettingItem( + icon = settingItemList[2].icon, + title = settingItemList[2].title, + ) + + val context = LocalContext.current + val scope = rememberCoroutineScope() + SettingItem( + icon = settingItemList[3].icon, + title = settingItemList[3].title, + modifier = Modifier.clickable { + scope.launch { + val intent = Intent(Settings.ACTION_LOCALE_SETTINGS) + context.startActivity(intent) + } + } + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Button( + onClick = logOutUser, + modifier = Modifier + .width(122.dp) + .height(41.dp) + .align(Alignment.End), + colors = ButtonDefaults.buttonColors( + containerColor = red.copy( + alpha = 0.9f + ) + ), + shape = RoundedCornerShape(10.dp) + ) { + Text(text = "Log out") + } + } + } + } + } + } + } + + InverseTopBar( + onClickNavigationIcon = { + navigateBackToMain() + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SettingScreenPreview() { + TechwasMark02Theme { + SettingContent( + navigateToProfile = {}, + userSession = UserSession( + userNameId = UserId( + username = "Ghina", + id = 1 + ), + userLoginToken = Token( + accessToken = "" + ) + ), + navigateToOnBoarding = {}, + logOutUser = {}, + navigateBackToMain = {} + ) + } +} diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreenViewModel.kt new file mode 100644 index 0000000..a1eb95f --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/setting/SettingScreenViewModel.kt @@ -0,0 +1,58 @@ +package com.capstone.techwasmark02.ui.screen.setting + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.common.Resource +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.repository.PreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingScreenViewModel @Inject constructor( + private val preferencesRepository: PreferencesRepository +): ViewModel() { + + private val _userSessionState: MutableStateFlow = MutableStateFlow(null) + val userSessionState = _userSessionState.asStateFlow() + + init { + viewModelScope.launch { + val result = preferencesRepository.getActiveSession() + when(result) { + is Resource.Error -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "", id = 0) + ) + } + is Resource.Success -> { + _userSessionState.value = result.data + } + } + } + } + + fun clearUserSession() { + viewModelScope.launch { + val result = preferencesRepository.clearSession() + when(result) { + is Resource.Error -> { + // do nothing + } + is Resource.Success -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "-", id = 0) + ) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreen.kt index faf1b82..34f8375 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreen.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreen.kt @@ -1,5 +1,6 @@ package com.capstone.techwasmark02.ui.screen.signIn +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,41 +16,51 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.* +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController import com.capstone.techwasmark02.data.model.UserLoginInfo -import com.capstone.techwasmark02.data.model.UserSession import com.capstone.techwasmark02.data.remote.response.UserLoginResponse import com.capstone.techwasmark02.ui.common.UiState import com.capstone.techwasmark02.ui.component.DefaultButton import com.capstone.techwasmark02.ui.component.DefaultTextField import com.capstone.techwasmark02.ui.component.PasswordTextField import com.capstone.techwasmark02.ui.component.SignInBanner +import com.capstone.techwasmark02.ui.navigation.Screen import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme @Composable fun SignInScreen( viewModel: SignInScreenViewModel = hiltViewModel(), + navController: NavHostController ) { val userToSignInState by viewModel.userToSignInState.collectAsState() val userToSignInInfo by viewModel.userToSignInInfo.collectAsState() - val userSessionState by viewModel.userSessionState.collectAsState() + val savedUsername by viewModel.savedUsername.collectAsState() +// val userSessionState by viewModel.userSessionState.collectAsState() SignInContent( userToSignInInfo = userToSignInInfo, updateUserLoginInfo = { viewModel.updateUserSignInInfo(it) }, userToSignInState = userToSignInState, signInUser = { viewModel.signInUser() }, - userSessionState = userSessionState + saveUserSession = { viewModel.saveUserSession() }, + navigateToMain = { navController.navigate("${Screen.Main.route}/0") }, + savedUsername = savedUsername +// userSessionState = userSessionState, ) } @@ -59,13 +70,18 @@ fun SignInContent( updateUserLoginInfo: (UserLoginInfo) -> Unit, userToSignInState: UiState?, signInUser: () -> Unit, - userSessionState: UserSession? + saveUserSession: () -> Unit, + navigateToMain: () -> Unit, + savedUsername: String? +// userSessionState: UserSession? ) { var showPassword by remember { mutableStateOf(false) } + val context = LocalContext.current + Column( modifier = Modifier .fillMaxSize() @@ -74,7 +90,6 @@ fun SignInContent( ) { SignInBanner( modifier = Modifier - ) Spacer(modifier = Modifier.height(16.dp)) @@ -93,15 +108,19 @@ fun SignInContent( ) { Text( text = "Sign In", - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.8f) ) Text( - text = "Please sign in to continue", - style = MaterialTheme.typography.bodySmall + text = "Please sign in! We're excited to have you on board!", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.8f) ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(28.dp)) DefaultTextField( value = userToSignInInfo.email, @@ -137,7 +156,10 @@ fun SignInContent( ) { Text( text = "Forgot your password?", - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium + ), + color = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.8f) ) } @@ -158,8 +180,14 @@ fun SignInContent( } } is UiState.Success -> { - userToSignInState.data?.loginResult?.token?.accessToken?.let { - Text(text = it) + saveUserSession() + + if (savedUsername != null && savedUsername != "") { + + LaunchedEffect(Unit) { + Toast.makeText(context, "Welcome $savedUsername", Toast.LENGTH_SHORT).show() + navigateToMain() + } } } } @@ -168,13 +196,6 @@ fun SignInContent( Spacer(modifier = Modifier.weight(1f)) } - if (userSessionState != null) { - Text( - text = userSessionState.userLoginToken.accessToken, - modifier = Modifier.padding(vertical = 10.dp) - ) - } - DefaultButton( contentText = "Sign In", modifier = Modifier @@ -192,7 +213,8 @@ fun SignInContent( ) { Text( text = "Don't have an account?", - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiary.copy(alpha = 0.8f) ) Spacer(modifier = Modifier.width(2.dp)) @@ -220,7 +242,10 @@ fun SingInContentPreview() { userToSignInInfo = UserLoginInfo("", ""), updateUserLoginInfo = {}, signInUser = {}, - userSessionState = null + saveUserSession = {}, + navigateToMain = {}, + savedUsername = null +// userSessionState = null ) } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreenViewModel.kt index 0492c01..c4722d5 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreenViewModel.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signIn/SignInScreenViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import com.capstone.techwasmark02.common.Resource import com.capstone.techwasmark02.data.mappers.toUserSession import com.capstone.techwasmark02.data.model.UserLoginInfo -import com.capstone.techwasmark02.data.model.UserSession import com.capstone.techwasmark02.data.remote.response.UserLoginResponse import com.capstone.techwasmark02.repository.PreferencesRepository import com.capstone.techwasmark02.repository.TechwasUserApiRepository @@ -32,38 +31,28 @@ class SignInScreenViewModel @Inject constructor( )) val userToSignInInfo = _userToSignInInfo.asStateFlow() - private val _userSessionState: MutableStateFlow = MutableStateFlow(null) - val userSessionState = _userSessionState.asStateFlow() + private val _savedUsername: MutableStateFlow = MutableStateFlow(null) + val savedUsername = _savedUsername.asStateFlow() - init { + fun signInUser() { + _userToSignInState.value = UiState.Loading() viewModelScope.launch { - val result = preferencesRepository.getActiveSession() - when(result) { - is Resource.Error -> { - _userSessionState.value = null - } - is Resource.Success -> { - _userSessionState.value = result.data - } - } + _userToSignInState.value = userApiRepository.userLogin(_userToSignInInfo.value) } } - fun signInUser() { - _userToSignInState.value = UiState.Loading() + fun saveUserSession() { viewModelScope.launch { - val result = userApiRepository.userLogin(_userToSignInInfo.value) - when(result) { - is UiState.Success -> { - _userToSignInState.value = result - result.data?.loginResult?.toUserSession() - ?.let { preferencesRepository.saveSession(it) } - } - is UiState.Error -> { - _userToSignInState.value = result - } - else -> { - // do nothing + val userSession = _userToSignInState.value?.data?.loginResult?.toUserSession() + if (userSession != null) { + val result = preferencesRepository.saveSession(userSession) + when(result) { + is Resource.Error -> { + _savedUsername.value = "" + } + is Resource.Success -> { + _savedUsername.value = result.data?.userNameId?.username + } } } } diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreen.kt index e2d2b39..416c455 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreen.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreen.kt @@ -1,134 +1,189 @@ package com.capstone.techwasmark02.ui.screen.signUp +import android.widget.Toast import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.data.model.UserRegisterInfo +import com.capstone.techwasmark02.data.remote.response.UserRegisterResponse +import com.capstone.techwasmark02.ui.common.UiState import com.capstone.techwasmark02.ui.component.DefaultButton import com.capstone.techwasmark02.ui.component.DefaultTextField -import com.capstone.techwasmark02.ui.component.InverseTopBar import com.capstone.techwasmark02.ui.component.PasswordTextField import com.capstone.techwasmark02.ui.component.SignUpBanner +import com.capstone.techwasmark02.ui.navigation.Screen import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme @Composable -fun SignUpScreen() { - SignUpContent() +fun SignUpScreen( + viewModel: SignUpScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + val userToSignUpState by viewModel.userToSignUpState.collectAsState() + val userToSignUpInfo by viewModel.userToSignUpInfo.collectAsState() + + SignUpContent( + userToSignUpInfo = userToSignUpInfo, + updateUserRegisterInfo = { viewModel.updateUserSignUpInfo(it) }, + userToSignUpState = userToSignUpState, + signUpUser = { viewModel.signUpUser() }, + navigateToSignIn = { navController.navigate(Screen.SignIn.route)} + ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun SignUpContent() { - var email by remember { - mutableStateOf("") - } - - var password by remember { - mutableStateOf("") - } +fun SignUpContent( + userToSignUpInfo: UserRegisterInfo, + updateUserRegisterInfo: (UserRegisterInfo) -> Unit, + userToSignUpState: UiState?, + signUpUser: () -> Unit, + navigateToSignIn: () -> Unit +) { var showPassword by remember { mutableStateOf(false) } - var fullName by remember { - mutableStateOf("") - } + val context = LocalContext.current - Scaffold( - topBar = { - InverseTopBar( - onClickNavigationIcon = {} - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .padding(top = 26.dp, bottom = 30.dp) - .padding(horizontal = 20.dp) - ) { - SignUpBanner() + Column( + modifier = Modifier + .padding(top = 26.dp, bottom = 30.dp) + .padding(horizontal = 20.dp) + ) { + SignUpBanner() - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Column( - modifier = Modifier - .fillMaxSize() - .shadow( - elevation = 6.dp, - shape = MaterialTheme.shapes.large, - clip = true - ) - .clip(MaterialTheme.shapes.large) - .background(MaterialTheme.colorScheme.tertiary) - .padding(horizontal = 20.dp, vertical = 30.dp) - ) { - Text( - text = "Sign Up", - style = MaterialTheme.typography.headlineSmall + Column( + modifier = Modifier + .fillMaxSize() + .shadow( + elevation = 6.dp, + shape = MaterialTheme.shapes.large, + clip = true ) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 20.dp, vertical = 30.dp) + ) { + Text( + text = "Sign Up", + style = MaterialTheme.typography.headlineSmall + ) - Text( - text = "Just a few steps to help the earth managing e-waste!", - style = MaterialTheme.typography.bodySmall + Text( + text = "Just a few steps to help the earth managing e-waste!", + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium ) + ) - Spacer(modifier = Modifier.height(20.dp)) - - DefaultTextField( - value = fullName, - onValueChange = { newValue -> fullName = newValue}, - labelText = "Full Name", - placeHolderText = "user full name", - modifier = Modifier.fillMaxWidth() - ) + Spacer(modifier = Modifier.height(20.dp)) + + DefaultTextField( + value = userToSignUpInfo.fullname, + onValueChange = { newValue -> + updateUserRegisterInfo(userToSignUpInfo.copy( + fullname = newValue + )) + }, + labelText = "Full Name", + placeHolderText = "user full name", + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - DefaultTextField( - value = email, - onValueChange = { newValue -> email = newValue}, - labelText = "Email", - placeHolderText = "user email", - modifier = Modifier.fillMaxWidth() - ) + DefaultTextField( + value = userToSignUpInfo.email, + onValueChange = { newValue -> + updateUserRegisterInfo(userToSignUpInfo.copy( + email = newValue + )) + }, + labelText = "Email", + placeHolderText = "user email", + modifier = Modifier.fillMaxWidth() + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - PasswordTextField( - value = password, - onValueChange = { newValue -> password = newValue}, - showPassword = showPassword, - toggleShowPassword = { showPassword = !showPassword }, - modifier = Modifier.fillMaxWidth() - ) + PasswordTextField( + value = userToSignUpInfo.password, + onValueChange = { newValue -> + updateUserRegisterInfo(userToSignUpInfo.copy( + password = newValue + )) + }, + showPassword = showPassword, + toggleShowPassword = { showPassword = !showPassword }, + modifier = Modifier.fillMaxWidth() + ) + if(userToSignUpState != null) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + when(userToSignUpState) { + is UiState.Loading -> { + CircularProgressIndicator() + } + is UiState.Error -> { + userToSignUpState.message?.let { + Text(text = it) + } + } + is UiState.Success -> { + + Toast.makeText(context, userToSignUpState.data?.message, Toast.LENGTH_SHORT).show() + + LaunchedEffect(Unit) { + navigateToSignIn() + } + } + } + } + } else { Spacer(modifier = Modifier.weight(1f)) + } - DefaultButton(contentText = "Sign Up", modifier = Modifier + DefaultButton( + contentText = "Sign Up", + modifier = Modifier .fillMaxWidth() - .height(50.dp)) - - } + .height(50.dp), + onClick = signUpUser + ) } } } @@ -137,6 +192,12 @@ fun SignUpContent() { @Composable fun SingUpContentPreview() { TechwasMark02Theme { - SignUpContent() + SignUpContent( + userToSignUpState = null, + userToSignUpInfo = UserRegisterInfo("", "", ""), + updateUserRegisterInfo = {}, + signUpUser = {}, + navigateToSignIn = {} + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreenViewModel.kt index a1dd5f2..053ef28 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreenViewModel.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/signUp/SignUpScreenViewModel.kt @@ -1,15 +1,45 @@ package com.capstone.techwasmark02.ui.screen.signUp import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.model.UserRegisterInfo +import com.capstone.techwasmark02.data.remote.response.UserRegisterResponse import com.capstone.techwasmark02.repository.PreferencesRepository import com.capstone.techwasmark02.repository.TechwasUserApiRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject +@HiltViewModel class SignUpScreenViewModel @Inject constructor( private val userApiRepository: TechwasUserApiRepository, - private val preferencesRepository: PreferencesRepository ): ViewModel() { -// private val _userToSignUpState + private val _userToSignUpState: MutableStateFlow?> = + MutableStateFlow(null) + val userToSignUpState = _userToSignUpState.asStateFlow() + private val _userToSignUpInfo: MutableStateFlow = + MutableStateFlow( + UserRegisterInfo( + fullname = "", + email = "", + password = "" + ) + ) + val userToSignUpInfo = _userToSignUpInfo.asStateFlow() + + fun signUpUser() { + _userToSignUpState.value = UiState.Loading() + viewModelScope.launch { + _userToSignUpState.value = userApiRepository.userRegister(_userToSignUpInfo.value) + } + } + + fun updateUserSignUpInfo(userToSignUpInfo: UserRegisterInfo) { + _userToSignUpInfo.value = userToSignUpInfo + } } \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreen.kt new file mode 100644 index 0000000..7ae09a5 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreen.kt @@ -0,0 +1,327 @@ +package com.capstone.techwasmark02.ui.screen.singleArticle + +import android.content.Intent +import android.text.Html +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavHostController +import coil.compose.rememberAsyncImagePainter +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity +import com.capstone.techwasmark02.data.model.FavoriteArticle +import com.capstone.techwasmark02.data.remote.response.SingleArticleResponse +import com.capstone.techwasmark02.ui.common.UiState +import com.capstone.techwasmark02.ui.component.DefaultButton +import com.capstone.techwasmark02.ui.component.HtmlText +import com.capstone.techwasmark02.ui.component.TransparentTopBar +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.Mist97 +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlinx.coroutines.launch + +@Composable +fun SingleArticleScreen( + idArticle: Int, + viewModel: SingleArticleScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + + LaunchedEffect(Unit) { + viewModel.viewModelScope.launch { + viewModel.getArticleById(idArticle) + viewModel.getFavArticleById(idArticle) + } + } + + val articleResult by viewModel.articleResult.collectAsState() + + val isArticleFavorited by viewModel.isArticleFavorited.collectAsState(initial = null) + + SingleArticleContent( + articleResult = articleResult, + navigateToArticle = { navController.navigate("${Screen.Main.route}/2") }, + isArticleFavorited = isArticleFavorited, + updateArticleFavorited = { articleGetFavorited, favoriteArticle -> viewModel.updateArticleFavorited(articleGetFavorited, favoriteArticle)} + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SingleArticleContent( + articleResult: UiState?, + navigateToArticle: () -> Unit, + isArticleFavorited: FavoriteArticleEntity?, + updateArticleFavorited: ((articleGetFavorited: Boolean, favoriteArticle: FavoriteArticle) -> Unit) +) { + val context = LocalContext.current + + BackHandler(true) { + navigateToArticle() + } + + val result = articleResult?.data?.article + + Scaffold { innerPadding -> + val scrollState = rememberScrollState() + + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + if (articleResult != null) { + when(articleResult) { + is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is UiState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + ) + } + is UiState.Success -> { + val currentArticle = articleResult.data?.article?.get(0) + val isArticleFavorite = currentArticle?.id == isArticleFavorited?.id + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .verticalScroll(scrollState) + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .background(Mist97) + ) { + Column( + modifier = Modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(300.dp) + .clip( + RoundedCornerShape( + topStart = 0.dp, + topEnd = 0.dp, + bottomEnd = 16.dp, + bottomStart = 16.dp + ) + ) + ) { + Image( + modifier = Modifier.matchParentSize(), + painter = rememberAsyncImagePainter( + model = result?.get(0)?.articleImageURL, + placeholder = painterResource(id = R.drawable.place_holder), + ), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + + Column(modifier = Modifier + .padding(bottom = 10.dp, top = 10.dp) + .padding(horizontal = 16.dp) + ) { + result?.get(0)?.componentName?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + result?.get(0)?.name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + ) + } + Text( + text = "source: techwaste", + style = MaterialTheme.typography.bodyMedium + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp, top = 270.dp), + contentAlignment = Alignment.CenterEnd + ) { + + IconButton( + onClick = { + val favoritedArticle = currentArticle?.id?.let { + currentArticle.name?.let { it1 -> + currentArticle.articleImageURL?.let { it2 -> + currentArticle.componentID?.let { it3 -> + currentArticle.description?.let { it4 -> + FavoriteArticle( + id = it, + name = it1, + imageURL = it2, + compId = it3, + desc = it4 + ) + } + } + } + } + } + if (isArticleFavorite) { + if (favoritedArticle != null) { + updateArticleFavorited(false, favoritedArticle) + } + } else { + if (favoritedArticle != null) { + updateArticleFavorited(true, favoritedArticle) + } + } + }, + modifier = Modifier + .clip(CircleShape) + .shadow(elevation = 4.dp, clip = true) + .background(Color.White) + .size(50.dp) + ) { + Icon( + Icons.Default.Favorite, + tint = if (isArticleFavorite) Color.Red else Color.LightGray, + contentDescription = null + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + result?.get(0)?.description?.let { + HtmlText( + html = it, + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = Color.Black, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ) + ) + } + + Spacer(modifier = Modifier.height(36.dp)) + + val plainText = Html.fromHtml(result?.get(0)?.description).toString() + + DefaultButton( + contentText = "Share", + onClick = { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_SUBJECT, result?.get(0)?.name) + intent.putExtra(Intent.EXTRA_TEXT, + "$plainText\nSource: Techwaste" + ) + context.startActivity(intent) + }, + modifier = Modifier.width(150.dp) + ) + } + } + } + } + } + + Box( + modifier = Modifier + .matchParentSize() + .background( + Brush.verticalGradient( + colors = listOf( + Color.Black.copy(alpha = 0.7f), + Color.Transparent + ), + startY = 0f, + endY = 600f + ) + ) + ) { + TransparentTopBar(onClickNavigationIcon = { navigateToArticle() }, pageTitle = "Detail") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun SingleArticleScreenPreview() { + TechwasMark02Theme { + SingleArticleContent( + articleResult = UiState.Loading(), + navigateToArticle = {}, + isArticleFavorited = null, + updateArticleFavorited = fun(articleGetFavorited: Boolean, + favoriteArticle: FavoriteArticle) {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreenViewModel.kt new file mode 100644 index 0000000..546742b --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/singleArticle/SingleArticleScreenViewModel.kt @@ -0,0 +1,54 @@ +package com.capstone.techwasmark02.ui.screen.singleArticle + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.data.local.database.entity.FavoriteArticleEntity +import com.capstone.techwasmark02.data.model.FavoriteArticle +import com.capstone.techwasmark02.data.remote.response.SingleArticleResponse +import com.capstone.techwasmark02.repository.FavoriteArticleRepository +import com.capstone.techwasmark02.repository.TechwasArticleRepository +import com.capstone.techwasmark02.ui.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SingleArticleScreenViewModel @Inject constructor( + private val articleRepository: TechwasArticleRepository, + private val favoriteArticleRepository: FavoriteArticleRepository +): ViewModel() { + + private val _articleResult: MutableStateFlow?> = MutableStateFlow(null) + val articleResult = _articleResult.asStateFlow() + + fun getArticleById(id: Int) { + _articleResult.value = UiState.Loading() + viewModelScope.launch { + _articleResult.value = articleRepository.getArticleById(id) + } + } + + var isArticleFavorited: Flow = favoriteArticleRepository.getFavArticleById(id = 0) + + fun getFavArticleById(id: Int) { + isArticleFavorited = favoriteArticleRepository.getFavArticleById(id = id) + } + + fun updateArticleFavorited( + articleGetFavorited: Boolean, + favoriteArticle: FavoriteArticle + ) { + if (articleGetFavorited) { + viewModelScope.launch { + favoriteArticleRepository.upsertFavoriteArticle(favoriteArticle) + } + } else { + viewModelScope.launch { + favoriteArticleRepository.deleteFavoriteArticle(favoriteArticle) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreen.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreen.kt new file mode 100644 index 0000000..51d08f8 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreen.kt @@ -0,0 +1,134 @@ +package com.capstone.techwasmark02.ui.screen.splashScreen + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.capstone.techwasmark02.R +import com.capstone.techwasmark02.ui.navigation.Screen +import com.capstone.techwasmark02.ui.theme.Green77 +import com.capstone.techwasmark02.ui.theme.TechwasMark02Theme +import kotlinx.coroutines.delay + +@Composable +fun SplashScreen( + viewModel: SplashScreenViewModel = hiltViewModel(), + navController: NavHostController +) { + + val userSession by viewModel.userSessionState.collectAsState() + + var animationState by remember { + mutableStateOf(false) + } + + val alphaAnim = animateFloatAsState( + targetValue = if (animationState) 1f else 0f, + animationSpec = tween( + durationMillis = 1000 // 3 sec + ) + ) + + LaunchedEffect(key1 = true) { + animationState = true + delay(2000) + // navController.navigate(Screen.Home.route) + } + + LaunchedEffect(key1 = userSession) { + delay(2000) + + if (userSession != null) { + if (userSession!!.userLoginToken.accessToken == "") { + navController.navigate(Screen.OnBoarding.route) + } else { + navController.navigate("${Screen.Main.route}/0") + } + } + } + + SplashContent(alpha = alphaAnim.value) +} + +@Composable +fun SplashContent(alpha: Float) { + Box( + modifier = Modifier + .fillMaxSize() + .alpha(alpha) + .background(color = Green77), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.img_logo_onboarding_nooutline), + contentDescription = null, + modifier = Modifier + .width(57.27.dp) + .height(63.9.dp) + ) + Text( + text = "Techwaste", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier + .padding(start = 5.24.dp) + ) + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + Text( + text = "Copyright © 2023", + color= Color.White, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .padding(bottom = 32.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SplashScreenPreview() { + TechwasMark02Theme { + SplashContent(alpha = 1f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreenViewModel.kt b/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreenViewModel.kt new file mode 100644 index 0000000..cecc022 --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/ui/screen/splashScreen/SplashScreenViewModel.kt @@ -0,0 +1,41 @@ +package com.capstone.techwasmark02.ui.screen.splashScreen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.capstone.techwasmark02.common.Resource +import com.capstone.techwasmark02.data.model.UserSession +import com.capstone.techwasmark02.data.remote.response.Token +import com.capstone.techwasmark02.data.remote.response.UserId +import com.capstone.techwasmark02.repository.PreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SplashScreenViewModel @Inject constructor( + private val preferencesRepository: PreferencesRepository +): ViewModel() { + + private val _userSessionState: MutableStateFlow = MutableStateFlow(null) + val userSessionState = _userSessionState.asStateFlow() + + init { + viewModelScope.launch { + val result = preferencesRepository.getActiveSession() + when(result) { + is Resource.Error -> { + _userSessionState.value = UserSession( + userLoginToken = Token(accessToken = ""), + userNameId = UserId(username = "", id = 0) + ) + } + is Resource.Success -> { + _userSessionState.value = result.data + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Color.kt b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Color.kt index 8678a3f..a569a46 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Color.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Color.kt @@ -3,10 +3,17 @@ package com.capstone.techwasmark02.ui.theme import androidx.compose.ui.graphics.Color val Green35 = Color(0xff5bb915) +val Green77 = Color(0xFF54B800) val Black20 = Color(0xff323232) val Black12 = Color(0xff1F1F1F) val Lime56 = Color(0xff9ED54A) -val Mist97 = Color(0xffF1EEFF) \ No newline at end of file +val Mist97 = Color(0xffF1EEFF) +val purple = Color(0xff8385E4) +val Yellow77 = Color(0xFFFFDF5D) +val sakura = Color(0xFFF5A37B) +val gray = Color(0xFFD9D9D9) +val yellow = Color(0xFFFEBC1F) +val red = Color(0xFFCD230F) \ No newline at end of file diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Theme.kt b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Theme.kt index 0f65185..f75438b 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Theme.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Theme.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme @@ -34,7 +33,7 @@ private val LightColorScheme = lightColorScheme( inverseSurface = Black12, inverseOnSurface = Color.White, - background = Mist97, + background = Color.White, onBackground = Black20 ) diff --git a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Type.kt b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Type.kt index 3ece5d6..39d1932 100644 --- a/app/src/main/java/com/capstone/techwasmark02/ui/theme/Type.kt +++ b/app/src/main/java/com/capstone/techwasmark02/ui/theme/Type.kt @@ -21,6 +21,13 @@ val poppins = FontFamily( // Set of Material typography styles to start with val Typography = Typography( + headlineMedium = TextStyle( + fontFamily = poppins, + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 42.sp, + letterSpacing = 0.1.sp + ), headlineSmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Bold, @@ -28,13 +35,27 @@ val Typography = Typography( lineHeight = 32.sp, letterSpacing = 0.sp ), - titleSmall = TextStyle( + titleLarge = TextStyle( + fontFamily = poppins, + fontWeight = FontWeight.Medium, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + titleMedium = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Bold, fontSize = 20.sp, - lineHeight = 14.sp, + lineHeight = 24.sp, letterSpacing = 0.1.sp ), + titleSmall = TextStyle( + fontFamily = poppins, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), labelLarge = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Bold, diff --git a/app/src/main/java/com/capstone/techwasmark02/utils/html/FromHtml.kt b/app/src/main/java/com/capstone/techwasmark02/utils/html/FromHtml.kt new file mode 100644 index 0000000..ae853fd --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/utils/html/FromHtml.kt @@ -0,0 +1,43 @@ +package com.capstone.techwasmark02.utils.html + +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.text.Spannable +import android.text.TextPaint +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan +import androidx.core.content.res.ResourcesCompat +import androidx.core.text.HtmlCompat +import com.capstone.techwasmark02.R + +fun fromHtml(context: Context, html: String): Spannable = parse(html).apply { + removeLinksUnderline() + styleBold(context) +} + +private fun parse(html: String): Spannable = + (HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) as Spannable) + +private fun Spannable.removeLinksUnderline() { + for (s in getSpans(0, length, URLSpan::class.java)) { + setSpan(object : UnderlineSpan() { + override fun updateDrawState(tp: TextPaint) { + tp.isUnderlineText = false + } + }, getSpanStart(s), getSpanEnd(s), 0) + } +} + +private fun Spannable.styleBold(context: Context) { + val bold = ResourcesCompat.getFont(context, R.font.poppins_semi_bold)!! + for (s in getSpans(0, length, StyleSpan::class.java)) { + if (s.style == Typeface.BOLD) { + setSpan(ForegroundColorSpan(Color.BLACK), getSpanStart(s), getSpanEnd(s), 0) + setSpan(bold.getTypefaceSpan(), getSpanStart(s), getSpanEnd(s), 0) + } + } +} + diff --git a/app/src/main/java/com/capstone/techwasmark02/utils/html/TypeFaceSpanCompat.kt b/app/src/main/java/com/capstone/techwasmark02/utils/html/TypeFaceSpanCompat.kt new file mode 100644 index 0000000..8c6f25c --- /dev/null +++ b/app/src/main/java/com/capstone/techwasmark02/utils/html/TypeFaceSpanCompat.kt @@ -0,0 +1,29 @@ +package com.capstone.techwasmark02.utils.html + +import android.annotation.TargetApi +import android.graphics.Typeface +import android.os.Build +import android.text.TextPaint +import android.text.style.MetricAffectingSpan +import android.text.style.TypefaceSpan + +fun Typeface.getTypefaceSpan(): MetricAffectingSpan = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + typefaceSpanCompatV28(this) + } else { + CustomTypefaceSpan(this) + } + +@TargetApi(Build.VERSION_CODES.P) +private fun typefaceSpanCompatV28(typeface: Typeface) = TypefaceSpan(typeface) + +private class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() { + + override fun updateDrawState(paint: TextPaint) { + paint.typeface = typeface + } + + override fun updateMeasureState(paint: TextPaint) { + paint.typeface = typeface + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/forum_box.webp b/app/src/main/res/drawable/forum_box.webp new file mode 100644 index 0000000..369fde8 Binary files /dev/null and b/app/src/main/res/drawable/forum_box.webp differ diff --git a/app/src/main/res/drawable/forum_chat.png b/app/src/main/res/drawable/forum_chat.png new file mode 100644 index 0000000..85ac970 Binary files /dev/null and b/app/src/main/res/drawable/forum_chat.png differ diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000..e55596d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_left.xml b/app/src/main/res/drawable/ic_arrow_left.xml new file mode 100644 index 0000000..7d12a80 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_left.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..e011dbc --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000..0b0eae3 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_camera_fill.xml b/app/src/main/res/drawable/ic_camera_fill.xml new file mode 100644 index 0000000..43a4fd5 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera_fill.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_center_focus.xml b/app/src/main/res/drawable/ic_center_focus.xml new file mode 100644 index 0000000..e215d44 --- /dev/null +++ b/app/src/main/res/drawable/ic_center_focus.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chat_buble.xml b/app/src/main/res/drawable/ic_chat_buble.xml new file mode 100644 index 0000000..3cf25bb --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_buble.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_create.xml b/app/src/main/res/drawable/ic_create.xml new file mode 100644 index 0000000..0dfda73 --- /dev/null +++ b/app/src/main/res/drawable/ic_create.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fill_notification.xml b/app/src/main/res/drawable/ic_fill_notification.xml new file mode 100644 index 0000000..1d038a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_fill_notification.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_gallery_big.xml b/app/src/main/res/drawable/ic_gallery_big.xml new file mode 100644 index 0000000..6b87b62 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery_big.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml new file mode 100644 index 0000000..a5061ae --- /dev/null +++ b/app/src/main/res/drawable/ic_language.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..ca3826a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_location_on.xml b/app/src/main/res/drawable/ic_location_on.xml new file mode 100644 index 0000000..dd12113 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..bceb2ec --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_book.xml b/app/src/main/res/drawable/ic_menu_book.xml new file mode 100644 index 0000000..21e9852 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_book.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_my_location.xml b/app/src/main/res/drawable/ic_my_location.xml new file mode 100644 index 0000000..11fdbe5 --- /dev/null +++ b/app/src/main/res/drawable/ic_my_location.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_question_mark.xml b/app/src/main/res/drawable/ic_question_mark.xml new file mode 100644 index 0000000..1d4bf08 --- /dev/null +++ b/app/src/main/res/drawable/ic_question_mark.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..a5687c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sell.xml b/app/src/main/res/drawable/ic_sell.xml new file mode 100644 index 0000000..2b8e90e --- /dev/null +++ b/app/src/main/res/drawable/ic_sell.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..298a5a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml new file mode 100644 index 0000000..e49a1f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_trash_outline.xml b/app/src/main/res/drawable/ic_trash_outline.xml new file mode 100644 index 0000000..d18155d --- /dev/null +++ b/app/src/main/res/drawable/ic_trash_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/img_bg_catalog_card.png b/app/src/main/res/drawable/img_bg_catalog_card.png new file mode 100644 index 0000000..c47d450 Binary files /dev/null and b/app/src/main/res/drawable/img_bg_catalog_card.png differ diff --git a/app/src/main/res/drawable/img_bg_green.png b/app/src/main/res/drawable/img_bg_green.png new file mode 100644 index 0000000..a3d4711 Binary files /dev/null and b/app/src/main/res/drawable/img_bg_green.png differ diff --git a/app/src/main/res/drawable/img_bg_green_large.webp b/app/src/main/res/drawable/img_bg_green_large.webp new file mode 100644 index 0000000..b6084ed Binary files /dev/null and b/app/src/main/res/drawable/img_bg_green_large.webp differ diff --git a/app/src/main/res/drawable/img_bg_purple.png b/app/src/main/res/drawable/img_bg_purple.png new file mode 100644 index 0000000..b01cacb Binary files /dev/null and b/app/src/main/res/drawable/img_bg_purple.png differ diff --git a/app/src/main/res/drawable/img_bg_sakura.png b/app/src/main/res/drawable/img_bg_sakura.png new file mode 100644 index 0000000..b016ffd Binary files /dev/null and b/app/src/main/res/drawable/img_bg_sakura.png differ diff --git a/app/src/main/res/drawable/img_bg_signup.webp b/app/src/main/res/drawable/img_bg_signup.webp new file mode 100644 index 0000000..3a87dcf Binary files /dev/null and b/app/src/main/res/drawable/img_bg_signup.webp differ diff --git a/app/src/main/res/drawable/img_bg_singin.webp b/app/src/main/res/drawable/img_bg_singin.webp new file mode 100644 index 0000000..afb9b7f Binary files /dev/null and b/app/src/main/res/drawable/img_bg_singin.webp differ diff --git a/app/src/main/res/drawable/img_feature_article_box.webp b/app/src/main/res/drawable/img_feature_article_box.webp new file mode 100644 index 0000000..af75c0a Binary files /dev/null and b/app/src/main/res/drawable/img_feature_article_box.webp differ diff --git a/app/src/main/res/drawable/img_feature_article_illustration.webp b/app/src/main/res/drawable/img_feature_article_illustration.webp new file mode 100644 index 0000000..b07b08b Binary files /dev/null and b/app/src/main/res/drawable/img_feature_article_illustration.webp differ diff --git a/app/src/main/res/drawable/img_feature_detect_box.webp b/app/src/main/res/drawable/img_feature_detect_box.webp new file mode 100644 index 0000000..bd102c4 Binary files /dev/null and b/app/src/main/res/drawable/img_feature_detect_box.webp differ diff --git a/app/src/main/res/drawable/img_feature_detect_frame.webp b/app/src/main/res/drawable/img_feature_detect_frame.webp new file mode 100644 index 0000000..bb89bc1 Binary files /dev/null and b/app/src/main/res/drawable/img_feature_detect_frame.webp differ diff --git a/app/src/main/res/drawable/img_feature_detect_illustration.webp b/app/src/main/res/drawable/img_feature_detect_illustration.webp new file mode 100644 index 0000000..1bd9831 Binary files /dev/null and b/app/src/main/res/drawable/img_feature_detect_illustration.webp differ diff --git a/app/src/main/res/drawable/img_feature_forum_box.webp b/app/src/main/res/drawable/img_feature_forum_box.webp new file mode 100644 index 0000000..369fde8 Binary files /dev/null and b/app/src/main/res/drawable/img_feature_forum_box.webp differ diff --git a/app/src/main/res/drawable/img_feature_forum_illustration.webp b/app/src/main/res/drawable/img_feature_forum_illustration.webp new file mode 100644 index 0000000..7fd2d63 Binary files /dev/null and b/app/src/main/res/drawable/img_feature_forum_illustration.webp differ diff --git a/app/src/main/res/drawable/img_forum_hp_nyala.webp b/app/src/main/res/drawable/img_forum_hp_nyala.webp new file mode 100644 index 0000000..85358aa Binary files /dev/null and b/app/src/main/res/drawable/img_forum_hp_nyala.webp differ diff --git a/app/src/main/res/drawable/img_forum_laptop_bekas.webp b/app/src/main/res/drawable/img_forum_laptop_bekas.webp new file mode 100644 index 0000000..df8e4f7 Binary files /dev/null and b/app/src/main/res/drawable/img_forum_laptop_bekas.webp differ diff --git a/app/src/main/res/drawable/img_logo_onboarding.webp b/app/src/main/res/drawable/img_logo_onboarding.webp new file mode 100644 index 0000000..51c97d5 Binary files /dev/null and b/app/src/main/res/drawable/img_logo_onboarding.webp differ diff --git a/app/src/main/res/drawable/img_logo_onboarding_nooutline.webp b/app/src/main/res/drawable/img_logo_onboarding_nooutline.webp new file mode 100644 index 0000000..86f926e Binary files /dev/null and b/app/src/main/res/drawable/img_logo_onboarding_nooutline.webp differ diff --git a/app/src/main/res/drawable/img_logo_onboarding_nooutline_green.webp b/app/src/main/res/drawable/img_logo_onboarding_nooutline_green.webp new file mode 100644 index 0000000..3c1036b Binary files /dev/null and b/app/src/main/res/drawable/img_logo_onboarding_nooutline_green.webp differ diff --git a/app/src/main/res/drawable/img_onboarding_ripple_peach_left.webp b/app/src/main/res/drawable/img_onboarding_ripple_peach_left.webp new file mode 100644 index 0000000..27433c9 Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_ripple_peach_left.webp differ diff --git a/app/src/main/res/drawable/img_onboarding_ripple_peach_right.webp b/app/src/main/res/drawable/img_onboarding_ripple_peach_right.webp new file mode 100644 index 0000000..ff148c9 Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_ripple_peach_right.webp differ diff --git a/app/src/main/res/drawable/img_onboarding_ripple_purple_left.webp b/app/src/main/res/drawable/img_onboarding_ripple_purple_left.webp new file mode 100644 index 0000000..819fddd Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_ripple_purple_left.webp differ diff --git a/app/src/main/res/drawable/img_onboarding_ripple_purple_right.webp b/app/src/main/res/drawable/img_onboarding_ripple_purple_right.webp new file mode 100644 index 0000000..e76bb4b Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_ripple_purple_right.webp differ diff --git a/app/src/main/res/drawable/img_profile_ripple_green.webp b/app/src/main/res/drawable/img_profile_ripple_green.webp new file mode 100644 index 0000000..6c03f03 Binary files /dev/null and b/app/src/main/res/drawable/img_profile_ripple_green.webp differ diff --git a/app/src/main/res/drawable/img_profile_ripple_green_reverse.webp b/app/src/main/res/drawable/img_profile_ripple_green_reverse.webp new file mode 100644 index 0000000..0e7dcd3 Binary files /dev/null and b/app/src/main/res/drawable/img_profile_ripple_green_reverse.webp differ diff --git a/app/src/main/res/drawable/img_user_1.webp b/app/src/main/res/drawable/img_user_1.webp new file mode 100644 index 0000000..6cec2ad Binary files /dev/null and b/app/src/main/res/drawable/img_user_1.webp differ diff --git a/app/src/main/res/drawable/img_user_2.webp b/app/src/main/res/drawable/img_user_2.webp new file mode 100644 index 0000000..2258acd Binary files /dev/null and b/app/src/main/res/drawable/img_user_2.webp differ diff --git a/app/src/main/res/drawable/img_user_3.webp b/app/src/main/res/drawable/img_user_3.webp new file mode 100644 index 0000000..a5f1877 Binary files /dev/null and b/app/src/main/res/drawable/img_user_3.webp differ diff --git a/app/src/main/res/drawable/img_user_4.webp b/app/src/main/res/drawable/img_user_4.webp new file mode 100644 index 0000000..d9dc169 Binary files /dev/null and b/app/src/main/res/drawable/img_user_4.webp differ diff --git a/app/src/main/res/drawable/logo_techwase.png b/app/src/main/res/drawable/logo_techwase.png new file mode 100644 index 0000000..7e42bd9 Binary files /dev/null and b/app/src/main/res/drawable/logo_techwase.png differ diff --git a/app/src/main/res/drawable/logo_techwaste.png b/app/src/main/res/drawable/logo_techwaste.png new file mode 100644 index 0000000..ff899fe Binary files /dev/null and b/app/src/main/res/drawable/logo_techwaste.png differ diff --git a/app/src/main/res/drawable/trash_bucket.png b/app/src/main/res/drawable/trash_bucket.png new file mode 100644 index 0000000..d75895d Binary files /dev/null and b/app/src/main/res/drawable/trash_bucket.png differ 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 6f3b755..ac2225b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,7 @@ - - - + + + + \ 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 6f3b755..ac2225b 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,6 +1,7 @@ - - - + + + + \ 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..4b41879 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_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ec560b0 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png 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..427b360 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..860be82 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_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1289872 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png 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..44cf692 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..d86a7cd 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_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9fce785 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png 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..373dc13 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..7b8ac6e 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_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..86b7665 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png 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..596144b 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..c03c9f3 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_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b40c936 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png 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..b5f51ff 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/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ 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 42d30f5..1826127 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ + - Techwas Mark02 + Techwaste \ No newline at end of file diff --git a/build.gradle b/build.gradle index 92d9f45..928c336 100644 --- a/build.gradle +++ b/build.gradle @@ -4,4 +4,5 @@ plugins { id 'com.android.library' version '8.0.1' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false -} \ No newline at end of file + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false +}