mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
feat(git): home polish + repo-card dashboard in the feed
Home screen: - Nav cards: tighter vertical spacing; badges now count only OPEN issues/PRs, derived from the live GitStatusIndex (started on the home so the split is correct without visiting the Issues screen first). - Moved the reaction row to after the recent-activity pulse. - Added the standard 3-dot note menu (MoreOptionsButton) to the top bar. Feed card (RenderGitRepositoryEvent): - Replaced the web/clone links with the same stat tiles + language bar + last-commit strip used on the home, loaded from a lazily-fetched shallow snapshot. (Factory made internal so the card can build the browser VM.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
This commit is contained in:
@@ -52,12 +52,15 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.nip34Git.GitBrowseState
|
||||
import com.vitorpamplona.amethyst.commons.nip34Git.GitRepositoryBrowserViewModel
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.GitPullRequestUpdateIndex
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -71,6 +74,11 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryBrowserViewModelFactory
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.RepoLanguageBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.RepoLastCommit
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.RepoStatTiles
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.computeLanguageBreakdown
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
@@ -785,8 +793,6 @@ private fun RenderGitRepositoryEvent(
|
||||
) {
|
||||
val title = noteEvent.name() ?: noteEvent.dTag()
|
||||
val summary = noteEvent.description()
|
||||
val web = noteEvent.web()
|
||||
val clone = noteEvent.clone()
|
||||
val topics = remember(noteEvent) { noteEvent.hashtags().filter { it.isNotBlank() } }
|
||||
val isPersonalFork = remember(noteEvent) { noteEvent.isPersonalFork() }
|
||||
|
||||
@@ -840,25 +846,7 @@ private fun RenderGitRepositoryEvent(
|
||||
)
|
||||
}
|
||||
|
||||
if (web != null || clone != null) {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Size5dp)) {
|
||||
web?.let {
|
||||
LinkRow(
|
||||
symbol = MaterialSymbols.Public,
|
||||
contentDescription = stringRes(id = R.string.git_web_address),
|
||||
url = it,
|
||||
)
|
||||
}
|
||||
clone?.let {
|
||||
LinkRow(
|
||||
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
|
||||
contentDescription = stringRes(id = R.string.git_clone_address),
|
||||
url = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RepoSnapshotDashboard(noteEvent, note, accountViewModel)
|
||||
|
||||
if (topics.isNotEmpty()) {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
@@ -877,3 +865,43 @@ private fun RenderGitRepositoryEvent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a shallow snapshot of the repository over smart-HTTP and renders the same
|
||||
* stat tiles / language bar / last-commit strip used on the project home, in place of
|
||||
* the old web/clone links. Fetches lazily (once) when the card is composed.
|
||||
*/
|
||||
@Composable
|
||||
private fun RepoSnapshotDashboard(
|
||||
noteEvent: GitRepositoryEvent,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val browser: GitRepositoryBrowserViewModel =
|
||||
viewModel(
|
||||
key = note.idHex + "GitRepoCardBrowser",
|
||||
factory = GitRepositoryBrowserViewModelFactory(accountViewModel.httpClientBuilder::okHttpClientForPreview),
|
||||
)
|
||||
LaunchedEffect(noteEvent) { browser.loadOnce(noteEvent.clones()) }
|
||||
val browserState by browser.state.collectAsStateWithLifecycle()
|
||||
val snapshot = (browserState as? GitBrowseState.Loaded)?.snapshot ?: return
|
||||
|
||||
val fileNames = remember(snapshot) { snapshot.walkFileNames() }
|
||||
val slices = remember(fileNames) { computeLanguageBreakdown(fileNames) }
|
||||
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
RepoStatTiles(
|
||||
branches = snapshot.branches.size,
|
||||
tags = snapshot.tags.size,
|
||||
files = fileNames.size,
|
||||
updatedEpochSec = snapshot.tipCommit?.authorTimeSec,
|
||||
)
|
||||
if (slices.isNotEmpty()) {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
RepoLanguageBar(slices)
|
||||
}
|
||||
snapshot.tipCommit?.let {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
RepoLastCommit(it)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-5
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.nip34Git.GitRepositoryBrowserViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.GitStatusIndex
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -82,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -163,7 +165,7 @@ fun GitRepositoryPullsScreen(
|
||||
* Builds the [GitRepositoryBrowserViewModel]. The factory lives app-side because the KMP
|
||||
* lifecycle artifact used in commons doesn't expose the `create(Class<T>)` override.
|
||||
*/
|
||||
private class GitRepositoryBrowserViewModelFactory(
|
||||
internal class GitRepositoryBrowserViewModelFactory(
|
||||
private val okHttpClient: (String) -> OkHttpClient,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = GitRepositoryBrowserViewModel(okHttpClient) as T
|
||||
@@ -258,6 +260,20 @@ private fun GitRepositoryHome(
|
||||
.take(6)
|
||||
}
|
||||
|
||||
// Nav-card badges count only the OPEN issues/PRs. The open/closed split needs the status
|
||||
// index (kinds 1630-1633), which is started here so the home reflects it without visiting
|
||||
// the Issues screen first; the count is then derived directly from the live index.
|
||||
LaunchedEffect(Unit) { GitStatusIndex.startIfNeeded() }
|
||||
val statusMap by GitStatusIndex.latestByTarget.collectAsStateWithLifecycle()
|
||||
val openIssueCount =
|
||||
remember(openIssueItems, closedIssueItems, statusMap) {
|
||||
(openIssueItems + closedIssueItems).distinctBy { it.idHex }.count { !GitStatusIndex.isClosedOrResolved(it.idHex, statusMap) }
|
||||
}
|
||||
val openPullCount =
|
||||
remember(openPatchItems, closedPatchItems, statusMap) {
|
||||
(openPatchItems + closedPatchItems).distinctBy { it.idHex }.count { !GitStatusIndex.isClosedOrResolved(it.idHex, statusMap) }
|
||||
}
|
||||
|
||||
var showSettings by rememberSaveable(note.idHex) { mutableStateOf(false) }
|
||||
val currentEventForSettings = event
|
||||
if (showSettings && currentEventForSettings != null) {
|
||||
@@ -290,6 +306,9 @@ private fun GitRepositoryHome(
|
||||
Icon(MaterialSymbols.Edit, contentDescription = stringRes(R.string.git_repo_settings_title))
|
||||
}
|
||||
}
|
||||
Row(Modifier.padding(end = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
MoreOptionsButton(note, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -309,7 +328,6 @@ private fun GitRepositoryHome(
|
||||
if (currentEvent != null) {
|
||||
RepoHero(currentEvent)
|
||||
RepoMaintainersRow(currentEvent, accountViewModel, nav)
|
||||
RepoSocialRow(note, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (snapshot != null) {
|
||||
@@ -325,11 +343,12 @@ private fun GitRepositoryHome(
|
||||
snapshot.tipCommit?.let { RepoLastCommit(it) }
|
||||
}
|
||||
|
||||
RepoNavCards(note, openIssueItems.size, openPatchItems.size, nav)
|
||||
RepoNavCards(note, openIssueCount, openPullCount, nav)
|
||||
|
||||
RepoActivityPulse(activity, accountViewModel, nav)
|
||||
|
||||
if (currentEvent != null) {
|
||||
RepoSocialRow(note, accountViewModel, nav)
|
||||
GitReadmeSection(browserState, browserViewModel, currentEvent, accountViewModel, nav)
|
||||
} else {
|
||||
EmptyMessage(stringRes(R.string.loading_feed))
|
||||
@@ -462,7 +481,7 @@ private fun RepoNavCards(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
RepoNavCard(MaterialSymbols.Code, stringRes(R.string.git_repo_tab_code), null) {
|
||||
nav.nav(Route.GitRepositoryCode(note.address))
|
||||
@@ -490,7 +509,7 @@ private fun RepoNavCard(
|
||||
.clip(RoundedCornerShape(15.dp))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user