From c7bdd25d6bf5a9303331faa689f84becb6f0e64b Mon Sep 17 00:00:00 2001 From: guilh Date: Sat, 13 Dec 2025 12:00:07 -0300 Subject: [PATCH 1/5] =?UTF-8?q?Implementada=20tela=20de=20perfil=20do=20us?= =?UTF-8?q?u=C3=A1rio=20e=20persist=C3=AAncia=20do=20nome=20localmente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Criados `ProfileScreen`, `ProfileViewModel` e `ProfileState` para exibir dados do usuário (avatar, bio) e lista de repositórios. - Adicionada rota `Profile` em `AppDestinations` e configurada navegação no `AppNavHost`. - Atualizado `UserRepositoryImpl` e `AuthLocalDataSource` para salvar o nome do usuário no DataStore ao buscar dados do Firebase. - Registrado `ProfileViewModel` no módulo de injeção de dependência (`AppModule`). --- .../local/dataStore/AuthLocalDataSource.kt | 2 + .../dataStore/AuthLocalDataSourceImpl.kt | 15 +- .../data/repository/UserRepositoryImpl.kt | 19 +- .../java/com/delecrode/devhub/di/AppModule.kt | 4 +- .../devhub/navigation/AppDestinations.kt | 3 + .../delecrode/devhub/navigation/AppNavHost.kt | 12 +- .../devhub/ui/profile/ProfileScreen.kt | 275 ++++++++++++++++++ .../devhub/ui/profile/ProfileState.kt | 14 + .../devhub/ui/profile/ProfileViewModel.kt | 107 +++++++ 9 files changed, 438 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/delecrode/devhub/ui/profile/ProfileState.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt 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..4bf7ab8 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 @@ -6,5 +6,7 @@ interface AuthLocalDataSource { fun getUID(): Flow suspend fun saveUID(uid: String) + suspend fun saveName(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..d14262b 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 = @@ -24,9 +25,17 @@ class AuthLocalDataSourceImpl(private val context: Context) : AuthLocalDataSourc override suspend fun saveUID(uid: String) { Log.i("AuthLocalDataSourceImpl", "saveUser: $uid") - context.dataStore.edit { prefs -> - prefs[PreferencesKeys.UID_KEY] = uid - } + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.UID_KEY] = uid + } + + } + + override suspend fun saveName(name: String){ + Log.i("AuthLocalDataSourceImpl", "saveName: $name") + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.NAME_KEY] = name + } } override suspend fun clearUID() { 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..d4f303f 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 @@ -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 name = body?.username + if (name != null) { + authLocalDataSource.saveName(name) + } 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..489ba82 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -13,6 +13,7 @@ import com.delecrode.devhub.domain.repository.UserRepository import com.delecrode.devhub.domain.session.SessionViewModel 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 @@ -38,10 +39,11 @@ val appModule = module { single { RepoRepositoryImpl(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()) } } \ 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..fabfbb3 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,7 @@ sealed class AppDestinations(val route: String) { object Register: AppDestinations("register") object Login: AppDestinations("login") object ForgotPassword: AppDestinations("forgotPassword") + + object Profile: AppDestinations("profile") + } \ 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..85e1657 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppNavHost.kt @@ -13,6 +13,8 @@ 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 +26,23 @@ 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 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) + HomeScreen(navController, homeViewModel) } //Repositorio Flow @@ -70,5 +73,8 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { ForgotPasswordScreen(navController) } + composable(AppDestinations.Profile.route){ + ProfileScreen(navController, profileViewModel) + } } } \ 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..f5edb22 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,277 @@ 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.width +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.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +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.res.painterResource +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 +import com.delecrode.devhub.ui.theme.PrimaryBlue + +@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("Meu Perfil") }, + onClick = { + expanded = false + navController.navigate("profile") + } + ) + DropdownMenuItem( + text = { Text("Sair") }, + onClick = { + expanded = false + //homeViewModel.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..e102875 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt @@ -0,0 +1,107 @@ +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("HomeViewModel", "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 + ) + } +} + From 936d30080cb07cce1b44be25f60f06f5194635f9 Mon Sep 17 00:00:00 2001 From: guilh Date: Sat, 13 Dec 2025 12:01:35 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Removendo=20imports=20n=C3=A3o=20utilizados?= =?UTF-8?q?=20na=20ProfileScreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove imports de componentes de UI não mais utilizados (OutlinedTextField, width, painterResource, etc). - Remove import de cor personalizada (PrimaryBlue) não utilizada. --- .../java/com/delecrode/devhub/ui/profile/ProfileScreen.kt | 5 ----- 1 file changed, 5 deletions(-) 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 f5edb22..bfbd0f7 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 @@ -12,7 +12,6 @@ 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.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape @@ -30,8 +29,6 @@ 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.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -48,7 +45,6 @@ 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.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -58,7 +54,6 @@ import androidx.navigation.NavController import coil.compose.AsyncImage import com.delecrode.devhub.R import com.delecrode.devhub.navigation.AppDestinations -import com.delecrode.devhub.ui.theme.PrimaryBlue @OptIn(ExperimentalMaterial3Api::class) @Composable From 65a6f6cd4585b5a840c88f54cd32978ef2a7dece Mon Sep 17 00:00:00 2001 From: guilh Date: Sat, 13 Dec 2025 13:46:08 -0300 Subject: [PATCH 3/5] =?UTF-8?q?Implementada=20funcionalidade=20de=20reposi?= =?UTF-8?q?t=C3=B3rios=20favoritos=20com=20persist=C3=AAncia=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Configurado Room Database, DAO e Entidades para persistência de dados. - Adicionado `ReposFavScreen` e `RepoFavViewModel` para listagem de favoritos. - Implementado botão de favoritar na `RepoDetailScreen` e lógica no repositório. - Atualizado navegação e `ProfileScreen` para acesso à tela de favoritos. - Refatorado DTOs para pacote dedicado e ajustado `AuthLocalDataSource` e injeção de dependências. --- app/build.gradle.kts | 6 + .../local/dataStore/AuthLocalDataSource.kt | 3 +- .../dataStore/AuthLocalDataSourceImpl.kt | 21 ++- .../devhub/data/local/database/AppDatabase.kt | 14 ++ .../devhub/data/local/database/dao/RepoDao.kt | 19 +++ .../database/data/RepoLocalDataSource.kt | 11 ++ .../database/data/RepoLocalDataSourceImpl.kt | 17 +++ .../devhub/data/mapper/ReposMapper.kt | 31 +++- .../devhub/data/mapper/UserMapper.kt | 4 +- .../devhub/data/model/{ => dto}/ReposDto.kt | 3 +- .../devhub/data/model/{ => dto}/UserDto.kt | 2 +- .../devhub/data/model/entity/Repo.kt | 16 ++ .../remote/webApi/service/RepoApiService.kt | 4 +- .../remote/webApi/service/UserApiService.kt | 4 +- .../data/repository/RepoRepositoryImpl.kt | 20 ++- .../data/repository/UserRepositoryImpl.kt | 8 +- .../java/com/delecrode/devhub/di/AppModule.kt | 23 ++- .../devhub/domain/model/RepoDetail.kt | 1 + .../delecrode/devhub/domain/model/Repos.kt | 8 + .../domain/repository/RepoRepository.kt | 6 + .../devhub/navigation/AppDestinations.kt | 2 + .../delecrode/devhub/navigation/AppNavHost.kt | 12 +- .../devhub/ui/favoritos/RepoFavState.kt | 10 ++ .../devhub/ui/favoritos/RepoFavViewModel.kt | 40 +++++ .../devhub/ui/favoritos/ReposFavScreen.kt | 139 ++++++++++++++++++ .../devhub/ui/profile/ProfileScreen.kt | 4 +- .../devhub/ui/repo/RepoDetailScreen.kt | 33 +++++ .../devhub/ui/repo/RepoDetailState.kt | 1 + .../devhub/ui/repo/RepoDetailViewModel.kt | 20 ++- gradle/libs.versions.toml | 8 +- 30 files changed, 457 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/database/AppDatabase.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/database/dao/RepoDao.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSource.kt create mode 100644 app/src/main/java/com/delecrode/devhub/data/local/database/data/RepoLocalDataSourceImpl.kt rename app/src/main/java/com/delecrode/devhub/data/model/{ => dto}/ReposDto.kt (92%) rename app/src/main/java/com/delecrode/devhub/data/model/{ => dto}/UserDto.kt (96%) create mode 100644 app/src/main/java/com/delecrode/devhub/data/model/entity/Repo.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavViewModel.kt create mode 100644 app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt 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 4bf7ab8..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,9 +4,10 @@ import kotlinx.coroutines.flow.Flow interface AuthLocalDataSource { fun getUID(): Flow + fun getUserName(): Flow suspend fun saveUID(uid: String) - suspend fun saveName(name: 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 d14262b..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 @@ -22,20 +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 - } + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.UID_KEY] = uid + } } - override suspend fun saveName(name: String){ - Log.i("AuthLocalDataSourceImpl", "saveName: $name") - context.dataStore.edit { prefs -> - prefs[PreferencesKeys.NAME_KEY] = name - } + 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 d4f303f..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 @@ -46,9 +46,9 @@ class UserRepositoryImpl( val response = userExtraData.getUser(uid) if (response.exists()) { val body = response.toObject(UserForFirebaseDto::class.java)?.toUserDomain() - val name = body?.username - if (name != null) { - authLocalDataSource.saveName(name) + val userName = body?.username + if (userName != null) { + authLocalDataSource.saveUserName(userName) } if (body != null) { return body 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 489ba82..11e80c2 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,12 @@ 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.dao.RepoDao +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,6 +16,7 @@ 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 @@ -18,6 +24,7 @@ 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 @@ -30,13 +37,26 @@ 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()) } @@ -45,5 +65,6 @@ val appModule = module { 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 fabfbb3..8cbbe49 100644 --- a/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt +++ b/app/src/main/java/com/delecrode/devhub/navigation/AppDestinations.kt @@ -14,5 +14,7 @@ sealed class AppDestinations(val route: String) { 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 85e1657..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,6 +8,8 @@ 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 @@ -31,6 +33,8 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { val authViewModel: AuthViewModel = koinViewModel() val registerViewModel: RegisterViewModel = koinViewModel() val profileViewModel: ProfileViewModel = koinViewModel() + val repoFavViewModel: RepoFavViewModel = koinViewModel() + val logged = sessionViewModel.isLoggedIn.collectAsState() @@ -41,7 +45,7 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { startDestination = if (logged.value) AppDestinations.Home.route else AppDestinations.Login.route ) { //Home Flow - composable(AppDestinations.Home.route){ + composable(AppDestinations.Home.route) { HomeScreen(navController, homeViewModel) } @@ -73,8 +77,12 @@ fun AppNavHost(sessionViewModel: SessionViewModel) { ForgotPasswordScreen(navController) } - composable(AppDestinations.Profile.route){ + 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..fcb715e --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt @@ -0,0 +1,10 @@ +package com.delecrode.devhub.ui.favoritos + +import com.delecrode.devhub.domain.model.RepoFav +import kotlinx.coroutines.flow.Flow + +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..a155338 --- /dev/null +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt @@ -0,0 +1,139 @@ +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.lazy.items +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 bfbd0f7..303b118 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 @@ -110,10 +110,10 @@ fun ProfileScreen(navController: NavController, viewModel: ProfileViewModel) { onDismissRequest = { expanded = false } ) { DropdownMenuItem( - text = { Text("Meu Perfil") }, + text = { Text("Favoritos") }, onClick = { expanded = false - navController.navigate("profile") + navController.navigate(AppDestinations.ReposFav.route) } ) DropdownMenuItem( 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..e7e2cd9 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,9 +2,11 @@ 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 +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() { @@ -55,4 +57,20 @@ 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 { + val userName = repository.getUserName().first() + 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 From 6e397eb4a68bf9a6a10b2e838a06826e8e899d34 Mon Sep 17 00:00:00 2001 From: guilh Date: Sat, 13 Dec 2025 13:56:10 -0300 Subject: [PATCH 4/5] =?UTF-8?q?Corre=C3=A7=C3=B5es=20no=20logout=20e=20lim?= =?UTF-8?q?peza=20de=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corrigida a chamada de `signOut` na `ProfileScreen` para utilizar o ViewModel correto. - Atualizada tag de log no `ProfileViewModel` para refletir a classe correta. - Removida leitura desnecessária de `userName` no `RepoDetailViewModel`. - Removidos imports não utilizados em arquivos de configuração e UI. --- app/src/main/java/com/delecrode/devhub/di/AppModule.kt | 1 - .../java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt | 1 - .../java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt | 1 - .../main/java/com/delecrode/devhub/ui/profile/ProfileScreen.kt | 2 +- .../java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt | 3 ++- .../java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt | 1 - 6 files changed, 3 insertions(+), 6 deletions(-) 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 11e80c2..cfd29d1 100644 --- a/app/src/main/java/com/delecrode/devhub/di/AppModule.kt +++ b/app/src/main/java/com/delecrode/devhub/di/AppModule.kt @@ -4,7 +4,6 @@ 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.dao.RepoDao 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 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 index fcb715e..334e811 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/RepoFavState.kt @@ -1,7 +1,6 @@ package com.delecrode.devhub.ui.favoritos import com.delecrode.devhub.domain.model.RepoFav -import kotlinx.coroutines.flow.Flow data class RepoFavState( val isLoading: Boolean = false, 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 index a155338..276e41e 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/favoritos/ReposFavScreen.kt @@ -11,7 +11,6 @@ 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.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack 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 303b118..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 @@ -120,7 +120,7 @@ fun ProfileScreen(navController: NavController, viewModel: ProfileViewModel) { text = { Text("Sair") }, onClick = { expanded = false - //homeViewModel.signOut() + viewModel.signOut() navController.navigate("login") { popUpTo(0) } 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 index e102875..69ee5a0 100644 --- a/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/delecrode/devhub/ui/profile/ProfileViewModel.kt @@ -88,7 +88,7 @@ class ProfileViewModel(private val userRepository: UserRepository, private val a try { authRepository.signOut() } catch (e: Exception) { - Log.e("HomeViewModel", "Erro ao fazer logout", e) + Log.e("ProfileViewModel", "Erro ao fazer logout", e) _uiState.value = _uiState.value.copy( error = e.message, isLoading = false @@ -103,5 +103,6 @@ class ProfileViewModel(private val userRepository: UserRepository, private val a error = null ) } + } 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 e7e2cd9..364bc25 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 @@ -61,7 +61,6 @@ class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() { fun favoriteRepo(id: Int, owner: String, name: String, description: String, url: String) { viewModelScope.launch { - val userName = repository.getUserName().first() repository.save( RepoFav( id = id, From f85130c7916b22dbe5ca07ab6368d1ab2c2c981b Mon Sep 17 00:00:00 2001 From: guilh Date: Sat, 13 Dec 2025 13:58:49 -0300 Subject: [PATCH 5/5] =?UTF-8?q?Removendo=20import=20n=C3=A3o=20utilizado?= =?UTF-8?q?=20na=20RepoDetailViewModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove import de `kotlinx.coroutines.flow.first` que não estava sendo utilizado. --- .../java/com/delecrode/devhub/ui/repo/RepoDetailViewModel.kt | 1 - 1 file changed, 1 deletion(-) 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 364bc25..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 @@ -6,7 +6,6 @@ import com.delecrode.devhub.domain.model.RepoFav import com.delecrode.devhub.domain.repository.RepoRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch class RepoDetailViewModel(val repository: RepoRepository) : ViewModel() {