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()
+ }
+}