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
|
package com.m57.hermescontrol.ui.bots
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
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.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -55,6 +59,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -82,6 +88,7 @@ fun BotsScreen(
|
|||||||
viewModel: BotsViewModel = viewModel { BotsViewModel() },
|
viewModel: BotsViewModel = viewModel { BotsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val pagerState = rememberPagerState(initialPage = state.selectedTab.ordinal) { BotsTab.entries.size }
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
var showCreateDialog by remember { mutableStateOf(false) }
|
var showCreateDialog by remember { mutableStateOf(false) }
|
||||||
var showCreateGroupDialog by remember { mutableStateOf(false) }
|
var showCreateGroupDialog by remember { mutableStateOf(false) }
|
||||||
@@ -89,6 +96,19 @@ fun BotsScreen(
|
|||||||
var disbandingGroup by remember { mutableStateOf<GroupInfo?>(null) }
|
var disbandingGroup by remember { mutableStateOf<GroupInfo?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadBots()
|
viewModel.loadBots()
|
||||||
}
|
}
|
||||||
@@ -176,6 +196,7 @@ fun BotsScreen(
|
|||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
drawerGesturesEnabled = pagerState.currentPage == 0,
|
||||||
title = {
|
title = {
|
||||||
if (isSearchActive) {
|
if (isSearchActive) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -270,18 +291,26 @@ fun BotsScreen(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
PrimaryTabRow(
|
PrimaryTabRow(
|
||||||
selectedTabIndex = state.selectedTab.ordinal,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Tab(
|
Tab(
|
||||||
selected = state.selectedTab == BotsTab.BOTS,
|
selected = pagerState.currentPage == BotsTab.BOTS.ordinal,
|
||||||
onClick = { viewModel.setSelectedTab(BotsTab.BOTS) },
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(BotsTab.BOTS.ordinal)
|
||||||
|
}
|
||||||
|
},
|
||||||
text = { Text(stringResource(R.string.bots_tab_all)) },
|
text = { Text(stringResource(R.string.bots_tab_all)) },
|
||||||
icon = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
icon = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||||
)
|
)
|
||||||
Tab(
|
Tab(
|
||||||
selected = state.selectedTab == BotsTab.GROUPS,
|
selected = pagerState.currentPage == BotsTab.GROUPS.ordinal,
|
||||||
onClick = { viewModel.setSelectedTab(BotsTab.GROUPS) },
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(BotsTab.GROUPS.ordinal)
|
||||||
|
}
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
if (state.allGroups.isNotEmpty()) {
|
if (state.allGroups.isNotEmpty()) {
|
||||||
@@ -295,120 +324,168 @@ fun BotsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (state.selectedTab) {
|
HorizontalPager(
|
||||||
BotsTab.BOTS -> {
|
state = pagerState,
|
||||||
when {
|
modifier = Modifier.fillMaxSize(),
|
||||||
state.isLoading && state.profiles.isEmpty() -> {
|
) { page ->
|
||||||
SkeletonListState()
|
when (BotsTab.entries.getOrElse(page) { BotsTab.BOTS }) {
|
||||||
}
|
BotsTab.BOTS -> {
|
||||||
|
Box(
|
||||||
state.errorMessage != null && state.profiles.isEmpty() -> {
|
modifier =
|
||||||
ErrorState(
|
Modifier
|
||||||
message = state.errorMessage ?: "",
|
.fillMaxSize()
|
||||||
onRetry = { viewModel.loadBots() },
|
.pointerInput(pagerState.currentPage, onOpenDrawer) {
|
||||||
)
|
if (pagerState.currentPage == 0) {
|
||||||
}
|
awaitEachGesture {
|
||||||
|
awaitFirstDown(pass = PointerEventPass.Initial)
|
||||||
state.displayProfiles.isEmpty() -> {
|
var totalDragX = 0f
|
||||||
EmptyState(
|
while (true) {
|
||||||
title = stringResource(R.string.bots_empty_title),
|
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||||
subtitle = stringResource(R.string.bots_empty_description),
|
val change = event.changes.firstOrNull() ?: break
|
||||||
modifier = Modifier.fillMaxSize(),
|
if (!change.pressed) break
|
||||||
)
|
val dx = change.position.x - change.previousPosition.x
|
||||||
}
|
if (dx > 0) {
|
||||||
|
totalDragX += dx
|
||||||
else -> {
|
if (totalDragX > 75f) {
|
||||||
LazyColumn(
|
onOpenDrawer?.invoke()
|
||||||
modifier = Modifier.fillMaxSize(),
|
break
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 24.dp),
|
}
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
} else if (dx < -10f) {
|
||||||
) {
|
break
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
state.isLoading && state.profiles.isEmpty() -> {
|
||||||
|
SkeletonListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
state.errorMessage != null && state.profiles.isEmpty() -> {
|
||||||
items = state.displayProfiles,
|
ErrorState(
|
||||||
key = { it.name },
|
message = state.errorMessage ?: "",
|
||||||
) { profile ->
|
onRetry = { viewModel.loadBots() },
|
||||||
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.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 -> {
|
BotsTab.GROUPS -> {
|
||||||
when {
|
when {
|
||||||
state.displayGroups.isEmpty() -> {
|
state.displayGroups.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = stringResource(R.string.bots_groups_empty_title),
|
title = stringResource(R.string.bots_groups_empty_title),
|
||||||
subtitle = stringResource(R.string.bots_groups_empty_desc),
|
subtitle = stringResource(R.string.bots_groups_empty_desc),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 24.dp),
|
contentPadding =
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
PaddingValues(
|
||||||
) {
|
start = 16.dp,
|
||||||
items(
|
end = 16.dp,
|
||||||
items = state.displayGroups,
|
top = 8.dp,
|
||||||
key = { it.name },
|
bottom = 24.dp,
|
||||||
) { group ->
|
),
|
||||||
GroupCard(
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
group = group,
|
) {
|
||||||
onClick = {
|
items(
|
||||||
if (group.members.isNotEmpty()) {
|
items = state.displayGroups,
|
||||||
NavigationController.navigateTo(
|
key = { it.name },
|
||||||
com.m57.hermescontrol.GroupChatKey(group.name),
|
) { group ->
|
||||||
)
|
GroupCard(
|
||||||
}
|
group = group,
|
||||||
},
|
onClick = {
|
||||||
onDisbandClick = {
|
if (group.members.isNotEmpty()) {
|
||||||
disbandingGroup = group
|
NavigationController.navigateTo(
|
||||||
},
|
com.m57.hermescontrol.GroupChatKey(group.name),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDisbandClick = {
|
||||||
|
disbandingGroup = group
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user