TASK-075: chip border, quiet short loads, exit question, back to top
A selected filter chip is outlined; the loading cup waits 450 ms before it shows; Back on 홈 asks before leaving; long lists offer a button back to the top; AnimatedContent no longer animates its own size, which made sideways moves drift diagonally. Agent: main (opus), implementer (sonnet) for Chips.kt and ExitConfirmDialog.kt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bb546421fc
commit
a140b56aec
@ -91,6 +91,8 @@ import com.ubisam.ubgitea.feature.work.WorkViewModel
|
|||||||
import com.ubisam.ubgitea.feature.workitems.WorkItemsScreen
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsScreen
|
||||||
import com.ubisam.ubgitea.feature.workitems.WorkItemsTarget
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsTarget
|
||||||
import com.ubisam.ubgitea.feature.workitems.WorkItemsViewModel
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsViewModel
|
||||||
|
import com.ubisam.ubgitea.ui.components.ExitConfirmDialog
|
||||||
|
import com.ubisam.ubgitea.ui.components.NoResize
|
||||||
import com.ubisam.ubgitea.ui.components.SplashOverlay
|
import com.ubisam.ubgitea.ui.components.SplashOverlay
|
||||||
import com.ubisam.ubgitea.ui.components.TabPager
|
import com.ubisam.ubgitea.ui.components.TabPager
|
||||||
import com.ubisam.ubgitea.ui.navigation.Route
|
import com.ubisam.ubgitea.ui.navigation.Route
|
||||||
@ -277,6 +279,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var confirmExit by remember { mutableStateOf(false) }
|
||||||
val instanceHost = UBGiteaOAuthDefaults.config.baseUri.host.orEmpty()
|
val instanceHost = UBGiteaOAuthDefaults.config.baseUri.host.orEmpty()
|
||||||
val codeViewerPreferences = CodeViewerPreferences(settingsState.codeWordWrapEnabled, settingsState.codeFontSizeSp)
|
val codeViewerPreferences = CodeViewerPreferences(settingsState.codeWordWrapEnabled, settingsState.codeFontSizeSp)
|
||||||
val showNavigation = sessionReady && when (currentRoute) {
|
val showNavigation = sessionReady && when (currentRoute) {
|
||||||
@ -394,9 +397,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
) { isWide ->
|
) { isWide ->
|
||||||
// Back on any tab but Home goes Home; back on Home leaves the app.
|
// Back on any tab but Home goes Home; back on Home asks before leaving the
|
||||||
BackHandler(enabled = sessionReady && currentRoute.isTab && currentRoute != Route.Home) {
|
// app, since it is easily pressed once too often.
|
||||||
selectTab(AppTab.HOME)
|
BackHandler(enabled = sessionReady && currentRoute.isTab) {
|
||||||
|
if (currentRoute == Route.Home) confirmExit = true else selectTab(AppTab.HOME)
|
||||||
}
|
}
|
||||||
val repositoryList: @Composable () -> Unit = {
|
val repositoryList: @Composable () -> Unit = {
|
||||||
RepositoryListScreen(
|
RepositoryListScreen(
|
||||||
@ -453,11 +457,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
// The session has just been found: the first screen takes the
|
// The session has just been found: the first screen takes the
|
||||||
// waiting one's place where it stands. Nothing was navigated, so
|
// waiting one's place where it stands. Nothing was navigated, so
|
||||||
// nothing slides.
|
// nothing slides.
|
||||||
fadeIn(tween(200)).togetherWith(fadeOut(tween(90)))
|
fadeIn(tween(200)).togetherWith(fadeOut(tween(90))).using(NoResize)
|
||||||
} else {
|
} else {
|
||||||
val direction = (stackDepth - shownDepth[0]).coerceIn(-1, 1)
|
val direction = (stackDepth - shownDepth[0]).coerceIn(-1, 1)
|
||||||
(fadeIn(tween(200, delayMillis = 50)) + slideInHorizontally(tween(260)) { it / 10 * direction })
|
(fadeIn(tween(200, delayMillis = 50)) + slideInHorizontally(tween(260)) { it / 10 * direction })
|
||||||
.togetherWith(fadeOut(tween(90)))
|
.togetherWith(fadeOut(tween(90)))
|
||||||
|
.using(NoResize)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentKey = { if (it.isTab) AppTab::class else it::class },
|
contentKey = { if (it.isTab) AppTab::class else it::class },
|
||||||
@ -691,6 +696,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (confirmExit) {
|
||||||
|
ExitConfirmDialog(onStay = { confirmExit = false }, onExit = { finish() })
|
||||||
|
}
|
||||||
SplashOverlay(settled = sessionReady || state.sessionReadyOrNull() != null)
|
SplashOverlay(settled = sessionReady || state.sessionReadyOrNull() != null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,19 +25,19 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@ -86,6 +86,7 @@ import com.ubisam.ubgitea.feature.codebrowser.content.ContentClassification
|
|||||||
import com.ubisam.ubgitea.ui.components.CenteredLoading
|
import com.ubisam.ubgitea.ui.components.CenteredLoading
|
||||||
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
||||||
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.UBIcon
|
import com.ubisam.ubgitea.ui.components.UBIcon
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -127,7 +128,7 @@ fun CodeBrowserScreen(
|
|||||||
if (state.entries.isEmpty()) {
|
if (state.entries.isEmpty()) {
|
||||||
CenteredMessage(stringResource(R.string.codebrowser_directory_empty))
|
CenteredMessage(stringResource(R.string.codebrowser_directory_empty))
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 16.dp)) {
|
ScrollTopLazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 16.dp)) {
|
||||||
itemsIndexed(state.entries, key = { _, entry -> entry.path.value }) { index, entry ->
|
itemsIndexed(state.entries, key = { _, entry -> entry.path.value }) { index, entry ->
|
||||||
EntryRow(entry, index == 0, index == state.entries.lastIndex, onEntrySelected)
|
EntryRow(entry, index == 0, index == state.entries.lastIndex, onEntrySelected)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@ -51,6 +50,7 @@ import com.ubisam.ubgitea.ui.components.CenteredMessage
|
|||||||
import com.ubisam.ubgitea.ui.components.EmptyCup
|
import com.ubisam.ubgitea.ui.components.EmptyCup
|
||||||
import com.ubisam.ubgitea.ui.components.ListCard
|
import com.ubisam.ubgitea.ui.components.ListCard
|
||||||
import com.ubisam.ubgitea.ui.components.MonoChip
|
import com.ubisam.ubgitea.ui.components.MonoChip
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.SectionTitle
|
import com.ubisam.ubgitea.ui.components.SectionTitle
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.formatInstant
|
import com.ubisam.ubgitea.ui.formatInstant
|
||||||
@ -89,7 +89,7 @@ fun HistoryScreen(
|
|||||||
if (state.commits.isEmpty()) {
|
if (state.commits.isEmpty()) {
|
||||||
CenteredMessage(stringResource(R.string.history_empty), decoration = { EmptyCup() })
|
CenteredMessage(stringResource(R.string.history_empty), decoration = { EmptyCup() })
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 16.dp)) {
|
ScrollTopLazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 16.dp)) {
|
||||||
itemsIndexed(state.commits, key = { _, commit -> commit.id }) { index, commit ->
|
itemsIndexed(state.commits, key = { _, commit -> commit.id }) { index, commit ->
|
||||||
CommitRow(commit, index == 0, index == state.commits.lastIndex) { onCommitSelected(commit) }
|
CommitRow(commit, index == 0, index == state.commits.lastIndex) { onCommitSelected(commit) }
|
||||||
}
|
}
|
||||||
@ -177,7 +177,7 @@ private fun CommitDetailView(
|
|||||||
onFileSelected: (FileChange) -> Unit,
|
onFileSelected: (FileChange) -> Unit,
|
||||||
) {
|
) {
|
||||||
val canOpenFileDiff = state.patch is PatchUiState.Loaded
|
val canOpenFileDiff = state.patch is PatchUiState.Loaded
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
||||||
item(key = "commit") {
|
item(key = "commit") {
|
||||||
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -63,10 +62,12 @@ import com.ubisam.ubgitea.domain.workitems.WorkTypeFilter
|
|||||||
import com.ubisam.ubgitea.ui.components.ActivityTone
|
import com.ubisam.ubgitea.ui.components.ActivityTone
|
||||||
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
||||||
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
||||||
|
import com.ubisam.ubgitea.ui.components.FilterChip
|
||||||
import com.ubisam.ubgitea.ui.components.KindTile
|
import com.ubisam.ubgitea.ui.components.KindTile
|
||||||
import com.ubisam.ubgitea.ui.components.ListCard
|
import com.ubisam.ubgitea.ui.components.ListCard
|
||||||
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
||||||
import com.ubisam.ubgitea.ui.components.MonoChip
|
import com.ubisam.ubgitea.ui.components.MonoChip
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.SectionTitle
|
import com.ubisam.ubgitea.ui.components.SectionTitle
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.components.TabTitle
|
import com.ubisam.ubgitea.ui.components.TabTitle
|
||||||
@ -159,7 +160,7 @@ private fun HomeContent(
|
|||||||
LazyColumn(Modifier.weight(0.5f).fillMaxHeight(), contentPadding = PaddingValues(bottom = 24.dp)) {
|
LazyColumn(Modifier.weight(0.5f).fillMaxHeight(), contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||||
overview(state, vertical = true, onNavigate, onSelectSource, showToday)
|
overview(state, vertical = true, onNavigate, onSelectSource, showToday)
|
||||||
}
|
}
|
||||||
LazyColumn(
|
ScrollTopLazyColumn(
|
||||||
Modifier.weight(0.5f).fillMaxHeight(),
|
Modifier.weight(0.5f).fillMaxHeight(),
|
||||||
state = timelineState,
|
state = timelineState,
|
||||||
// Starts level with the headline card on the left.
|
// Starts level with the headline card on the left.
|
||||||
@ -169,7 +170,7 @@ private fun HomeContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(bottom = 24.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||||
overview(state, vertical = false, onNavigate, onSelectSource, showToday)
|
overview(state, vertical = false, onNavigate, onSelectSource, showToday)
|
||||||
timeline(state, onNavigate, onSelectFilter, onSetTodayOnly, onLoadMore)
|
timeline(state, onNavigate, onSelectFilter, onSetTodayOnly, onLoadMore)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,11 +13,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@ -43,8 +41,10 @@ import com.ubisam.ubgitea.ui.components.ActivityTone
|
|||||||
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
||||||
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
||||||
import com.ubisam.ubgitea.ui.components.EmptyCup
|
import com.ubisam.ubgitea.ui.components.EmptyCup
|
||||||
|
import com.ubisam.ubgitea.ui.components.FilterChip
|
||||||
import com.ubisam.ubgitea.ui.components.KindTile
|
import com.ubisam.ubgitea.ui.components.KindTile
|
||||||
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.formatRelative
|
import com.ubisam.ubgitea.ui.formatRelative
|
||||||
import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
|
import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
|
||||||
@ -120,7 +120,7 @@ private fun NotificationsList(
|
|||||||
onNextPage: () -> Unit,
|
onNextPage: () -> Unit,
|
||||||
onPreviousPage: () -> Unit,
|
onPreviousPage: () -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
||||||
itemsIndexed(state.items, key = { _, item -> item.id }) { index, notification ->
|
itemsIndexed(state.items, key = { _, item -> item.id }) { index, notification ->
|
||||||
NotificationRow(notification, index == 0, index == state.items.lastIndex) { onSelected(notification) }
|
NotificationRow(notification, index == 0, index == state.items.lastIndex) { onSelected(notification) }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,14 +17,12 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -63,9 +61,11 @@ import com.ubisam.ubgitea.ui.components.CautionBanner
|
|||||||
import com.ubisam.ubgitea.ui.components.CenteredLoading
|
import com.ubisam.ubgitea.ui.components.CenteredLoading
|
||||||
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
||||||
import com.ubisam.ubgitea.ui.components.EmptyCup
|
import com.ubisam.ubgitea.ui.components.EmptyCup
|
||||||
|
import com.ubisam.ubgitea.ui.components.FilterChip
|
||||||
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
||||||
import com.ubisam.ubgitea.ui.components.MonoChip
|
import com.ubisam.ubgitea.ui.components.MonoChip
|
||||||
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.SectionTitle
|
import com.ubisam.ubgitea.ui.components.SectionTitle
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.components.TabGlide
|
import com.ubisam.ubgitea.ui.components.TabGlide
|
||||||
@ -267,7 +267,7 @@ private fun RepositoryContent(
|
|||||||
CautionBanner(stringResource(R.string.repositories_offline_cached_banner, formatInstant(cachedAt)))
|
CautionBanner(stringResource(R.string.repositories_offline_cached_banner, formatInstant(cachedAt)))
|
||||||
}
|
}
|
||||||
TeaPullToRefreshBox(onRefresh = onRefreshRepositories, modifier = Modifier.fillMaxSize()) {
|
TeaPullToRefreshBox(onRefresh = onRefreshRepositories, modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
ScrollTopLazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().modeSwipeGesture(state.mode, onShowAllRepositories, onShowFavorites),
|
modifier = Modifier.fillMaxSize().modeSwipeGesture(state.mode, onShowAllRepositories, onShowFavorites),
|
||||||
contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp),
|
contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
@ -533,7 +533,7 @@ private fun ReferencesContent(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
TeaPullToRefreshBox(onRefresh = onRefresh, modifier = Modifier.fillMaxSize()) {
|
TeaPullToRefreshBox(onRefresh = onRefresh, modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
||||||
item(key = "branches-title") { SectionTitle(stringResource(R.string.repositories_branches_title)) }
|
item(key = "branches-title") { SectionTitle(stringResource(R.string.repositories_branches_title)) }
|
||||||
if (state.branches.isEmpty()) {
|
if (state.branches.isEmpty()) {
|
||||||
item(key = "branches-empty") { EmptyReferenceMessage(stringResource(R.string.repositories_branches_empty)) }
|
item(key = "branches-empty") { EmptyReferenceMessage(stringResource(R.string.repositories_branches_empty)) }
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
@ -37,6 +36,7 @@ import com.ubisam.ubgitea.core.settings.ThemeMode
|
|||||||
import com.ubisam.ubgitea.core.update.AppUpdateUiState
|
import com.ubisam.ubgitea.core.update.AppUpdateUiState
|
||||||
import com.ubisam.ubgitea.core.update.UpdateCheck
|
import com.ubisam.ubgitea.core.update.UpdateCheck
|
||||||
import com.ubisam.ubgitea.ui.components.Avatar
|
import com.ubisam.ubgitea.ui.components.Avatar
|
||||||
|
import com.ubisam.ubgitea.ui.components.FilterChip
|
||||||
import com.ubisam.ubgitea.ui.components.ListCard
|
import com.ubisam.ubgitea.ui.components.ListCard
|
||||||
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
||||||
import com.ubisam.ubgitea.ui.components.SectionTitle
|
import com.ubisam.ubgitea.ui.components.SectionTitle
|
||||||
|
|||||||
@ -11,11 +11,9 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@ -43,8 +41,10 @@ import com.ubisam.ubgitea.ui.components.ActivityTone
|
|||||||
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
import com.ubisam.ubgitea.ui.components.BrandTopBar
|
||||||
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
import com.ubisam.ubgitea.ui.components.CenteredMessage
|
||||||
import com.ubisam.ubgitea.ui.components.EmptyCup
|
import com.ubisam.ubgitea.ui.components.EmptyCup
|
||||||
|
import com.ubisam.ubgitea.ui.components.FilterChip
|
||||||
import com.ubisam.ubgitea.ui.components.KindTile
|
import com.ubisam.ubgitea.ui.components.KindTile
|
||||||
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.components.TeaPullToRefreshBox
|
import com.ubisam.ubgitea.ui.components.TeaPullToRefreshBox
|
||||||
import com.ubisam.ubgitea.ui.components.UBIcon
|
import com.ubisam.ubgitea.ui.components.UBIcon
|
||||||
@ -147,7 +147,7 @@ private fun WorkList(
|
|||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
TeaPullToRefreshBox(onRefresh = onRefresh, modifier = Modifier.fillMaxSize()) {
|
TeaPullToRefreshBox(onRefresh = onRefresh, modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
||||||
items(
|
items(
|
||||||
count = state.items.size,
|
count = state.items.size,
|
||||||
key = { index -> state.items[index].let { "${it.repository.owner}/${it.repository.name}#${it.number}" } },
|
key = { index -> state.items[index].let { "${it.repository.owner}/${it.repository.name}#${it.number}" } },
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@ -68,6 +67,7 @@ import com.ubisam.ubgitea.ui.components.ListCard
|
|||||||
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
import com.ubisam.ubgitea.ui.components.ListSkeleton
|
||||||
import com.ubisam.ubgitea.ui.components.MonoChip
|
import com.ubisam.ubgitea.ui.components.MonoChip
|
||||||
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
import com.ubisam.ubgitea.ui.components.ScreenTopBar
|
||||||
|
import com.ubisam.ubgitea.ui.components.ScrollTopLazyColumn
|
||||||
import com.ubisam.ubgitea.ui.components.SectionTitle
|
import com.ubisam.ubgitea.ui.components.SectionTitle
|
||||||
import com.ubisam.ubgitea.ui.components.StatusBadge
|
import com.ubisam.ubgitea.ui.components.StatusBadge
|
||||||
import com.ubisam.ubgitea.ui.components.TabGlide
|
import com.ubisam.ubgitea.ui.components.TabGlide
|
||||||
@ -183,7 +183,7 @@ private fun ColumnScope.ListBody(
|
|||||||
ListSkeleton()
|
ListSkeleton()
|
||||||
}
|
}
|
||||||
is WorkItemsUiState.IssueList -> {
|
is WorkItemsUiState.IssueList -> {
|
||||||
LazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
ScrollTopLazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
||||||
itemsIndexed(state.items, key = { _, issue -> issue.number }) { index, issue ->
|
itemsIndexed(state.items, key = { _, issue -> issue.number }) { index, issue ->
|
||||||
WorkItemRow(
|
WorkItemRow(
|
||||||
icon = R.drawable.ic_ub_issue,
|
icon = R.drawable.ic_ub_issue,
|
||||||
@ -203,7 +203,7 @@ private fun ColumnScope.ListBody(
|
|||||||
PageFooter(state.page, state.page > 1, state.hasNextPage, state.totalPages, onPreviousPage, onNextPage, onGoToPage)
|
PageFooter(state.page, state.page > 1, state.hasNextPage, state.totalPages, onPreviousPage, onNextPage, onGoToPage)
|
||||||
}
|
}
|
||||||
is WorkItemsUiState.PullRequestList -> {
|
is WorkItemsUiState.PullRequestList -> {
|
||||||
LazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
ScrollTopLazyColumn(Modifier.weight(1f), contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp)) {
|
||||||
itemsIndexed(state.items, key = { _, pr -> pr.number }) { index, pr ->
|
itemsIndexed(state.items, key = { _, pr -> pr.number }) { index, pr ->
|
||||||
WorkItemRow(
|
WorkItemRow(
|
||||||
icon = if (pr.state == PullRequestState.MERGED) R.drawable.ic_ub_merge else R.drawable.ic_ub_pull_request,
|
icon = if (pr.state == PullRequestState.MERGED) R.drawable.ic_ub_merge else R.drawable.ic_ub_pull_request,
|
||||||
@ -341,7 +341,7 @@ private fun WorkItemRow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IssueDetailView(state: WorkItemsUiState.IssueDetailContent) {
|
private fun IssueDetailView(state: WorkItemsUiState.IssueDetailContent) {
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
||||||
item(key = "header") {
|
item(key = "header") {
|
||||||
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
@ -395,7 +395,7 @@ private fun IssueDetailView(state: WorkItemsUiState.IssueDetailContent) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PullRequestDetailView(state: WorkItemsUiState.PullRequestDetailContent) {
|
private fun PullRequestDetailView(state: WorkItemsUiState.PullRequestDetailContent) {
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
ScrollTopLazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp)) {
|
||||||
item(key = "header") {
|
item(key = "header") {
|
||||||
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
ListCard(Modifier.padding(horizontal = 20.dp)) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
|||||||
39
app/src/main/java/com/ubisam/ubgitea/ui/components/Chips.kt
Normal file
39
app/src/main/java/com/ubisam/ubgitea/ui/components/Chips.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package com.ubisam.ubgitea.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.material3.FilterChip as MaterialFilterChip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app's filter chip. A selected chip is outlined in the brand colour so the choice is visible
|
||||||
|
* on a white surface.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FilterChip(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
trailingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
MaterialFilterChip(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
label = label,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon,
|
||||||
|
border = FilterChipDefaults.filterChipBorder(
|
||||||
|
enabled = enabled,
|
||||||
|
selected = selected,
|
||||||
|
selectedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
selectedBorderWidth = 1.5.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -178,8 +178,13 @@ fun CenteredLoading(message: String?, modifier: Modifier = Modifier) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TeaCupLoader()
|
// Most loads finish before this would be noticed; the cup is for the ones that do not.
|
||||||
if (message != null) Text(message, modifier = Modifier.padding(top = 16.dp), textAlign = TextAlign.Center)
|
AfterShortWait {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
TeaCupLoader()
|
||||||
|
if (message != null) Text(message, modifier = Modifier.padding(top = 16.dp), textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
package com.ubisam.ubgitea.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.ubisam.ubgitea.R
|
||||||
|
import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Leave the app?" confirmation shown when Back is pressed on the Home tab. Staying is the default:
|
||||||
|
* Back, a tap outside and the emphasised "계속 사용" button all call [onStay]; only "종료" calls [onExit].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ExitConfirmDialog(onStay: () -> Unit, onExit: () -> Unit) {
|
||||||
|
// Entrance: the card grows from 0.92 to 1 while fading in.
|
||||||
|
val progress = remember { Animatable(0f) }
|
||||||
|
LaunchedEffect(Unit) { progress.animateTo(1f, tween180) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onStay) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 340.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
val scale = 0.92f + 0.08f * progress.value
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
.alpha(progress.value),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 6.dp,
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(84.dp)
|
||||||
|
.background(UBGiteaTheme.colors.brand.copy(alpha = 0.12f), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
TeaCupLoader(size = 40.dp)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.shell_exit_title),
|
||||||
|
modifier = Modifier.padding(top = 20.dp),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.shell_exit_body),
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onExit,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.shell_exit_confirm))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = onStay,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.shell_exit_stay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tween180 = androidx.compose.animation.core.tween<Float>(durationMillis = 180)
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.ubisam.ubgitea.ui.components
|
package com.ubisam.ubgitea.ui.components
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
@ -9,6 +10,7 @@ import androidx.compose.animation.core.Spring
|
|||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
@ -124,6 +126,22 @@ private fun rememberSteam(): () -> Float {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The tea cup with rising steam: the app's "working on it" mark. */
|
/** The tea cup with rising steam: the app's "working on it" mark. */
|
||||||
|
/**
|
||||||
|
* Holds [content] back for [LoaderDelayMillis] and then fades it in. A wait shorter than that
|
||||||
|
* shows nothing, so a quick load goes straight to what was asked for.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AfterShortWait(content: @Composable () -> Unit) {
|
||||||
|
val shown = remember { Animatable(0f) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(LoaderDelayMillis)
|
||||||
|
shown.animateTo(1f, tween(180))
|
||||||
|
}
|
||||||
|
Box(Modifier.graphicsLayer { alpha = shown.value }) { content() }
|
||||||
|
}
|
||||||
|
|
||||||
|
const val LoaderDelayMillis = 450L
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TeaCupLoader(modifier: Modifier = Modifier, size: Dp = 48.dp, color: Color = UBGiteaTheme.colors.brand) {
|
fun TeaCupLoader(modifier: Modifier = Modifier, size: Dp = 48.dp, color: Color = UBGiteaTheme.colors.brand) {
|
||||||
val steam = rememberSteam()
|
val steam = rememberSteam()
|
||||||
@ -309,6 +327,7 @@ fun <S> TabGlide(
|
|||||||
val glide = tween<IntOffset>(TabGlideMillis, easing = FastOutSlowInEasing)
|
val glide = tween<IntOffset>(TabGlideMillis, easing = FastOutSlowInEasing)
|
||||||
slideInHorizontally(glide) { it * direction }
|
slideInHorizontally(glide) { it * direction }
|
||||||
.togetherWith(slideOutHorizontally(glide) { -it * direction })
|
.togetherWith(slideOutHorizontally(glide) { -it * direction })
|
||||||
|
.using(NoResize)
|
||||||
},
|
},
|
||||||
contentKey = tabIndex,
|
contentKey = tabIndex,
|
||||||
label = "tab",
|
label = "tab",
|
||||||
@ -317,6 +336,13 @@ fun <S> TabGlide(
|
|||||||
|
|
||||||
const val TabGlideMillis = 320
|
const val TabGlideMillis = 320
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For content that changes inside a fixed area. Left to itself, AnimatedContent also animates its
|
||||||
|
* own size from the top left corner whenever the two sides measure differently for a frame, which
|
||||||
|
* turns a sideways move into a diagonal one.
|
||||||
|
*/
|
||||||
|
val NoResize: SizeTransform = SizeTransform(clip = true) { _, _ -> snap() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bottom tabs' screens, laid side by side in the order of [tabs]. Changing [current] glides
|
* The bottom tabs' screens, laid side by side in the order of [tabs]. Changing [current] glides
|
||||||
* to it past whatever lies between, a little longer for each tab passed. Only a tab press moves
|
* to it past whatever lies between, a little longer for each tab passed. Only a tab press moves
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.ubisam.ubgitea.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ubisam.ubgitea.R
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list that, once it has been scrolled a long way down, offers a small round button in its
|
||||||
|
* bottom corner that takes it back to the top. Takes what a LazyColumn takes.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ScrollTopLazyColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: LazyListState = rememberLazyListState(),
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||||
|
content: LazyListScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier) {
|
||||||
|
LazyColumn(Modifier.fillMaxSize(), state, contentPadding, verticalArrangement = verticalArrangement, content = content)
|
||||||
|
ScrollToTopButton(state, Modifier.align(Alignment.BottomEnd).padding(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScrollToTopButton(state: LazyListState, modifier: Modifier = Modifier) {
|
||||||
|
val far by remember(state) { derivedStateOf { state.firstVisibleItemIndex >= ScrollTopAfterRows } }
|
||||||
|
val shown by animateFloatAsState(if (far) 1f else 0f, tween(160), label = "top")
|
||||||
|
// Not composed at the top of a list, so it takes no taps and is not read out there.
|
||||||
|
if (shown == 0f) return
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Surface(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
// From very far down, start near the top: the glide stays short and still reads as one.
|
||||||
|
if (state.firstVisibleItemIndex > ScrollTopGlideRows) state.scrollToItem(ScrollTopGlideRows)
|
||||||
|
state.animateScrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = far,
|
||||||
|
shape = CircleShape,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||||
|
shadowElevation = 3.dp,
|
||||||
|
modifier = modifier.size(40.dp).graphicsLayer {
|
||||||
|
alpha = shown
|
||||||
|
scaleX = 0.8f + 0.2f * shown
|
||||||
|
scaleY = 0.8f + 0.2f * shown
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
UBIcon(R.drawable.ic_ub_chevron_up, stringResource(R.string.shell_scroll_to_top), size = 22.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ScrollTopAfterRows = 8
|
||||||
|
private const val ScrollTopGlideRows = 12
|
||||||
@ -12,4 +12,9 @@
|
|||||||
<string name="time_hours_ago">%1$d시간 전</string>
|
<string name="time_hours_ago">%1$d시간 전</string>
|
||||||
<string name="time_yesterday">어제</string>
|
<string name="time_yesterday">어제</string>
|
||||||
<string name="time_days_ago">%1$d일 전</string>
|
<string name="time_days_ago">%1$d일 전</string>
|
||||||
|
<string name="shell_exit_title">앱을 종료할까요?</string>
|
||||||
|
<string name="shell_exit_body">뒤로 가기를 실수로 누르셨다면 계속 사용을 눌러 주세요.</string>
|
||||||
|
<string name="shell_exit_confirm">종료</string>
|
||||||
|
<string name="shell_exit_stay">계속 사용</string>
|
||||||
|
<string name="shell_scroll_to_top">맨 위로</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -1642,3 +1642,18 @@ This is the append-only implementation and handoff record shared by Codex and Cl
|
|||||||
or its loading state if never opened); it is not loaded for the pass.
|
or its loading state if never opened); it is not loaded for the pass.
|
||||||
- Checks and results: the five required checks pass.
|
- Checks and results: the five required checks pass.
|
||||||
- Caution: not seen on a device.
|
- Caution: not seen on a device.
|
||||||
|
|
||||||
|
## 2026-10-12 (continued) — TASK-075: small interaction fixes
|
||||||
|
|
||||||
|
- Request: the product owner asked for a border on the selected timeline chip, no tea cup on
|
||||||
|
short loads, a question before leaving from 홈, a button back to the top of long lists, and
|
||||||
|
said that sideways moves looked diagonal (top left to bottom right), also at launch.
|
||||||
|
- What changed: `Chips.kt` wraps Material's FilterChip with a selected border and the five
|
||||||
|
screens import it; `CenteredLoading` shows its cup only after 450 ms (`AfterShortWait`);
|
||||||
|
`ExitConfirmDialog` is shown by Back on 홈; `ScrollTopLazyColumn` replaces the LazyColumn of
|
||||||
|
the list screens; `NoResize` is applied to the route transition and to `TabGlide`.
|
||||||
|
- Design decision: the diagonal drift is attributed to AnimatedContent's default size animation,
|
||||||
|
which grows from the top left; this was not confirmed on a device before the change. The exit
|
||||||
|
question was designed in code in the app's style, not on the Claude Design canvas first.
|
||||||
|
- Agents: chips and the exit dialog were written by implementer (sonnet); the rest by main (opus).
|
||||||
|
- Checks and results: the five required checks pass.
|
||||||
|
|||||||
59
tasks/075-small-interaction-fixes.md
Normal file
59
tasks/075-small-interaction-fixes.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# TASK-075: Small interaction fixes from the product owner's first look
|
||||||
|
|
||||||
|
Status: implemented; required checks pass; installed on the product owner's device for a look
|
||||||
|
Owner: Claude
|
||||||
|
Reviewer: human product owner
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
1. A selected filter chip is outlined in the brand colour, everywhere chips are used.
|
||||||
|
2. A short load shows no tea cup: the cup appears only after 450 ms of waiting, so opening a
|
||||||
|
repository from 활동 중인 저장소 goes straight to the code when it loads quickly.
|
||||||
|
3. Back on 홈 asks before leaving the app (계속 사용 is the default; 종료 leaves).
|
||||||
|
4. A list scrolled a long way down offers a small round button that returns it to the top.
|
||||||
|
5. Screens that move sideways no longer drift diagonally: `AnimatedContent` is told not to animate
|
||||||
|
its own size (it does so from the top left corner).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Included: the five items above.
|
||||||
|
- Excluded: code, diff and compare panes (they keep their own scrolling); the loading skeletons.
|
||||||
|
|
||||||
|
## Writable paths
|
||||||
|
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/ui/components/Chips.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/ui/components/ExitConfirmDialog.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/ui/components/ScrollToTop.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/ui/components/Motion.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/ui/components/Elements.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt`
|
||||||
|
- `app/src/main/java/com/ubisam/ubgitea/feature/**/*Screen.kt`
|
||||||
|
- `app/src/main/res/values/strings_shell.xml`
|
||||||
|
- `tasks/075-small-interaction-fixes.md`
|
||||||
|
- `docs/development-log.md`
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] Each of the five items is in the code and the app builds.
|
||||||
|
- [x] Chip call sites are unchanged: only the import differs.
|
||||||
|
- [x] The exit question closes on Back or a tap outside, staying in the app.
|
||||||
|
- [x] The top button is not composed while a list is near its top.
|
||||||
|
- [ ] Seen on the product owner's device.
|
||||||
|
|
||||||
|
## Security and privacy
|
||||||
|
|
||||||
|
No additional concerns. No network, storage or credential code is touched.
|
||||||
|
|
||||||
|
## Required checks
|
||||||
|
|
||||||
|
```text
|
||||||
|
./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
See the development log entry for TASK-075.
|
||||||
|
|
||||||
|
## Review findings
|
||||||
|
|
||||||
|
None yet.
|
||||||
Loading…
x
Reference in New Issue
Block a user