From a298c8510f5ce3c87b9ebaa70c82e30d569792db Mon Sep 17 00:00:00 2001 From: TheRealAshik <177647015+TheRealAshik@users.noreply.github.com> Date: Sat, 16 May 2026 08:08:06 +0000 Subject: [PATCH] feat: Add StarredScreen to display starred repositories - Created `StarredScreen` and `StarredViewModel` to display user's starred repos. - Updated `ProfileScreen` to wire the navigation to the new Starred route. - Updated `App.kt` to define the new `Route.Starred`. - Added string resource `starred_title`. --- .../composeResources/values/strings.xml | 2 + .../kotlin/dev/therealashik/github/App.kt | 9 ++ .../dev/therealashik/github/NavRoutes.kt | 1 + .../github/profile/ProfileScreen.kt | 9 +- .../github/profile/StarredScreen.kt | 146 ++++++++++++++++++ .../github/profile/StarredUiState.kt | 15 ++ .../github/profile/StarredViewModel.kt | 46 ++++++ 7 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredUiState.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredViewModel.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index d9b77f4..f99f5d3 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -169,6 +169,8 @@ Starred Projects + Starred + Repositories TheRealAshik diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt index feab061..c3d59f8 100755 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt @@ -10,6 +10,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import dev.therealashik.github.profile.ProfileScreen import dev.therealashik.github.profile.ProfileViewModel +import dev.therealashik.github.profile.StarredScreen +import dev.therealashik.github.profile.StarredViewModel import dev.therealashik.github.repository.RepositoryListScreen import dev.therealashik.github.repository.RepositoryListViewModel import dev.therealashik.github.settings.AddPatScreen @@ -45,9 +47,16 @@ fun App() { viewModel = ProfileViewModel(), onBack = { navController.popBackStack() }, onNavigateToRepositories = { navController.navigate(Route.Repositories) }, + onNavigateToStarred = { navController.navigate(Route.Starred) }, onNavigateToSettings = { navController.navigate(Route.Settings) } ) } + composable { + StarredScreen( + viewModel = StarredViewModel(), + onBack = { navController.popBackStack() } + ) + } composable { RepositoryListScreen( viewModel = RepositoryListViewModel(), diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt index caa8988..a77eef1 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt @@ -11,4 +11,5 @@ sealed interface Route { @Serializable data object NotificationOptions : Route @Serializable data object CodeOptions : Route @Serializable data object AddPat : Route + @Serializable data object Starred : Route } diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt index 9d053c2..cef6fdc 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt @@ -34,6 +34,7 @@ fun ProfileScreen( viewModel: ProfileViewModel, onBack: () -> Unit, onNavigateToRepositories: () -> Unit, + onNavigateToStarred: () -> Unit, onNavigateToSettings: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsState() @@ -105,7 +106,8 @@ fun ProfileScreen( HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) NavigationListSection( state = state, - onNavigateToRepositories = onNavigateToRepositories + onNavigateToRepositories = onNavigateToRepositories, + onNavigateToStarred = onNavigateToStarred ) } } @@ -349,7 +351,8 @@ private fun PopularReposSection(popularRepos: List) { @Composable private fun NavigationListSection( state: ProfileUiState.Success, - onNavigateToRepositories: () -> Unit + onNavigateToRepositories: () -> Unit, + onNavigateToStarred: () -> Unit ) { Column { NavigationItem( @@ -410,7 +413,7 @@ private fun NavigationListSection( }, label = stringResource(Res.string.nav_starred), count = state.starredCount, - onClick = { /* TODO */ } + onClick = onNavigateToStarred ) NavigationItem( icon = { diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredScreen.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredScreen.kt new file mode 100644 index 0000000..cd47d43 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredScreen.kt @@ -0,0 +1,146 @@ +package dev.therealashik.github.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import dev.therealashik.github.Dimens +import github.composeapp.generated.resources.Res +import github.composeapp.generated.resources.content_description_back +import github.composeapp.generated.resources.retry +import github.composeapp.generated.resources.starred_title +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StarredScreen( + viewModel: StarredViewModel, + onBack: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(Res.string.starred_title)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.content_description_back) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface + ) + ) + } + ) { innerPadding -> + when (val state = uiState) { + is StarredUiState.Loading -> { + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.Center + ) { CircularProgressIndicator() } + } + is StarredUiState.Error -> { + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(state.message, color = MaterialTheme.colorScheme.error) + Spacer(modifier = Modifier.height(Dimens.SpacingMedium)) + Button(onClick = viewModel::loadData) { Text(stringResource(Res.string.retry)) } + } + } + } + is StarredUiState.Success -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentPadding = PaddingValues(vertical = Dimens.SpacingMedium), + verticalArrangement = Arrangement.spacedBy(Dimens.SpacingMedium) + ) { + items(state.starredRepos) { repo -> + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Dimens.SpacingMedium), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Dimens.SpacingMedium) + ) { + Text( + text = repo.name, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface + ) + if (repo.description != null) { + Spacer(modifier = Modifier.height(Dimens.SpacingSmall)) + Text( + text = repo.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.height(Dimens.SpacingMedium)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(Dimens.IconSizeSmall) + ) + Spacer(modifier = Modifier.width(Dimens.SpacingExtraSmall)) + Text( + text = repo.stars.toString(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (repo.language != null) { + Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) + Box( + modifier = Modifier + .size(Dimens.IndicatorSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + ) + Spacer(modifier = Modifier.width(Dimens.SpacingExtraSmall)) + Text( + text = repo.language, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredUiState.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredUiState.kt new file mode 100644 index 0000000..76798f6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredUiState.kt @@ -0,0 +1,15 @@ +package dev.therealashik.github.profile + +sealed class StarredUiState { + data object Loading : StarredUiState() + data class Error(val message: String) : StarredUiState() + data class Success(val starredRepos: List) : StarredUiState() +} + +data class StarredRepoItem( + val id: String, + val name: String, + val description: String?, + val stars: Int, + val language: String? +) diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredViewModel.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredViewModel.kt new file mode 100644 index 0000000..95310d9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/StarredViewModel.kt @@ -0,0 +1,46 @@ +package dev.therealashik.github.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.therealashik.github.data.GitHubApiClient +import dev.therealashik.github.data.createTokenStorage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class StarredViewModel : ViewModel() { + private val apiClient = GitHubApiClient(createTokenStorage()) + private val _uiState = MutableStateFlow(StarredUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadData() + } + + fun loadData() { + viewModelScope.launch { + _uiState.value = StarredUiState.Loading + apiClient.getStarredRepos(perPage = 100) + .onSuccess { repos -> + _uiState.value = StarredUiState.Success( + repos.map { r -> + StarredRepoItem( + id = r.id.toString(), + name = r.name, + description = r.description, + stars = r.stars, + language = r.language + ) + } + ) + } + .onFailure { _uiState.value = StarredUiState.Error(it.message ?: "Unknown error") } + } + } + + override fun onCleared() { + super.onCleared() + apiClient.close() + } +}