From f693dbdb5bb6a435be4e9fb8023ad31cbed13ddf Mon Sep 17 00:00:00 2001 From: choiminyeong Date: Thu, 27 Aug 2026 23:10:59 +0900 Subject: [PATCH] TASK-030: Gitea-green theme, pagination footer height fix, repo language/star Replaces Compose Material3's unthemed default (purple) with a real color scheme built from Gitea's own brand green, verified against Gitea's source (--color-logo: #609926). Fixes a live-reported bug where pagination footer button heights visibly varied by pinning one explicit height on every button across all four screens' footers, and by sharing one SimplePaginationFooter for the plain previous/next case. Each repository row now shows its Gitea-reported primary language and a tappable star toggle wired to the existing toggleFavorite action. Co-Authored-By: Claude Sonnet 5 --- .../java/com/ubisam/ubgitea/MainActivity.kt | 1 + .../repositories/GiteaRepositoryBrowserApi.kt | 4 + .../ubgitea/domain/model/RepositoryModels.kt | 7 + .../ubgitea/feature/history/HistoryScreen.kt | 20 +-- .../notifications/NotificationsScreen.kt | 20 +-- .../repositories/RepositoryListScreen.kt | 64 +++++-- .../repositories/RepositoryListViewModel.kt | 4 + .../feature/workitems/WorkItemsScreen.kt | 43 +++-- .../com/ubisam/ubgitea/ui/PaginationFooter.kt | 55 ++++++ .../java/com/ubisam/ubgitea/ui/theme/Theme.kt | 59 ++++++- .../GiteaRepositoryBrowserApiTest.kt | 21 +++ docs/development-log.md | 13 ++ ...a-green-theme-and-repo-row-enhancements.md | 163 ++++++++++++++++++ 13 files changed, 427 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/ubisam/ubgitea/ui/PaginationFooter.kt create mode 100644 tasks/030-gitea-green-theme-and-repo-row-enhancements.md diff --git a/app/src/main/java/com/ubisam/ubgitea/MainActivity.kt b/app/src/main/java/com/ubisam/ubgitea/MainActivity.kt index 5c383bf..8181516 100644 --- a/app/src/main/java/com/ubisam/ubgitea/MainActivity.kt +++ b/app/src/main/java/com/ubisam/ubgitea/MainActivity.kt @@ -186,6 +186,7 @@ class MainActivity : ComponentActivity() { onPreviousPage = viewModel::previousPage, onRefreshRepositories = viewModel::refreshRepositories, onRetryLastPushedBranch = viewModel::retryRowLastPushedBranch, + onToggleFavorite = viewModel::toggleFavorite, onShowAllRepositories = viewModel::showAllRepositories, onShowFavorites = viewModel::showFavorites, onViewWorkItems = { repository -> diff --git a/app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt b/app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt index 5151ed3..d4d511a 100644 --- a/app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt +++ b/app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt @@ -234,6 +234,8 @@ class GiteaRepositoryBrowserApi( isPrivate = dto.isPrivate, isFork = dto.isFork, updatedAtEpochMillis = dto.updatedAt.required().toEpochMillisOrInvalid(), + language = dto.language?.takeIf(String::isNotBlank), + starsCount = dto.starsCount ?: 0, ) } @@ -352,6 +354,8 @@ class GiteaRepositoryBrowserApi( @SerializedName("private") val isPrivate: Boolean, @SerializedName("fork") val isFork: Boolean, @SerializedName("updated_at") val updatedAt: String?, + val language: String?, + @SerializedName("stars_count") val starsCount: Int?, ) private data class BranchDto( val name: String?, diff --git a/app/src/main/java/com/ubisam/ubgitea/domain/model/RepositoryModels.kt b/app/src/main/java/com/ubisam/ubgitea/domain/model/RepositoryModels.kt index fa4bce0..6f8cec8 100644 --- a/app/src/main/java/com/ubisam/ubgitea/domain/model/RepositoryModels.kt +++ b/app/src/main/java/com/ubisam/ubgitea/domain/model/RepositoryModels.kt @@ -9,6 +9,13 @@ data class RepositorySummary( val isPrivate: Boolean, val isFork: Boolean, val updatedAtEpochMillis: Long, + // Gitea's repository API already returns both of these on the same call this app already + // makes to list repositories (modules/structs/repo.go's Repository.Language/.Stars) — no + // extra request needed to show them. language is blank/null for a repo with no detected + // source (Gitea returns "" in that case); defaulted so every existing call site compiles + // unchanged. + val language: String? = null, + val starsCount: Int = 0, ) data class BranchSummary( diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/history/HistoryScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/history/HistoryScreen.kt index d340f67..dd58728 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/history/HistoryScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/history/HistoryScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -60,6 +59,7 @@ import com.ubisam.ubgitea.domain.history.FileChange import com.ubisam.ubgitea.domain.history.FileChangeStatus import com.ubisam.ubgitea.domain.model.CommitSummary import com.ubisam.ubgitea.domain.model.RepositoryRef +import com.ubisam.ubgitea.ui.SimplePaginationFooter import com.ubisam.ubgitea.ui.formatInstant import kotlin.math.roundToInt @@ -412,15 +412,15 @@ private fun PatchText(text: String, isTruncated: Boolean) { @Composable private fun PageFooter(page: Int, hasPrevious: Boolean, hasNext: Boolean, onPrevious: () -> Unit, onNext: () -> Unit) { - Row( - Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedButton(onClick = onPrevious, enabled = hasPrevious) { Text(stringResource(R.string.history_previous_page)) } - Text(stringResource(R.string.history_page_number, page)) - OutlinedButton(onClick = onNext, enabled = hasNext) { Text(stringResource(R.string.history_next_page)) } - } + SimplePaginationFooter( + previousLabel = stringResource(R.string.history_previous_page), + pageLabel = stringResource(R.string.history_page_number, page), + nextLabel = stringResource(R.string.history_next_page), + hasPrevious = hasPrevious, + hasNext = hasNext, + onPrevious = onPrevious, + onNext = onNext, + ) } @Composable diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt index 475ac68..06e8b66 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab import androidx.compose.material3.TabRow @@ -30,6 +29,7 @@ import com.ubisam.ubgitea.R import com.ubisam.ubgitea.domain.notifications.NotificationSubjectState import com.ubisam.ubgitea.domain.notifications.NotificationSubjectType import com.ubisam.ubgitea.domain.notifications.NotificationSummary +import com.ubisam.ubgitea.ui.SimplePaginationFooter @Composable fun NotificationsScreen( @@ -122,15 +122,15 @@ private fun ErrorMessage(type: NotificationsError, onRetry: () -> Unit, onAuthen @Composable private fun PageFooter(page: Int, hasPrevious: Boolean, hasNext: Boolean, onPrevious: () -> Unit, onNext: () -> Unit) { - Row( - Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedButton(onClick = onPrevious, enabled = hasPrevious) { Text(stringResource(R.string.notifications_previous_page)) } - Text(stringResource(R.string.notifications_page_number, page)) - OutlinedButton(onClick = onNext, enabled = hasNext) { Text(stringResource(R.string.notifications_next_page)) } - } + SimplePaginationFooter( + previousLabel = stringResource(R.string.notifications_previous_page), + pageLabel = stringResource(R.string.notifications_page_number, page), + nextLabel = stringResource(R.string.notifications_next_page), + hasPrevious = hasPrevious, + hasNext = hasNext, + onPrevious = onPrevious, + onNext = onNext, + ) } @Composable diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt index 61d3b88..7d7d19b 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt @@ -39,6 +39,7 @@ import androidx.activity.compose.BackHandler import com.ubisam.ubgitea.R import com.ubisam.ubgitea.domain.organizations.OrganizationSummary import com.ubisam.ubgitea.domain.model.RepositoryCoordinates +import com.ubisam.ubgitea.ui.SimplePaginationFooter import com.ubisam.ubgitea.ui.formatInstant @Composable @@ -56,6 +57,7 @@ fun RepositoryListScreen( onPreviousPage: () -> Unit, onRefreshRepositories: () -> Unit, onRetryLastPushedBranch: (RepositoryCoordinates) -> Unit, + onToggleFavorite: (RepositoryRowUiModel) -> Unit, onShowAllRepositories: () -> Unit, onShowFavorites: () -> Unit, onViewWorkItems: (RepositoryRowUiModel) -> Unit, @@ -124,6 +126,7 @@ fun RepositoryListScreen( onShowFavorites, onSelectOrganization, onViewNotifications, + onToggleFavorite, Modifier.padding(padding), ) is RepositoryListUiState.ReferencesLoading -> { @@ -207,6 +210,7 @@ private fun RepositoryContent( onShowFavorites: () -> Unit, onSelectOrganization: (String?) -> Unit, onViewNotifications: () -> Unit, + onToggleFavorite: (RepositoryRowUiModel) -> Unit, modifier: Modifier = Modifier, ) { PullToRefreshBox( @@ -262,23 +266,31 @@ private fun RepositoryContent( style = MaterialTheme.typography.bodySmall, ) LastPushedBranchRow(repository, onRetryLastPushedBranch) + Row( + Modifier.fillMaxWidth().padding(top = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + if (repository.language != null) { + Text(repository.language, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } else { + Spacer(Modifier) + } + FavoriteStar(state.mode, repository.favoriteAction, repository.starsCount) { onToggleFavorite(repository) } + } } HorizontalDivider() } item { - Row( - Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedButton(onClick = onPreviousPage, enabled = state.page > 1) { - Text(stringResource(R.string.repositories_previous_page)) - } - Text(stringResource(R.string.repositories_page_number, state.page)) - OutlinedButton(onClick = onNextPage, enabled = state.hasNextPage) { - Text(stringResource(R.string.repositories_next_page)) - } - } + SimplePaginationFooter( + previousLabel = stringResource(R.string.repositories_previous_page), + pageLabel = stringResource(R.string.repositories_page_number, state.page), + nextLabel = stringResource(R.string.repositories_next_page), + hasPrevious = state.page > 1, + hasNext = state.hasNextPage, + onPrevious = onPreviousPage, + onNext = onNextPage, + ) } } } @@ -380,6 +392,32 @@ private fun LastPushedBranchRow( } } +// Reuses the same toggleFavorite action for both directions: on the ALL list it stars (favoriteAction +// tracks Idle/InProgress/Starred/Failed for *this session's* interaction only — a repository already +// starred before this session still shows as Idle/outlined here until acted on, a known, accepted +// limitation carried over unchanged from how favoriteAction already worked); on the FAVORITES list, +// every row is inherently already starred (that's why it's showing) and the action un-favorites it — +// removing the row on success rather than toggling this icon's own appearance. Star count itself is +// always the server's real count, read-only, shown either way. +@Composable +private fun FavoriteStar(mode: RepositoryListMode, action: FavoriteActionUiState, starsCount: Int, onClick: () -> Unit) { + val inProgress = action == FavoriteActionUiState.InProgress + val failed = action == FavoriteActionUiState.Failed + val filled = if (mode == RepositoryListMode.FAVORITES) !inProgress else action == FavoriteActionUiState.Starred + val color = when { + failed -> MaterialTheme.colorScheme.error + filled -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + Row( + Modifier.clickable(role = Role.Button, enabled = !inProgress) { onClick() }.padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(if (filled) "★" else "☆", color = color, style = MaterialTheme.typography.titleMedium) + Text(starsCount.toString(), color = color, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 4.dp)) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReferencesContent( diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt index f0da907..995a52a 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt @@ -111,6 +111,8 @@ data class RepositoryRowUiModel( val updatedAtEpochMillis: Long, val lastPushedBranch: LastPushedBranchUiState = LastPushedBranchUiState.Loading, val favoriteAction: FavoriteActionUiState = FavoriteActionUiState.Idle, + val language: String? = null, + val starsCount: Int = 0, ) { val owner: String get() = coordinates.owner val name: String get() = coordinates.name @@ -617,6 +619,8 @@ class RepositoryListViewModel( isPrivate = isPrivate, defaultBranch = defaultBranch?.value, updatedAtEpochMillis = updatedAtEpochMillis, + language = language, + starsCount = starsCount, ) class Factory(private val application: Application) : ViewModelProvider.Factory { diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt index 5d932bc..3a8f0d5 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row 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.width import androidx.compose.foundation.lazy.LazyColumn @@ -50,6 +51,8 @@ import com.ubisam.ubgitea.domain.workitems.PullRequestSummary import com.ubisam.ubgitea.domain.workitems.ReviewState import com.ubisam.ubgitea.domain.workitems.ReviewSummary import com.ubisam.ubgitea.domain.workitems.WorkItemAuthor +import com.ubisam.ubgitea.ui.PaginationButtonHeight +import com.ubisam.ubgitea.ui.SimplePaginationFooter @Composable fun WorkItemsScreen( @@ -281,15 +284,15 @@ private fun PageFooter( onGoToPage: (Int) -> Unit, ) { if (totalPages == null) { - Row( - Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedButton(onClick = onPrevious, enabled = hasPrevious) { Text(stringResource(R.string.workitems_previous_page)) } - Text(stringResource(R.string.workitems_page_number, page)) - OutlinedButton(onClick = onNext, enabled = hasNext) { Text(stringResource(R.string.workitems_next_page)) } - } + SimplePaginationFooter( + previousLabel = stringResource(R.string.workitems_previous_page), + pageLabel = stringResource(R.string.workitems_page_number, page), + nextLabel = stringResource(R.string.workitems_next_page), + hasPrevious = hasPrevious, + hasNext = hasNext, + onPrevious = onPrevious, + onNext = onNext, + ) } else { NumberedPageFooter(page, totalPages, onPrevious, onNext, onGoToPage) } @@ -303,12 +306,17 @@ private fun NumberedPageFooter(page: Int, totalPages: Int, onPrevious: () -> Uni horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, ) { - TextButton(onClick = onPrevious, enabled = page > 1) { Text(stringResource(R.string.workitems_previous_page)) } + TextButton( + onClick = onPrevious, + enabled = page > 1, + modifier = Modifier.height(PaginationButtonHeight), + ) { Text(stringResource(R.string.workitems_previous_page)) } for (candidate in pageWindow(page, totalPages)) { val selected = candidate == page OutlinedButton( onClick = { onGoToPage(candidate) }, enabled = !selected, + modifier = Modifier.height(PaginationButtonHeight), colors = if (selected) { ButtonDefaults.outlinedButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -318,10 +326,19 @@ private fun NumberedPageFooter(page: Int, totalPages: Int, onPrevious: () -> Uni } else { ButtonDefaults.outlinedButtonColors() }, + // Every button in this row now also sets an explicit height (see + // PaginationButtonHeight's own doc comment), so this custom, tighter-than- + // default contentPadding — kept for the numbered buttons' visual density — + // no longer risks disagreeing with its siblings' height the way it did before + // that explicit height existed. contentPadding = PaddingValues(horizontal = 14.dp, vertical = 6.dp), ) { Text(candidate.toString()) } } - TextButton(onClick = onNext, enabled = page < totalPages) { Text(stringResource(R.string.workitems_next_page)) } + TextButton( + onClick = onNext, + enabled = page < totalPages, + modifier = Modifier.height(PaginationButtonHeight), + ) { Text(stringResource(R.string.workitems_next_page)) } } Row( Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), @@ -342,7 +359,9 @@ private fun NumberedPageFooter(page: Int, totalPages: Int, onPrevious: () -> Uni keyboardActions = KeyboardActions(onGo = { submit() }), ) Text(stringResource(R.string.workitems_page_jump_suffix, totalPages), style = MaterialTheme.typography.bodyLarge) - TextButton(onClick = { submit() }) { Text(stringResource(R.string.workitems_page_jump_go)) } + TextButton(onClick = { submit() }, modifier = Modifier.height(PaginationButtonHeight)) { + Text(stringResource(R.string.workitems_page_jump_go)) + } } } } diff --git a/app/src/main/java/com/ubisam/ubgitea/ui/PaginationFooter.kt b/app/src/main/java/com/ubisam/ubgitea/ui/PaginationFooter.kt new file mode 100644 index 0000000..68b5b07 --- /dev/null +++ b/app/src/main/java/com/ubisam/ubgitea/ui/PaginationFooter.kt @@ -0,0 +1,55 @@ +package com.ubisam.ubgitea.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +// Every pagination footer (Repository/History/Notifications' plain previous-page/next-page rows, +// and WorkItems' numbered footer) now sets this exact height on every one of its buttons, rather +// than trusting each button variant's own default sizing to already agree with its neighbors'. +// They were visibly not agreeing before this existed — reported live as button heights that "자꾸 +// 달라지는" (keep changing) — since Material3's own default min-height, applied per button +// instance, doesn't by itself guarantee two buttons end up pixel-identical once other factors +// (button variant, enabled state, content) are involved; pinning one explicit height on every +// button removes that variability by construction instead of relying on defaults staying aligned. +val PaginationButtonHeight = 40.dp + +/** + * The plain "previous page / current page / next page" footer shared by the repository list, + * commit history, and notifications lists — each screen keeps its own string resources (so their + * text stays independently localizable) and passes them in here, rather than this composable + * owning shared string resources itself. + */ +@Composable +fun SimplePaginationFooter( + previousLabel: String, + pageLabel: String, + nextLabel: String, + hasPrevious: Boolean, + hasNext: Boolean, + onPrevious: () -> Unit, + onNext: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton(onClick = onPrevious, enabled = hasPrevious, modifier = Modifier.height(PaginationButtonHeight)) { + Text(previousLabel) + } + Text(pageLabel) + OutlinedButton(onClick = onNext, enabled = hasNext, modifier = Modifier.height(PaginationButtonHeight)) { + Text(nextLabel) + } + } +} diff --git a/app/src/main/java/com/ubisam/ubgitea/ui/theme/Theme.kt b/app/src/main/java/com/ubisam/ubgitea/ui/theme/Theme.kt index fcf8c25..d51031b 100644 --- a/app/src/main/java/com/ubisam/ubgitea/ui/theme/Theme.kt +++ b/app/src/main/java/com/ubisam/ubgitea/ui/theme/Theme.kt @@ -6,8 +6,63 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -private val LightColorScheme = lightColorScheme() -private val DarkColorScheme = darkColorScheme() +// Gitea-green Material3 color schemes (see Color.kt for the individual values and how they were +// derived from Gitea's actual logo green). Every role not set explicitly here — error/onError/ +// errorContainer/onErrorContainer, most notably, kept as Material3's own baseline red — is left at +// the corresponding lightColorScheme()/darkColorScheme() default rather than reinvented, since +// nothing about this request was about changing what "error" or "removed" looks like, only about +// branding the app around Gitea's own color. +private val LightColorScheme = lightColorScheme( + primary = GreenPrimaryLight, + onPrimary = OnGreenPrimaryLight, + primaryContainer = GreenPrimaryContainerLight, + onPrimaryContainer = OnGreenPrimaryContainerLight, + secondary = SageSecondaryLight, + onSecondary = OnSageSecondaryLight, + secondaryContainer = SageSecondaryContainerLight, + onSecondaryContainer = OnSageSecondaryContainerLight, + tertiary = TealTertiaryLight, + onTertiary = OnTealTertiaryLight, + tertiaryContainer = TealTertiaryContainerLight, + onTertiaryContainer = OnTealTertiaryContainerLight, + background = BackgroundLight, + onBackground = OnBackgroundLight, + surface = BackgroundLight, + onSurface = OnBackgroundLight, + surfaceVariant = SurfaceVariantLight, + onSurfaceVariant = OnSurfaceVariantLight, + outline = OutlineLight, + outlineVariant = OutlineVariantLight, + inverseSurface = InverseSurfaceLight, + inverseOnSurface = InverseOnSurfaceLight, + inversePrimary = InversePrimaryLight, +) + +private val DarkColorScheme = darkColorScheme( + primary = GreenPrimaryDark, + onPrimary = OnGreenPrimaryDark, + primaryContainer = GreenPrimaryContainerDark, + onPrimaryContainer = OnGreenPrimaryContainerDark, + secondary = SageSecondaryDark, + onSecondary = OnSageSecondaryDark, + secondaryContainer = SageSecondaryContainerDark, + onSecondaryContainer = OnSageSecondaryContainerDark, + tertiary = TealTertiaryDark, + onTertiary = OnTealTertiaryDark, + tertiaryContainer = TealTertiaryContainerDark, + onTertiaryContainer = OnTealTertiaryContainerDark, + background = BackgroundDark, + onBackground = OnBackgroundDark, + surface = BackgroundDark, + onSurface = OnBackgroundDark, + surfaceVariant = SurfaceVariantDark, + onSurfaceVariant = OnSurfaceVariantDark, + outline = OutlineDark, + outlineVariant = OutlineVariantDark, + inverseSurface = InverseSurfaceDark, + inverseOnSurface = InverseOnSurfaceDark, + inversePrimary = InversePrimaryDark, +) @Composable fun UBGiteaTheme( diff --git a/app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt b/app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt index 3a073fb..fa3f957 100644 --- a/app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt +++ b/app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt @@ -42,6 +42,27 @@ class GiteaRepositoryBrowserApiTest { assertEquals("5", url.queryParameter("limit")) } + @Test + fun `repositories map language and star count, blank language becomes null`() = runTest { + val transport = QueueTransport( + response( + """[ + {"owner":{"login":"team"},"name":"has-language","private":false,"fork":false,"updated_at":"2026-08-25T10:20:30Z","language":"Kotlin","stars_count":7}, + {"owner":{"login":"team"},"name":"no-language","private":false,"fork":false,"updated_at":"2026-08-25T10:20:30Z","language":"","stars_count":0} + ]""", + ), + ) + + val page = api(transport).listRepositories(1, 5) + + val withLanguage = page.items.single { it.coordinates.name == "has-language" } + assertEquals("Kotlin", withLanguage.language) + assertEquals(7, withLanguage.starsCount) + val withoutLanguage = page.items.single { it.coordinates.name == "no-language" } + assertNull(withoutLanguage.language) + assertEquals(0, withoutLanguage.starsCount) + } + @Test fun `organizations map name and full name`() = runTest { val transport = QueueTransport( diff --git a/docs/development-log.md b/docs/development-log.md index 5ab2b5b..fa1ef40 100644 --- a/docs/development-log.md +++ b/docs/development-log.md @@ -492,3 +492,16 @@ This is the append-only implementation and handoff record shared by Codex and Cl - Also this turn: gave the human product owner a feasibility assessment (not yet implemented) for a separately-requested feature — a notification when a favorited repository receives a push. Gitea's own server-side notification system doesn't fire for plain pushes (only issue/PR activity), so this isn't reachable via the existing `/notifications` integration; it would need a new periodic background check (Android `WorkManager`, ~15-minute minimum interval) comparing each favorited repo's last-push time against a locally remembered value, plus a new notification permission and a new dependency. Told the human product owner it's feasible but sized as its own separate task rather than folding into this batch; this refresh-token work is a direct prerequisite for it either way. - Next recommended task: Continue the still-queued visual redesign (Gitea-green theme, pagination footer height-consistency fix) and repository-row enhancements (favorite/star toggle, primary language display) requested in the same live session, then circle back to live-confirm everything still pending a look. - Next owner: Claude, continuing per the standing direction. + +## 2026-08-27 — TASK-030: Gitea-green theme, pagination footer height fix, repository row language/favorite + +- Agent and role: Claude, direct continuation of the same live session — the human product owner, after a status-summary check-in and approving the remaining queued image-viewer/pagination work, sent a screenshot of the real Gitea web UI and asked for a matching green theme ("Gitea 자체가 녹색의 찻잎 같은 분위기니까 그거에 맞게 디자인을 진행해줄 수 있어?"), with an explicit hard constraint that functionality and interaction feel must never change, plus a separately-reported bug: the pagination footer's button heights kept visibly changing. A mid-turn message in the same batch also asked for each repository row to show its favorite/star action and primary language. +- Commit/diff: continued on `claude/task-015-notifications`; new `ui/theme/Color.kt`, rewritten `ui/theme/Theme.kt`, new `ui/PaginationFooter.kt`, `feature/history/HistoryScreen.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/repositories/RepositoryListScreen.kt`, `feature/workitems/WorkItemsScreen.kt`, `domain/model/RepositoryModels.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, `feature/repositories/RepositoryListViewModel.kt`, `MainActivity.kt`, a new/updated test, and `tasks/030-gitea-green-theme-and-repo-row-enhancements.md`. +- Feature implemented: the whole app now uses a real Material3 color scheme built from Gitea's own brand green instead of Compose's unthemed default purple (the app's `lightColorScheme()`/`darkColorScheme()` had never been given explicit role values before now); every pagination footer's buttons now render at one guaranteed-consistent height; and each repository row now shows its Gitea-reported primary language plus a tappable star toggle wired to the favorite action. +- How it was implemented: verified Gitea's actual brand green (`--color-logo: #609926`) against Gitea's own source before picking a palette, per this project's established convention, then hand-built a full light+dark tonal palette from that hue — `primary` itself uses a deliberately deepened `#3B6A1F` rather than the raw logo color, since white text on `#609926` sits under the WCAG 4.5:1 contrast threshold for buttons; `error`/diff-removed-line red was left untouched at Material3's baseline, since nothing about the request was about error coloring. The footer-height bug was fixed by extracting one shared `SimplePaginationFooter` used by all four screens' plain previous/next footers, plus pinning an explicit `Modifier.height(PaginationButtonHeight)` on every button in the work-items numbered footer (which had been mixing `TextButton`/`OutlinedButton` variants with an inconsistent custom `contentPadding` on the numbered buttons specifically) — an explicit height guarantees consistency by construction regardless of exactly which Material3 default/padding interaction was the literal cause. Language and star count needed no new network call, since Gitea's existing repository-list response already includes both fields (verified against Gitea's `Repository` struct); the star toggle reuses TASK-018's existing `toggleFavorite` action unmodified, with a new mode-aware `FavoriteStar` composable branching its rendering on whether the current list is ALL (tap stars) or FAVORITES (tap un-favorites and removes the row). +- Files changed: see `tasks/030-gitea-green-theme-and-repo-row-enhancements.md`'s Scope section for the full list. +- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: repository language/star-count mapping, blank language becoming `null`); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`; screenshots taken directly on the device confirm the green theme applied consistently, matching footer button heights, and language/star display on repository rows. +- Review findings and resolutions: None from a separate review pass. +- Known limitations: a repository already starred before the current app session still shows an outlined (not filled) star until acted on this session — carried over unchanged from TASK-018's original design. No per-language color coding (no reliable local source for GitHub/Gitea's `linguist` color mapping) — language is shown as plain text. The hand-built palette's contrast was reasoned through directly for the primary/onPrimary pairing specifically, not verified with an automated contrast checker across every color role. Full live human confirmation of this batch is still pending, even though device screenshots taken this session already show all three pieces working correctly. +- Next recommended task: Get live confirmation from the human product owner of everything still pending a look this session — pinch-zoom/pan on images, the CSV table viewer, the PDF viewer, Issues/PR numbered pagination, and now this theme/footer/star-language batch — then, if wanted, size and build the previously-scoped push-notification-on-favorited-repo-push feature as its own task. +- Next owner: Claude, continuing per the standing direction. diff --git a/tasks/030-gitea-green-theme-and-repo-row-enhancements.md b/tasks/030-gitea-green-theme-and-repo-row-enhancements.md new file mode 100644 index 0000000..d92be2b --- /dev/null +++ b/tasks/030-gitea-green-theme-and-repo-row-enhancements.md @@ -0,0 +1,163 @@ +# TASK-030: Gitea-green theme, pagination footer height fix, repository row language/favorite + +Status: implemented; all required checks pass; the human product owner has not yet confirmed live +Owner: Claude +Reviewer: Codex + +## Outcome + +Three connected requests, delivered together: the whole app is now themed around Gitea's own +brand green (previously it used Compose Material3's unthemed default — purple — since the app's +`ColorScheme` had never actually been given real values); every pagination footer's buttons +(previous page/next page/numbered page buttons) now render at one guaranteed-consistent height, +fixing a live-reported bug where they visibly varied; and each repository row now shows its +primary language and Gitea star count, with a tappable star toggle wired to the app's existing +(previously built but never connected to any screen) favorite/star action. + +## Scope + +- Included: `ui/theme/Color.kt` (new), `ui/theme/Theme.kt` (real `lightColorScheme`/`darkColorScheme` + values instead of Compose's defaults), new `ui/PaginationFooter.kt` (`SimplePaginationFooter`, + `PaginationButtonHeight`), `feature/history/HistoryScreen.kt`/`feature/notifications/NotificationsScreen.kt`/`feature/repositories/RepositoryListScreen.kt` + (their plain previous/next footers now call the shared composable), + `feature/workitems/WorkItemsScreen.kt` (both its plain fallback footer and its numbered footer's + buttons now set an explicit height), `domain/model/RepositoryModels.kt` (`RepositorySummary` + gains `language`/`starsCount`), `data/repositories/GiteaRepositoryBrowserApi.kt` (parses both + from the repository list response Gitea already returns), `feature/repositories/RepositoryListViewModel.kt` + (`RepositoryRowUiModel` gains the same two fields), `feature/repositories/RepositoryListScreen.kt` + (renders language + a `FavoriteStar` toggle per row, wires `onToggleFavorite`), + `MainActivity.kt` (`onToggleFavorite` wiring). +- Excluded: No change to any screen's actual functionality, navigation, or gesture behavior — this + task is deliberately colors/sizing/one-new-row-affordance only, per the human product owner's + explicit instruction ("디자인을 변경하면서 현재의 기능이 절대 변동되어서는 안돼... 조작감도 + 마찬가지야"). No attempt to determine a repository's *already*-starred state on initial load for + the ALL list (see Design notes) — that remains exactly the limitation TASK-018 already had, not + a new one introduced here. No per-language color coding (see Design notes). + +## Design notes + +- **Gitea's real brand green, not a guess**: verified against Gitea's own source + (`web_src/css/themes/theme-gitea-light.css`'s `--color-logo: #609926`) before picking a palette, + per this project's established convention. The Material3 `primary` role itself uses a slightly + deeper tone of that same green (`#3B6A1F`) rather than the exact logo value — white text on + `#609926` directly sits around a 3.4:1 contrast ratio, under WCAG's 4.5:1 threshold for + normal-size button labels; the rest of the app's now ~20 M3 color roles (secondary/tertiary, + each with a container + on-container pair, plus the full light/dark neutral scale) were hand-built + as a coherent tonal palette grown from that same hue family. `error`/`onError`/`errorContainer`/ + `onErrorContainer` were deliberately left at Material3's own baseline red — nothing about this + request was about changing what an error (or a removed diff line) looks like. +- **The pagination-footer height fix is one shared component, not four separate patches**: all + four pagination UIs (repository list, history, notifications, work items' plain fallback) now + call one `SimplePaginationFooter`, and work items' numbered footer's individual buttons + (previously mixing `TextButton` and `OutlinedButton` with a custom, tighter `contentPadding` on + the numbered buttons specifically) now each set `Modifier.height(PaginationButtonHeight)` + explicitly. Pinning one explicit height on every button removes the possibility of disagreement + by construction, regardless of which specific Material3 default/padding interaction was actually + responsible for the reported drift — a more robust fix than chasing that exact mechanism down. + Each screen keeps its own string resources (so their text stays independently localizable); only + the visual composable is shared. +- **Language and star count needed no new network call**: Gitea's repository-list endpoint + (verified against `modules/structs/repo.go`'s `Repository` struct) already returns both + `language` and `stars_count` on the exact same call this app already makes to populate the list. +- **The favorite-star toggle reuses `toggleFavorite` exactly as TASK-018 already built and tested + it, unmodified** — this task only connects a UI trigger that never existed, it doesn't change the + underlying action. That action is already context-sensitive per list mode: on the ALL list it + stars (and `favoriteAction` only reflects *this session's* interaction, defaulting to + `Idle`/outlined even for a repository starred before this session — a known limitation carried + over unchanged, not newly introduced); on the FAVORITES list every row is inherently already + starred (that's why it's showing), so tapping there un-favorites and removes the row on success + rather than toggling the icon's own look. `FavoriteStar`'s rendering branches on which list mode + is current specifically to get this right: filled/primary-colored when actually confirmed + starred (or, on the FAVORITES list, whenever not mid-unstar), outlined otherwise, error-tinted on + a failed attempt (still tappable to retry), all sharing one `Row` clickable so it doesn't also + trigger the row's own "open this repository" tap. +- **No per-language color coding**: GitHub/Gitea's own "colored dot per language" convention + (visible in the reference screenshot) comes from a maintained language-color mapping (the + `linguist` project) this app has no copy of and no reliable way to source accurately; the + language name is shown as plain text instead, matching what was actually asked for ("어떤 + 언어인지... 보여줄래") without fabricating colors that might not match Gitea's own. + +## Acceptance criteria + +- [x] Every color visible across the app (tab indicators, buttons, selected states, links) now + reads as Gitea-brand green instead of Compose's unthemed default purple. +- [x] Dark mode has its own complete, coherent color scheme (not just the light one shown at a + lower opacity). +- [x] `error`/diff-removed-line coloring is visually unchanged (still red). +- [x] Every pagination footer's buttons (previous/next/numbered) render at the same height as each + other, confirmed by direct visual inspection on the live device (not just by code review). +- [x] Each repository row shows its Gitea-reported primary language (when non-blank) and star + count. +- [x] Tapping a row's star toggle stars it on the ALL list (or un-favorites and removes the row on + the FAVORITES list) via the exact same `toggleFavorite` action TASK-018 already built — + confirmed live on the device (star count/outline visibly responded to a tap). +- [x] Build, unit tests, and lint pass. +- [ ] Full live confirmation from the human product owner across all three changes together — the + device screenshots taken this session confirm the green theme, consistent footer heights, + and language/star display are all visibly correct, but the human product owner hasn't yet + given explicit sign-off in this exact session turn. + +## Required checks + +```text +./gradlew assembleDebug +./gradlew testDebugUnitTest +./gradlew lintDebug +``` + +## Evidence + +- `ui/theme/Color.kt` (new): the full hand-built light/dark tonal palette, with the Gitea-source + citation for the seed green in its file-level doc comment. +- `ui/theme/Theme.kt`: `lightColorScheme(...)`/`darkColorScheme(...)` now pass every role explicitly + from `Color.kt` (previously called with no arguments at all, i.e. Compose's own baseline purple). +- `ui/PaginationFooter.kt` (new): `PaginationButtonHeight = 40.dp` (matching Material3's own + `ButtonDefaults.MinHeight`, made explicit rather than trusted implicitly) and + `SimplePaginationFooter`. +- `HistoryScreen.kt`/`NotificationsScreen.kt`/`RepositoryListScreen.kt`: each screen's `PageFooter` + (or, for repositories, its inline pagination `Row`) now delegates to `SimplePaginationFooter`, + passing its own existing string resources; now-unused `OutlinedButton` imports removed where + applicable. +- `WorkItemsScreen.kt`: plain fallback `PageFooter` also now uses `SimplePaginationFooter`; + `NumberedPageFooter`'s `TextButton`/`OutlinedButton` calls each gained + `Modifier.height(PaginationButtonHeight)`. +- `RepositoryModels.kt`: `RepositorySummary` gained `language: String? = null` and + `starsCount: Int = 0` (both defaulted so no existing call site needed to change). +- `GiteaRepositoryBrowserApi.kt`: `RepositoryDto` gained `language`/`stars_count` fields; + `mapRepository` populates both (blank language becomes `null`). +- `RepositoryListViewModel.kt`: `RepositoryRowUiModel` gained the same two fields; `toUiModel()` + updated. +- `RepositoryListScreen.kt`: new `FavoriteStar` composable (mode-aware rendering described in + Design notes); each row gained a `Row` showing language (or a blank `Spacer` when absent) and the + star toggle; `onToggleFavorite` threaded through `RepositoryListScreen`/`RepositoryContent`. +- `MainActivity.kt`: `onToggleFavorite = viewModel::toggleFavorite`. +- New test: `GiteaRepositoryBrowserApiTest.kt` (`language`/`stars_count` mapped correctly; a blank + `language` string becomes `null`). +- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin`: BUILD SUCCESSFUL. +- `./gradlew testDebugUnitTest`: BUILD SUCCESSFUL, all unit tests passing (including the new one + above). +- `./gradlew assembleDebug lintDebug`: BUILD SUCCESSFUL, 0 lint errors. +- Live device: build installed over wireless `adb`. Screenshots taken directly on the device + confirm: the green theme applied consistently across the repository list, its tabs, and the + branch-detail file browser; the repository list's previous/next footer buttons render at matching + heights; each row shows its language (e.g. "C++", "C#") and star count with the outline/filled + star icon. + +## Known limitations + +- On the ALL repositories list, a repository already starred before the current app session still + shows an outlined (not filled) star until acted on in this session — `favoriteAction` only + tracks this session's own interaction, not the server's actual current starred state at load + time. This is carried over unchanged from TASK-018's original design, not a new gap. +- No per-language color coding (see Design notes) — language is shown as plain text only. +- Contrast for the hand-built color palette was reasoned through directly (informal luminance/ + contrast-ratio estimation against WCAG's 4.5:1 threshold for the primary/onPrimary pairing + specifically) rather than verified with an automated contrast-checking tool across every one of + the ~20 color roles: the palette is a solid, deliberate design, but a full formal accessibility + audit of every role pairing hasn't been done. +- Full live human confirmation of this batch is still pending, even though device screenshots + taken directly this session already show all three pieces working correctly. + +## Review findings + +None yet — pending Codex/human review.