-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Polish HomeScreen UI to match prototype design #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| package dev.therealashik.github.home | ||
| import kotlinx.datetime.Clock | ||
| import kotlinx.datetime.Instant | ||
| import kotlin.time.* | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.* | ||
|
|
@@ -9,6 +12,10 @@ import androidx.compose.material.icons.Icons | |
| import androidx.compose.material.icons.filled.MoreVert | ||
| import androidx.compose.material.icons.outlined.* | ||
| import androidx.compose.material3.* | ||
| import coil3.compose.AsyncImage | ||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.material.icons.outlined.CallSplit | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.collectAsState | ||
| import androidx.compose.runtime.getValue | ||
|
|
@@ -52,14 +59,25 @@ fun HomeScreenContent(state: HomeUiState, onRetry: () -> Unit = {}, onNavigateTo | |
| Icon(Icons.Outlined.AddCircle, contentDescription = stringResource(Res.string.cd_create)) | ||
| } | ||
| IconButton(onClick = onNavigateToProfile) { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Person, | ||
| contentDescription = stringResource(Res.string.cd_user_avatar), | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeNormal) | ||
| .clip(CircleShape) | ||
| .background(MaterialTheme.colorScheme.surfaceVariant) | ||
| ) | ||
| if (state is HomeUiState.Success && state.avatarUrl.isNotEmpty()) { | ||
| AsyncImage( | ||
| model = state.avatarUrl, | ||
| contentDescription = stringResource(Res.string.cd_user_avatar), | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeNormal) | ||
| .clip(CircleShape) | ||
| .background(MaterialTheme.colorScheme.surfaceVariant) | ||
| ) | ||
| } else { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Person, | ||
| contentDescription = stringResource(Res.string.cd_user_avatar), | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeNormal) | ||
| .clip(CircleShape) | ||
| .background(MaterialTheme.colorScheme.surfaceVariant) | ||
| ) | ||
| } | ||
| } | ||
| }, | ||
| colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.background), | ||
|
|
@@ -93,20 +111,38 @@ fun HomeScreenContent(state: HomeUiState, onRetry: () -> Unit = {}, onNavigateTo | |
| } | ||
| is HomeUiState.Success -> { | ||
| LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) { | ||
| // Repositories | ||
| // My Work | ||
| item { SectionHeader(title = stringResource(Res.string.section_my_work)) } | ||
| items(state.repos) { RepoRow(it) } | ||
| item { | ||
| MyWorkRow( | ||
| icon = Icons.Outlined.AccountBox, | ||
| title = stringResource(Res.string.my_work_repos), | ||
| color = MaterialTheme.colorScheme.surfaceVariant | ||
| ) | ||
| } | ||
| item { | ||
| MyWorkRow( | ||
| icon = Icons.Outlined.Menu, | ||
| title = stringResource(Res.string.my_work_orgs), | ||
| color = MaterialTheme.colorScheme.tertiary | ||
| ) | ||
| } | ||
| item { HomeDivider() } | ||
|
|
||
| // Organizations | ||
| item { SectionHeader(title = "Organizations") } | ||
| if (state.orgs.isEmpty()) { | ||
| item { EmptyHint("No organizations") } | ||
| // Favorites | ||
| item { SectionHeader(title = stringResource(Res.string.section_favorites)) } | ||
| if (state.starred.isEmpty()) { | ||
| item { EmptyHint(stringResource(Res.string.no_starred_repos)) } | ||
| } else { | ||
| items(state.orgs) { OrgRow(it) } | ||
| items(state.starred.take(5)) { StarredRow(it) } | ||
| } | ||
| item { HomeDivider() } | ||
|
|
||
| // Shortcuts | ||
| item { SectionHeader(title = stringResource(Res.string.section_shortcuts)) } | ||
| item { ShortcutsRow() } | ||
| item { HomeDivider() } | ||
|
|
||
| // Notifications | ||
| item { SectionHeader(title = stringResource(Res.string.section_recent)) } | ||
| if (state.notifications.isEmpty()) { | ||
|
|
@@ -144,6 +180,125 @@ private fun SectionHeader(title: String) { | |
| } | ||
| } | ||
|
|
||
|
|
||
| @Composable | ||
| private fun MyWorkRow(icon: androidx.compose.ui.graphics.vector.ImageVector, title: String, color: androidx.compose.ui.graphics.Color) { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .clickable { } | ||
| .padding(horizontal = Dimens.SpacingMedium, vertical = Dimens.SpacingSmall), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| Box( | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeLarge) | ||
| .background(color, MaterialTheme.shapes.medium), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Icon( | ||
| imageVector = icon, | ||
| contentDescription = null, | ||
| tint = MaterialTheme.colorScheme.onPrimaryContainer, | ||
| modifier = Modifier.size(Dimens.IconSizeNormal) | ||
| ) | ||
| } | ||
| Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) | ||
| Text( | ||
| text = title, | ||
| style = MaterialTheme.typography.bodyLarge, | ||
| color = MaterialTheme.colorScheme.onBackground | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun ShortcutsRow() { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .clickable { } | ||
| .padding(horizontal = Dimens.SpacingMedium, vertical = Dimens.SpacingSmall), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| Box( | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeLarge) | ||
| .background(MaterialTheme.colorScheme.secondary, MaterialTheme.shapes.medium), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Info, | ||
| contentDescription = null, | ||
| tint = MaterialTheme.colorScheme.onSecondary, | ||
| modifier = Modifier.size(Dimens.IconSizeNormal) | ||
| ) | ||
| } | ||
| Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) | ||
| Column { | ||
| Text( | ||
| text = stringResource(Res.string.shortcuts_issues), | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||
| ) | ||
| Text( | ||
| text = stringResource(Res.string.shortcuts_mentioned), | ||
| style = MaterialTheme.typography.bodyLarge, | ||
| color = MaterialTheme.colorScheme.onBackground | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+216
to
+251
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| @Composable | ||
| private fun StarredRow(item: StarredItem) { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .clickable { } | ||
| .padding(horizontal = Dimens.SpacingMedium, vertical = Dimens.SpacingSmall), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| if (item.ownerAvatarUrl.isNotEmpty()) { | ||
| AsyncImage( | ||
| model = item.ownerAvatarUrl, | ||
| contentDescription = null, | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeAvatar) | ||
| .clip(CircleShape) | ||
| ) | ||
| } else { | ||
| Box( | ||
| modifier = Modifier | ||
| .size(Dimens.IconSizeAvatar) | ||
| .clip(CircleShape) | ||
| .background(MaterialTheme.colorScheme.surfaceVariant), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Person, | ||
| contentDescription = null, | ||
| tint = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| modifier = Modifier.size(Dimens.IconSizeNormal) | ||
| ) | ||
| } | ||
| } | ||
| Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) | ||
| Column { | ||
| Text( | ||
| text = item.ownerLogin, | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||
| ) | ||
| Text( | ||
| text = item.name, | ||
| style = MaterialTheme.typography.bodyLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), | ||
| color = MaterialTheme.colorScheme.onBackground | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun RepoRow(item: RepoItem) { | ||
| Row( | ||
|
|
@@ -307,3 +462,56 @@ private fun HomeDivider() { | |
| color = MaterialTheme.colorScheme.outlineVariant | ||
| ) | ||
| } | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| private fun relativeTime(iso: String): String { | ||
| return try { | ||
| // Fallback implementation without kotlinx.datetime since it fails to resolve properly in this environment | ||
| if (iso.length < 19) return "" | ||
| val now = io.ktor.util.date.getTimeMillis() | ||
| // Approximation of ISO parsing using basic math since kotlinx.datetime is not resolving: | ||
| val year = iso.substring(0, 4).toInt() | ||
| val month = iso.substring(5, 7).toInt() | ||
| val day = iso.substring(8, 10).toInt() | ||
| val hour = iso.substring(11, 13).toInt() | ||
| val min = iso.substring(14, 16).toInt() | ||
| val sec = iso.substring(17, 19).toInt() | ||
|
|
||
| val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) | ||
| var totalDays = 0L | ||
| for (y in 1970 until year) { | ||
| totalDays += if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) 366 else 365 | ||
| } | ||
| val isLeap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) | ||
| for (m in 1 until month) { | ||
| totalDays += daysInMonth[m] | ||
| if (m == 2 && isLeap) totalDays++ | ||
| } | ||
| totalDays += (day - 1) | ||
|
|
||
| val totalSeconds = (totalDays * 24L * 60L * 60L) + (hour * 60L * 60L) + (min * 60L) + sec | ||
| val millis = totalSeconds * 1000L | ||
|
|
||
| val diff = now - millis | ||
| if (diff < 0) return "just now" | ||
|
|
||
| val diffSeconds = diff / 1000 | ||
| val diffMinutes = diffSeconds / 60 | ||
| val diffHours = diffMinutes / 60 | ||
| val diffDays = diffHours / 24 | ||
|
|
||
| when { | ||
| diffSeconds < 60 -> "${diffSeconds}s" | ||
| diffMinutes < 60 -> "${diffMinutes}m" | ||
| diffHours < 24 -> "${diffHours}h" | ||
| diffDays < 7 -> "${diffDays}d" | ||
| else -> "${diffDays / 7}w" | ||
| } | ||
| } catch (e: Exception) { | ||
| "" | ||
| } | ||
| } | ||
|
Comment on lines
+465
to
+517
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The private fun relativeTime(iso: String): String {
return try {
val instant = Instant.parse(iso)
val now = Clock.System.now()
val diff = now - instant
val seconds = diff.inWholeSeconds
if (seconds < 0) return "just now"
val minutes = diff.inWholeMinutes
val hours = diff.inWholeHours
val days = diff.inWholeDays
when {
seconds < 60 -> "${seconds}s"
minutes < 60 -> "${minutes}m"
hours < 24 -> "${hours}h"
days < 7 -> "${days}d"
else -> "${days / 7}w"
}
} catch (e: Exception) {
""
}
} |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import sys | ||
| import re | ||
|
|
||
| # Wait, `UIViewLayoutRegion` is an iOS 17 framework thing from `SwiftUI` or `UIKit` probably. | ||
| # If I add `-framework SwiftUI` to `linkerOpts`? | ||
| with open('composeApp/build.gradle.kts', 'r') as f: | ||
| content = f.read() | ||
|
|
||
| if "linkerOpts" not in content: | ||
| content = content.replace('isStatic = true', 'isStatic = true\n linkerOpts("-lsqlite3", "-framework", "SwiftUI", "-framework", "UIKit")') | ||
|
|
||
| with open('composeApp/build.gradle.kts', 'w') as f: | ||
| f.write(content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These string resources (
shortcuts_issuesandshortcuts_mentioned) appear to be duplicates ofshortcut_issuesandshortcut_mentioneddefined later in the file (lines 90-91). Please remove the duplicates and use a consistent naming convention throughout the project.