diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eae278a..33f8152 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) id("com.google.gms.google-services") + alias(libs.plugins.ksp) } android { @@ -49,6 +50,11 @@ android { dependencies { + //Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + ksp(libs.room.compiler) + //DataStore implementation(libs.data.store) diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt index 405f969..c284783 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSource.kt @@ -4,7 +4,10 @@ import kotlinx.coroutines.flow.Flow interface AuthLocalDataSource { fun getUID(): Flow + fun getUserName(): Flow suspend fun saveUID(uid: String) + suspend fun saveUserName(name: String) + suspend fun clearUID() } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt index 730dedf..f74999d 100644 --- a/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/local/dataStore/AuthLocalDataSourceImpl.kt @@ -14,6 +14,7 @@ class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSourc private object PreferencesKeys { val UID_KEY = stringPreferencesKey("uid") + val NAME_KEY = stringPreferencesKey("name") } override fun getUID(): Flow = @@ -21,12 +22,25 @@ class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSourc prefs[PreferencesKeys.UID_KEY] } + override fun getUserName(): Flow = + context.dataStore.data.map { prefs -> + prefs[PreferencesKeys.NAME_KEY] + } + override suspend fun saveUID(uid: String) { Log.i("AuthLocalDataSourceImpl", "saveUser: $uid") context.dataStore.edit { prefs -> prefs[PreferencesKeys.UID_KEY] = uid } + + } + + override suspend fun saveUserName(userName: String) { + Log.i("AuthLocalDataSourceImpl", "saveName: $userName") + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.NAME_KEY] = userName + } } override suspend fun clearUID() { diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/AppDatabase.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/AppDatabase.kt new file mode 100644 index 0000000..d7f918d --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/AppDatabase.kt @@ -0,0 +1,14 @@ +package com.delecrode.devhub.data.local.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.delecrode.devhub.data.local.database.dao.RepoDao +import com.delecrode.devhub.data.model.entity.RepoEntity + +@Database( + entities = [RepoEntity::class], + version = 1 +) +abstract class AppDatabase : RoomDatabase() { + abstract fun repoDao(): RepoDao +} diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt new file mode 100644 index 0000000..9ca6795 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt @@ -0,0 +1,19 @@ +package com.delecrode.devhub.data.local.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.delecrode.devhub.data.model.entity.RepoEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface RepoDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(repo: RepoEntity) + + @Query("SELECT * FROM repositories") + fun getAll(): Flow> + +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt new file mode 100644 index 0000000..841baab --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt @@ -0,0 +1,11 @@ +package com.delecrode.devhub.data.local.database.data + +import com.delecrode.devhub.data.model.entity.RepoEntity +import kotlinx.coroutines.flow.Flow + +interface RepoLocalDataSource { + + suspend fun save(repo: RepoEntity) + + fun getAll(): Flow> +} diff --git a/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt new file mode 100644 index 0000000..9d8e8e4 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt @@ -0,0 +1,17 @@ +package com.delecrode.devhub.data.local.database.data + +import com.delecrode.devhub.data.local.database.dao.RepoDao +import com.delecrode.devhub.data.model.entity.RepoEntity +import kotlinx.coroutines.flow.Flow + +class RepoLocalDataSourceImpl( + private val dao: RepoDao +) : RepoLocalDataSource { + + override suspend fun save(repo: RepoEntity) { + dao.insert(repo) + } + + override fun getAll(): Flow> = + dao.getAll() +} diff --git a/app/src/main/java/com/delecrode/devhub/data/mapper/ReposMapper.kt b/app/src/main/java/com/delecrode/devhub/data/mapper/ReposMapper.kt index 40feef9..de7c061 100644 --- a/app/src/main/java/com/delecrode/devhub/data/mapper/ReposMapper.kt +++ b/app/src/main/java/com/delecrode/devhub/data/mapper/ReposMapper.kt @@ -1,11 +1,34 @@ package com.delecrode.devhub.data.mapper -import com.delecrode.devhub.data.model.LanguagesDto -import com.delecrode.devhub.data.model.RepoDetailDto -import com.delecrode.devhub.data.model.ReposDto +import com.delecrode.devhub.data.model.dto.LanguagesDto +import com.delecrode.devhub.data.model.dto.RepoDetailDto +import com.delecrode.devhub.data.model.dto.ReposDto +import com.delecrode.devhub.data.model.entity.RepoEntity import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail +import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.model.Repos + + +//Repositorios favoritos +fun RepoFav.toEntity(): RepoEntity = + RepoEntity( + id = id, + name = name, + userName = userName, + description = description, + url = url + ) + +fun RepoEntity.toDomain(): RepoFav = + RepoFav( + id = id, + name = name, + userName = userName, + description = description, + url = url + ) + fun LanguagesDto.toLanguagesDomain(): Languages { return Languages( languages = this.keys.toList() @@ -13,7 +36,6 @@ fun LanguagesDto.toLanguagesDomain(): Languages { } - fun ReposDto.toReposDomain(): Repos { return Repos( id = id, @@ -32,6 +54,7 @@ fun ReposDto.toReposDomain(): Repos { fun RepoDetailDto.toRepoDetailDomain(): RepoDetail { return RepoDetail( + id = id, name = name, html_url = html_url, description = description ?: "", diff --git a/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt b/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt index b0647e6..101e968 100644 --- a/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt +++ b/app/src/main/java/com/delecrode/devhub/data/mapper/UserMapper.kt @@ -1,7 +1,7 @@ package com.delecrode.devhub.data.mapper -import com.delecrode.devhub.data.model.UserForFirebaseDto -import com.delecrode.devhub.data.model.UserForGitDto +import com.delecrode.devhub.data.model.dto.UserForFirebaseDto +import com.delecrode.devhub.data.model.dto.UserForGitDto import com.delecrode.devhub.domain.model.UserForFirebase import com.delecrode.devhub.domain.model.UserForGit diff --git a/app/src/main/java/com/delecrode/devhub/data/model/ReposDto.kt b/app/src/main/java/com/delecrode/devhub/data/model/dto/ReposDto.kt similarity index 92% rename from app/src/main/java/com/delecrode/devhub/data/model/ReposDto.kt rename to app/src/main/java/com/delecrode/devhub/data/model/dto/ReposDto.kt index b9e347c..c1ab04d 100644 --- a/app/src/main/java/com/delecrode/devhub/data/model/ReposDto.kt +++ b/app/src/main/java/com/delecrode/devhub/data/model/dto/ReposDto.kt @@ -1,4 +1,4 @@ -package com.delecrode.devhub.data.model +package com.delecrode.devhub.data.model.dto data class ReposDto( val id: Int, @@ -16,6 +16,7 @@ data class ReposDto( data class RepoDetailDto( + val id: Int, val name: String, val html_url: String, val description: String?, diff --git a/app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt b/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt similarity index 96% rename from app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt rename to app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt index b3628e1..16327c6 100644 --- a/app/src/main/java/com/delecrode/devhub/data/model/UserDto.kt +++ b/app/src/main/java/com/delecrode/devhub/data/model/dto/UserDto.kt @@ -1,4 +1,4 @@ -package com.delecrode.devhub.data.model +package com.delecrode.devhub.data.model.dto data class UserForGitDto( val login: String ?, diff --git a/app/src/main/java/com/delecrode/devhub/data/model/entity/Repo.kt b/app/src/main/java/com/delecrode/devhub/data/model/entity/Repo.kt new file mode 100644 index 0000000..5f8a52d --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/data/model/entity/Repo.kt @@ -0,0 +1,16 @@ +package com.delecrode.devhub.data.model.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "repositories") +data class RepoEntity( + @PrimaryKey(autoGenerate = true) + val idLocal: Long = 0, + val id: Int, + val name: String, + val userName: String, + val description: String, + val url: String, +) diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt index dcfee62..8557672 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/RepoApiService.kt @@ -1,7 +1,7 @@ package com.delecrode.devhub.data.remote.webApi.service -import com.delecrode.devhub.data.model.LanguagesDto -import com.delecrode.devhub.data.model.RepoDetailDto +import com.delecrode.devhub.data.model.dto.LanguagesDto +import com.delecrode.devhub.data.model.dto.RepoDetailDto import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path diff --git a/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt index 213e2ca..00763c4 100644 --- a/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt +++ b/app/src/main/java/com/delecrode/devhub/data/remote/webApi/service/UserApiService.kt @@ -1,7 +1,7 @@ package com.delecrode.devhub.data.remote.webApi.service -import com.delecrode.devhub.data.model.ReposDto -import com.delecrode.devhub.data.model.UserForGitDto +import com.delecrode.devhub.data.model.dto.ReposDto +import com.delecrode.devhub.data.model.dto.UserForGitDto import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path diff --git a/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt b/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt index 43ced05..95c5b37 100644 --- a/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/repository/RepoRepositoryImpl.kt @@ -1,13 +1,31 @@ package com.delecrode.devhub.data.repository +import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource +import com.delecrode.devhub.data.local.database.data.RepoLocalDataSource +import com.delecrode.devhub.data.mapper.toDomain +import com.delecrode.devhub.data.mapper.toEntity import com.delecrode.devhub.data.mapper.toLanguagesDomain import com.delecrode.devhub.data.mapper.toRepoDetailDomain import com.delecrode.devhub.data.remote.webApi.service.RepoApiService import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail +import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.repository.RepoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map -class RepoRepositoryImpl(val repoApi: RepoApiService) : RepoRepository { +class RepoRepositoryImpl(val repoApi: RepoApiService, private val localDataSource: RepoLocalDataSource, private val authLocalDataSource: AuthLocalDataSource) : RepoRepository { + + override suspend fun save(repo: RepoFav) { + localDataSource.save(repo.toEntity()) + } + + override fun getAll(): Flow> = + localDataSource.getAll() + .map { list -> list.map { it.toDomain() } } + + override fun getUserName(): Flow = + authLocalDataSource.getUserName() override suspend fun getRepoDetail(owner: String, repo: String): RepoDetail { try { diff --git a/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt index 88abdfa..aeb7139 100644 --- a/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/delecrode/devhub/data/repository/UserRepositoryImpl.kt @@ -4,7 +4,7 @@ import android.util.Log import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource import com.delecrode.devhub.data.mapper.toReposDomain import com.delecrode.devhub.data.mapper.toUserDomain -import com.delecrode.devhub.data.model.UserForFirebaseDto +import com.delecrode.devhub.data.model.dto.UserForFirebaseDto import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.data.remote.webApi.service.UserApiService import com.delecrode.devhub.domain.model.Repos @@ -13,7 +13,11 @@ import com.delecrode.devhub.domain.model.UserForGit import com.delecrode.devhub.domain.repository.UserRepository import kotlinx.coroutines.flow.first -class UserRepositoryImpl(private val userApi: UserApiService, private val userExtraData: UserExtraData, private val authLocalDataSource: AuthLocalDataSource) : UserRepository { +class UserRepositoryImpl( + private val userApi: UserApiService, + private val userExtraData: UserExtraData, + private val authLocalDataSource: AuthLocalDataSource +) : UserRepository { override suspend fun getUserForGitHub(userName: String): UserForGit { try { @@ -38,23 +42,26 @@ class UserRepositoryImpl(private val userApi: UserApiService, private val userEx try { val uid = authLocalDataSource.getUID().first() Log.i("UserRepositoryImpl", "getUserForFirebase (UID Real): $uid") - if(uid != null){ + if (uid != null) { val response = userExtraData.getUser(uid) if (response.exists()) { val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() + val userName = body?.username + if (userName != null) { + authLocalDataSource.saveUserName(userName) + } if (body != null) { return body } else { throw Exception("Resposta vazia do servidor") } - }else{ + } else { throw Exception("Usuário não encontrado") } - } - else{ + } else { return UserForFirebase() } - }catch (e: Exception){ + } catch (e: Exception) { throw e } } diff --git a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt index 2801b91..cfd29d1 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -1,7 +1,11 @@ package com.delecrode.devhub.di +import androidx.room.Room import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSource import com.delecrode.devhub.data.local.dataStore.AuthLocalDataSourceImpl +import com.delecrode.devhub.data.local.database.AppDatabase +import com.delecrode.devhub.data.local.database.data.RepoLocalDataSource +import com.delecrode.devhub.data.local.database.data.RepoLocalDataSourceImpl import com.delecrode.devhub.data.remote.firebase.UserExtraData import com.delecrode.devhub.data.remote.webApi.instance.RetrofitInstance import com.delecrode.devhub.data.repository.AuthRepositoryImpl @@ -11,12 +15,15 @@ import com.delecrode.devhub.domain.repository.AuthRepository import com.delecrode.devhub.domain.repository.RepoRepository import com.delecrode.devhub.domain.repository.UserRepository import com.delecrode.devhub.domain.session.SessionViewModel +import com.delecrode.devhub.ui.favoritos.RepoFavViewModel import com.delecrode.devhub.ui.home.HomeViewModel import com.delecrode.devhub.ui.login.AuthViewModel +import com.delecrode.devhub.ui.profile.ProfileViewModel import com.delecrode.devhub.ui.register.RegisterViewModel import com.delecrode.devhub.ui.repo.RepoDetailViewModel import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -29,19 +36,34 @@ val appModule = module { single { RetrofitInstance.userApi } single { RetrofitInstance.repoApi } + single { + Room.databaseBuilder( + androidContext(), + AppDatabase::class.java, + "app_database" + ).build() + } + + single { + get().repoDao() + } + single { AuthLocalDataSourceImpl(get()) } + single { RepoLocalDataSourceImpl(get()) } single { com.delecrode.devhub.data.remote.firebase.FirebaseAuth(get()) } single { UserExtraData(get()) } single { UserRepositoryImpl(get(), get(), get()) } - single { RepoRepositoryImpl(get()) } + single { RepoRepositoryImpl(get(),get(),get()) } single { AuthRepositoryImpl(get(), get(), get()) } - viewModel{ HomeViewModel(get(), get()) } + viewModel { HomeViewModel(get(), get()) } viewModel { RepoDetailViewModel(get()) } viewModel { AuthViewModel(get()) } viewModel { SessionViewModel(get()) } viewModel { RegisterViewModel(get()) } + viewModel { ProfileViewModel(get(),get()) } + viewModel { RepoFavViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt b/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt index 7421331..fe6441f 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/model/RepoDetail.kt @@ -1,6 +1,7 @@ package com.delecrode.devhub.domain.model data class RepoDetail( + val id: Int, val name: String, val html_url: String, val description: String?, diff --git a/app/src/main/java/com/delecrode/devhub/domain/model/Repos.kt b/app/src/main/java/com/delecrode/devhub/domain/model/Repos.kt index e45a6d6..588387a 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/model/Repos.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/model/Repos.kt @@ -13,3 +13,11 @@ data class Repos( val pushed_at: String, val clone_url: String ) + +data class RepoFav( + val id: Int, + val name: String, + val userName: String, + val description: String, + val url: String, +) diff --git a/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt b/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt index 72adbf9..c2d510c 100644 --- a/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt +++ b/app/src/main/java/com/delecrode/devhub/domain/repository/RepoRepository.kt @@ -2,6 +2,8 @@ package com.delecrode.devhub.domain.repository import com.delecrode.devhub.domain.model.Languages import com.delecrode.devhub.domain.model.RepoDetail +import com.delecrode.devhub.domain.model.RepoFav +import kotlinx.coroutines.flow.Flow interface RepoRepository { @@ -9,4 +11,8 @@ interface RepoRepository { suspend fun getLanguagesRepo(owner: String, repo: String): Languages + suspend fun save(repo: RepoFav) + fun getAll(): Flow> + fun getUserName(): Flow + } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt index 127b1ba..8cbbe49 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt @@ -12,4 +12,9 @@ sealed class AppDestinations(val route: String) { object Register: AppDestinations("register") object Login: AppDestinations("login") object ForgotPassword: AppDestinations("forgotPassword") + + object Profile: AppDestinations("profile") + object ReposFav: AppDestinations("reposFav") + + } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt index 6dad5f0..f81f9ee 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -8,11 +8,15 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.delecrode.devhub.domain.session.SessionViewModel +import com.delecrode.devhub.ui.favoritos.RepoFavViewModel +import com.delecrode.devhub.ui.favoritos.ReposFavScreen import com.delecrode.devhub.ui.forgot.ForgotPasswordScreen import com.delecrode.devhub.ui.home.HomeScreen import com.delecrode.devhub.ui.home.HomeViewModel import com.delecrode.devhub.ui.login.AuthViewModel import com.delecrode.devhub.ui.login.LoginScreen +import com.delecrode.devhub.ui.profile.ProfileScreen +import com.delecrode.devhub.ui.profile.ProfileViewModel import com.delecrode.devhub.ui.register.RegisterScreen import com.delecrode.devhub.ui.register.RegisterViewModel import com.delecrode.devhub.ui.repo.RepoDetailScreen @@ -24,22 +28,25 @@ import org.koin.androidx.compose.koinViewModel fun AppNavHost(sessionViewModel: SessionViewModel) { val navController = rememberNavController() - val profileViewModel: HomeViewModel = koinViewModel() + val homeViewModel: HomeViewModel = koinViewModel() val repoViewModel: RepoDetailViewModel = koinViewModel() val authViewModel: AuthViewModel = koinViewModel() val registerViewModel: RegisterViewModel = koinViewModel() + val profileViewModel: ProfileViewModel = koinViewModel() + val repoFavViewModel: RepoFavViewModel = koinViewModel() val logged = sessionViewModel.isLoggedIn.collectAsState() + NavHost( navController = navController, startDestination = if (logged.value) AppDestinations.Home.route else AppDestinations.Login.route ) { //Home Flow - composable(AppDestinations.Home.route){ - HomeScreen(navController, profileViewModel) + composable(AppDestinations.Home.route) { + HomeScreen(navController, homeViewModel) } //Repositorio Flow @@ -70,5 +77,12 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { ForgotPasswordScreen(navController) } + composable(AppDestinations.Profile.route) { + ProfileScreen(navController, profileViewModel) + } + + composable(AppDestinations.ReposFav.route) { + ReposFavScreen(navController, repoFavViewModel) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt new file mode 100644 index 0000000..334e811 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt @@ -0,0 +1,9 @@ +package com.delecrode.devhub.ui.favoritos + +import com.delecrode.devhub.domain.model.RepoFav + +data class RepoFavState( + val isLoading: Boolean = false, + val repoFav: List = emptyList(), + val error: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavViewModel.kt new file mode 100644 index 0000000..cf4cb41 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavViewModel.kt @@ -0,0 +1,40 @@ +package com.delecrode.devhub.ui.favoritos + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.delecrode.devhub.domain.repository.RepoRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class RepoFavViewModel(private val repoRepository: RepoRepository): ViewModel(){ + + private val _uiState = MutableStateFlow(RepoFavState()) + val uiState: StateFlow = _uiState + + init { + getAllRepoFav() + } + + fun getAllRepoFav(){ + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + error = null + ) + try { + repoRepository.getAll().collect { repos -> + _uiState.value = _uiState.value.copy( + repoFav = repos, + isLoading = false + ) + } + }catch (e: Exception){ + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt new file mode 100644 index 0000000..276e41e --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt @@ -0,0 +1,138 @@ +package com.delecrode.devhub.ui.favoritos + +import androidx.compose.foundation.background +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.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +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.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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import com.delecrode.devhub.navigation.AppDestinations + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReposFavScreen(navController: NavController, viewModel: RepoFavViewModel) { + + val state by viewModel.uiState.collectAsState() + val repos = state.repoFav + + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { Text("Meu Repositorios Favoritos") }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Voltar" + ) + } + }, + ) + } + + ) + { padding -> + Column(modifier = Modifier.padding(padding)) { + + Text( + "Repositórios Favoritos", fontWeight = FontWeight.Bold, + fontSize = 28.sp, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(state.repoFav.size) { index -> + val repo = repos[index] + Card( + modifier = Modifier.padding(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + onClick = { + navController.navigate( + AppDestinations.RepoDetail.createRoute( + state.repoFav[index].userName, + repo.name + ) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.LightGray) + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = repo.name) + } + + if (repo.description != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = repo.description) + } + + } + } + } + + } + } + + if (state.isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(0.5f)), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .size(48.dp) + .zIndex(11f) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt index 617e247..1108783 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt @@ -1,2 +1,272 @@ package com.delecrode.devhub.ui.profile +import android.util.Log +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.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.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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.TopAppBarDefaults +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.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import coil.compose.AsyncImage +import com.delecrode.devhub.R +import com.delecrode.devhub.navigation.AppDestinations + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen(navController: NavController, viewModel: ProfileViewModel) { + + var expanded by remember { mutableStateOf(false) } + val context = LocalContext.current + + val state = viewModel.uiState.collectAsState() + + val user = state.value.userForGit + val userName = state.value.userForFirebase.username + val repos = state.value.repos + + Log.i("ProfileScreen", "ProfileScreen: $userName") + LaunchedEffect(Unit) { + viewModel.getUserForGit(userName) + viewModel.getRepos(userName) + } + + LaunchedEffect(state.value.error) { + if (state.value.error != null) { + Toast.makeText(context, state.value.error, Toast.LENGTH_SHORT).show() + viewModel.clearState() + } + } + + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { Text("Meu Perfil") }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Voltar" + ) + } + }, + actions = { + IconButton(onClick = { expanded = true }) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Menu" + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Favoritos") }, + onClick = { + expanded = false + navController.navigate(AppDestinations.ReposFav.route) + } + ) + DropdownMenuItem( + text = { Text("Sair") }, + onClick = { + expanded = false + viewModel.signOut() + navController.navigate("login") { + popUpTo(0) + } + } + ) + } + } + ) + } + + ) + { padding -> + + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Column( + modifier = Modifier + .padding(8.dp), + horizontalAlignment = CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Box( + modifier = Modifier + .background(Color.White, shape = CircleShape) + .wrapContentSize() + ) { + AsyncImage( + model = if (user?.avatar_url != null) user.avatar_url else R.drawable.git_logo, + contentDescription = "Foto de Perfil", + modifier = Modifier + .size(100.dp) + .clip(CircleShape) + ) + } + + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = CenterHorizontally + ) { + + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = user?.name ?: "", + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = user?.login ?: "", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = user?.bio ?: "", + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onBackground + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + if (user?.repos_url != null) { + Text( + "Repositórios", fontWeight = FontWeight.Bold, + fontSize = 28.sp, + color = MaterialTheme.colorScheme.onBackground + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(repos.size) { index -> + val repo = repos[index] + Card( + modifier = Modifier.padding(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + onClick = { + navController.navigate( + AppDestinations.RepoDetail.createRoute( + user?.login ?: "", + repo.name + ) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.LightGray) + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = repo.name) + } + + if (repo.description != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = repo.description) + } + + } + } + } + + } + } + + } + } + if (state.value.isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(0.5f)), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .size(48.dp) + .zIndex(11f) + ) + } + } +} + diff --git a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileState.kt b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileState.kt new file mode 100644 index 0000000..3d9cc51 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileState.kt @@ -0,0 +1,14 @@ +package com.delecrode.devhub.ui.profile + +import com.delecrode.devhub.domain.model.Repos +import com.delecrode.devhub.domain.model.UserForFirebase +import com.delecrode.devhub.domain.model.UserForGit + +data class ProfileState( + val isLoading: Boolean = false, + val userForFirebase: UserForFirebase = UserForFirebase(), + val userForGit: UserForGit? = null, + val repos: List = emptyList(), + val error: String? = null + +) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt new file mode 100644 index 0000000..69ee5a0 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt @@ -0,0 +1,108 @@ +package com.delecrode.devhub.ui.profile + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.delecrode.devhub.domain.model.Repos +import com.delecrode.devhub.domain.repository.AuthRepository +import com.delecrode.devhub.domain.repository.UserRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + + +class ProfileViewModel(private val userRepository: UserRepository, private val authRepository: AuthRepository) : ViewModel() { + + private val _uiState = MutableStateFlow(ProfileState()) + val uiState: StateFlow = _uiState + + init{ + getUserForFirebase() + } + + fun getUserForFirebase() { + _uiState.value = _uiState.value.copy( + isLoading = true + ) + viewModelScope.launch { + try { + val userForFirebase = userRepository.getUserForFirebase() + _uiState.value = _uiState.value.copy( + userForFirebase = userForFirebase, + isLoading = false + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } + } + } + + fun getUserForGit(userName: String) { + _uiState.value = _uiState.value.copy( + isLoading = true + ) + viewModelScope.launch { + try { + val userForGit = + userRepository.getUserForGitHub(userName) + _uiState.value = _uiState.value.copy( + userForGit = userForGit, + isLoading = false + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } + } + } + + fun getRepos(userName: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + error = null + ) + try { + val repos: List = userRepository.getRepos(userName) + _uiState.value = _uiState.value.copy( + repos = repos, + isLoading = false + ) + } catch (e: Exception) { + Log.e("HomeViewModel", "Erro ao buscar repositórios", e) + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } + } + } + + fun signOut() { + viewModelScope.launch { + try { + authRepository.signOut() + } catch (e: Exception) { + Log.e("ProfileViewModel", "Erro ao fazer logout", e) + _uiState.value = _uiState.value.copy( + error = e.message, + isLoading = false + ) + } + } + } + + fun clearState(){ + _uiState.value = _uiState.value.copy( + isLoading = false, + error = null + ) + } + +} + diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt index c673024..fa9da5c 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailScreen.kt @@ -17,6 +17,8 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material3.Button import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator @@ -31,6 +33,9 @@ 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 @@ -58,6 +63,8 @@ fun RepoDetailScreen( ) { Log.i("RepoDetailScreen", "RepoDetailScreen: $owner, $repo") + var isFavorite by remember { mutableStateOf(false) } + val state by viewModel.uiState.collectAsState() val context = LocalContext.current @@ -87,6 +94,32 @@ fun RepoDetailScreen( color = MaterialTheme.colorScheme.onBackground ) }, + actions = { + IconButton( + onClick = { + isFavorite = !isFavorite + viewModel.favoriteRepo( + id = state.repo?.id ?: 0, + name = state.repo?.name ?: "", + owner = owner, + description = state.repo?.description ?: "", + url = state.repo?.html_url ?: "" + ) + } + ) { + Icon( + imageVector = if (isFavorite) + Icons.Filled.Favorite + else + Icons.Outlined.FavoriteBorder, + contentDescription = "Favorito", + tint = if (isFavorite) + Color.Red + else + MaterialTheme.colorScheme.onBackground + ) + } + }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.background ), diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt index 6883d73..a43caa2 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailState.kt @@ -5,6 +5,7 @@ import com.delecrode.devhub.domain.model.RepoDetail data class RepoState( val isLoading: Boolean = false, val error: String? = null, + val userName: String? = null, val repo: RepoDetail? = null, val languages: List? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt index 5124e07..a054b28 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt @@ -2,6 +2,7 @@ package com.delecrode.devhub.ui.repo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.repository.RepoRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -55,4 +56,19 @@ class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() { error = null ) } -} \ No newline at end of file + + + fun favoriteRepo(id: Int, owner: String, name: String, description: String, url: String) { + viewModelScope.launch { + repository.save( + RepoFav( + id = id, + name = name, + userName = owner ?: "", + description = description, + url = url + ) + ) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc03f11..bbefc6d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,8 @@ composeBom = "2024.09.00" navigationCompose = "2.9.6" coilCompose = "2.7.0" dataStore = "1.2.0" +room = "2.6.1" +ksp = "2.0.21-1.0.27" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -30,8 +32,12 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } data-store = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStore"} +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file