feat(bots): horizontal swipe between bots and groups tabs with drawer gesture gating (#1011)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
2f3bf5114d
commit
bd4a73dd48
1 file changed
+185
-108
@@ -1,6 +1,8 @@
|
||||
package com.m57.hermescontrol.ui.bots
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -16,6 +18,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -55,6 +59,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -82,6 +88,7 @@ fun BotsScreen(
|
||||
viewModel: BotsViewModel = viewModel { BotsViewModel() },
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val pagerState = rememberPagerState(initialPage = state.selectedTab.ordinal) { BotsTab.entries.size }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
var showCreateGroupDialog by remember { mutableStateOf(false) }
|
||||
@@ -89,6 +96,19 @@ fun BotsScreen(
|
||||
var disbandingGroup by remember { mutableStateOf<GroupInfo?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
val targetTab = BotsTab.entries.getOrNull(pagerState.currentPage) ?: BotsTab.BOTS
|
||||
if (state.selectedTab != targetTab) {
|
||||
viewModel.setSelectedTab(targetTab)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.selectedTab) {
|
||||
if (pagerState.currentPage != state.selectedTab.ordinal) {
|
||||
pagerState.animateScrollToPage(state.selectedTab.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadBots()
|
||||
}
|
||||
@@ -176,6 +196,7 @@ fun BotsScreen(
|
||||
|
||||
HermesScaffold(
|
||||
modifier = modifier,
|
||||
drawerGesturesEnabled = pagerState.currentPage == 0,
|
||||
title = {
|
||||
if (isSearchActive) {
|
||||
OutlinedTextField(
|
||||
@@ -270,18 +291,26 @@ fun BotsScreen(
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = state.selectedTab.ordinal,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Tab(
|
||||
selected = state.selectedTab == BotsTab.BOTS,
|
||||
onClick = { viewModel.setSelectedTab(BotsTab.BOTS) },
|
||||
selected = pagerState.currentPage == BotsTab.BOTS.ordinal,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(BotsTab.BOTS.ordinal)
|
||||
}
|
||||
},
|
||||
text = { Text(stringResource(R.string.bots_tab_all)) },
|
||||
icon = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
)
|
||||
Tab(
|
||||
selected = state.selectedTab == BotsTab.GROUPS,
|
||||
onClick = { viewModel.setSelectedTab(BotsTab.GROUPS) },
|
||||
selected = pagerState.currentPage == BotsTab.GROUPS.ordinal,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(BotsTab.GROUPS.ordinal)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
if (state.allGroups.isNotEmpty()) {
|
||||
@@ -295,120 +324,168 @@ fun BotsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
when (state.selectedTab) {
|
||||
BotsTab.BOTS -> {
|
||||
when {
|
||||
state.isLoading && state.profiles.isEmpty() -> {
|
||||
SkeletonListState()
|
||||
}
|
||||
|
||||
state.errorMessage != null && state.profiles.isEmpty() -> {
|
||||
ErrorState(
|
||||
message = state.errorMessage ?: "",
|
||||
onRetry = { viewModel.loadBots() },
|
||||
)
|
||||
}
|
||||
|
||||
state.displayProfiles.isEmpty() -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.bots_empty_title),
|
||||
subtitle = stringResource(R.string.bots_empty_description),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (state.activeNowBots.isNotEmpty() && !isSearchActive) {
|
||||
item(key = "active_now_strip") {
|
||||
ActiveNowStrip(
|
||||
activeBots = state.activeNowBots,
|
||||
onSelectBot = { bot ->
|
||||
scope.launch {
|
||||
viewModel.selectBot(bot)
|
||||
val canonicalId =
|
||||
bot.canonical_session?.resolved_id
|
||||
?: bot.canonical_session?.id
|
||||
if (!canonicalId.isNullOrBlank()) {
|
||||
NavigationController.openChatSession(canonicalId)
|
||||
} else {
|
||||
NavigationController.navigateTo(
|
||||
com.m57.hermescontrol.ChatScreen,
|
||||
)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { page ->
|
||||
when (BotsTab.entries.getOrElse(page) { BotsTab.BOTS }) {
|
||||
BotsTab.BOTS -> {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(pagerState.currentPage, onOpenDrawer) {
|
||||
if (pagerState.currentPage == 0) {
|
||||
awaitEachGesture {
|
||||
awaitFirstDown(pass = PointerEventPass.Initial)
|
||||
var totalDragX = 0f
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
val change = event.changes.firstOrNull() ?: break
|
||||
if (!change.pressed) break
|
||||
val dx = change.position.x - change.previousPosition.x
|
||||
if (dx > 0) {
|
||||
totalDragX += dx
|
||||
if (totalDragX > 75f) {
|
||||
onOpenDrawer?.invoke()
|
||||
break
|
||||
}
|
||||
} else if (dx < -10f) {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
when {
|
||||
state.isLoading && state.profiles.isEmpty() -> {
|
||||
SkeletonListState()
|
||||
}
|
||||
|
||||
items(
|
||||
items = state.displayProfiles,
|
||||
key = { it.name },
|
||||
) { profile ->
|
||||
BotCard(
|
||||
profile = profile,
|
||||
isActiveProfile = profile.name == state.activeProfileName,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.selectBot(profile)
|
||||
val canonicalId =
|
||||
profile.canonical_session?.resolved_id
|
||||
?: profile.canonical_session?.id
|
||||
if (!canonicalId.isNullOrBlank()) {
|
||||
NavigationController.openChatSession(canonicalId)
|
||||
} else {
|
||||
NavigationController.navigateTo(com.m57.hermescontrol.ChatScreen)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEditClick = {
|
||||
editingBot = profile
|
||||
},
|
||||
state.errorMessage != null && state.profiles.isEmpty() -> {
|
||||
ErrorState(
|
||||
message = state.errorMessage ?: "",
|
||||
onRetry = { viewModel.loadBots() },
|
||||
)
|
||||
}
|
||||
|
||||
state.displayProfiles.isEmpty() -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.bots_empty_title),
|
||||
subtitle = stringResource(R.string.bots_empty_description),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 8.dp,
|
||||
bottom = 24.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (state.activeNowBots.isNotEmpty() && !isSearchActive) {
|
||||
item(key = "active_now_strip") {
|
||||
ActiveNowStrip(
|
||||
activeBots = state.activeNowBots,
|
||||
onSelectBot = { bot ->
|
||||
scope.launch {
|
||||
viewModel.selectBot(bot)
|
||||
val canonicalId =
|
||||
bot.canonical_session?.resolved_id
|
||||
?: bot.canonical_session?.id
|
||||
if (!canonicalId.isNullOrBlank()) {
|
||||
NavigationController.openChatSession(canonicalId)
|
||||
} else {
|
||||
NavigationController.navigateTo(
|
||||
com.m57.hermescontrol.ChatScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = state.displayProfiles,
|
||||
key = { it.name },
|
||||
) { profile ->
|
||||
BotCard(
|
||||
profile = profile,
|
||||
isActiveProfile = profile.name == state.activeProfileName,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.selectBot(profile)
|
||||
val canonicalId =
|
||||
profile.canonical_session?.resolved_id
|
||||
?: profile.canonical_session?.id
|
||||
if (!canonicalId.isNullOrBlank()) {
|
||||
NavigationController.openChatSession(canonicalId)
|
||||
} else {
|
||||
NavigationController.navigateTo(
|
||||
com.m57.hermescontrol.ChatScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEditClick = {
|
||||
editingBot = profile
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BotsTab.GROUPS -> {
|
||||
when {
|
||||
state.displayGroups.isEmpty() -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.bots_groups_empty_title),
|
||||
subtitle = stringResource(R.string.bots_groups_empty_desc),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
BotsTab.GROUPS -> {
|
||||
when {
|
||||
state.displayGroups.isEmpty() -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.bots_groups_empty_title),
|
||||
subtitle = stringResource(R.string.bots_groups_empty_desc),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(
|
||||
items = state.displayGroups,
|
||||
key = { it.name },
|
||||
) { group ->
|
||||
GroupCard(
|
||||
group = group,
|
||||
onClick = {
|
||||
if (group.members.isNotEmpty()) {
|
||||
NavigationController.navigateTo(
|
||||
com.m57.hermescontrol.GroupChatKey(group.name),
|
||||
)
|
||||
}
|
||||
},
|
||||
onDisbandClick = {
|
||||
disbandingGroup = group
|
||||
},
|
||||
)
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 8.dp,
|
||||
bottom = 24.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(
|
||||
items = state.displayGroups,
|
||||
key = { it.name },
|
||||
) { group ->
|
||||
GroupCard(
|
||||
group = group,
|
||||
onClick = {
|
||||
if (group.members.isNotEmpty()) {
|
||||
NavigationController.navigateTo(
|
||||
com.m57.hermescontrol.GroupChatKey(group.name),
|
||||
)
|
||||
}
|
||||
},
|
||||
onDisbandClick = {
|
||||
disbandingGroup = group
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user