pkg: restructure BottomAppBars into separate files in a child package
This commit is contained in:
parent
0bcb182d17
commit
3c539dc146
|
|
@ -38,11 +38,11 @@ import com.example.llama.revamp.navigation.NavigationActions
|
|||
import com.example.llama.revamp.ui.scaffold.AnimatedNavHost
|
||||
import com.example.llama.revamp.ui.scaffold.AppNavigationDrawer
|
||||
import com.example.llama.revamp.ui.scaffold.AppScaffold
|
||||
import com.example.llama.revamp.ui.scaffold.BottomBarConfig
|
||||
import com.example.llama.revamp.ui.scaffold.NavigationIcon
|
||||
import com.example.llama.revamp.ui.scaffold.ScaffoldConfig
|
||||
import com.example.llama.revamp.ui.scaffold.ScaffoldEvent
|
||||
import com.example.llama.revamp.ui.scaffold.TopBarConfig
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.BottomBarConfig
|
||||
import com.example.llama.revamp.ui.screens.BenchmarkScreen
|
||||
import com.example.llama.revamp.ui.screens.ConversationScreen
|
||||
import com.example.llama.revamp.ui.screens.ModelLoadingScreen
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import androidx.compose.material3.SnackbarHost
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.BenchmarkBottomBar
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.BottomBarConfig
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.ConversationBottomBar
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.ModelSelectionBottomBar
|
||||
import com.example.llama.revamp.ui.scaffold.bottombar.ModelsManagementBottomBar
|
||||
|
||||
/**
|
||||
* Configuration of both [TopBarConfig] and [BottomBarConfig]
|
||||
|
|
|
|||
|
|
@ -1,612 +0,0 @@
|
|||
package com.example.llama.revamp.ui.scaffold
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.automirrored.outlined.Backspace
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.outlined.AddPhotoAlternate
|
||||
import androidx.compose.material.icons.outlined.AttachFile
|
||||
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.FilterAltOff
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.BottomAppBarDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.R
|
||||
import com.example.llama.revamp.APP_NAME
|
||||
import com.example.llama.revamp.data.model.ModelFilter
|
||||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
||||
|
||||
/**
|
||||
* [BottomAppBar] configurations
|
||||
*/
|
||||
sealed class BottomBarConfig {
|
||||
|
||||
object None : BottomBarConfig()
|
||||
|
||||
data class ModelSelection(
|
||||
val search: SearchConfig,
|
||||
val sorting: SortingConfig,
|
||||
val filtering: FilteringConfig,
|
||||
val runAction: RunActionConfig
|
||||
) : BottomBarConfig() {
|
||||
data class SearchConfig(
|
||||
val isActive: Boolean,
|
||||
val onToggleSearch: (Boolean) -> Unit,
|
||||
val textFieldState: TextFieldState,
|
||||
val onSearch: (String) -> Unit,
|
||||
)
|
||||
|
||||
data class SortingConfig(
|
||||
val currentOrder: ModelSortOrder,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val selectOrder: (ModelSortOrder) -> Unit
|
||||
)
|
||||
|
||||
data class FilteringConfig(
|
||||
val isActive: Boolean,
|
||||
val filters: Map<ModelFilter, Boolean>,
|
||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||
val onClearFilters: () -> Unit,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
data class RunActionConfig(
|
||||
val selectedModel: ModelInfo?,
|
||||
val onRun: (ModelInfo) -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
data class ModelsManagement(
|
||||
val sorting: SortingConfig,
|
||||
val filtering: FilteringConfig,
|
||||
val selection: SelectionConfig,
|
||||
val importing: ImportConfig
|
||||
) : BottomBarConfig() {
|
||||
data class SortingConfig(
|
||||
val currentOrder: ModelSortOrder,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val selectOrder: (ModelSortOrder) -> Unit
|
||||
)
|
||||
|
||||
data class FilteringConfig(
|
||||
val isActive: Boolean,
|
||||
val filters: Map<ModelFilter, Boolean>,
|
||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||
val onClearFilters: () -> Unit,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
data class SelectionConfig(
|
||||
val isActive: Boolean,
|
||||
val toggleMode: (Boolean) -> Unit,
|
||||
val selectedModels: Map<String, ModelInfo>,
|
||||
val toggleAllSelection: (Boolean) -> Unit,
|
||||
val deleteSelected: () -> Unit
|
||||
)
|
||||
|
||||
data class ImportConfig(
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val importFromLocal: () -> Unit,
|
||||
val importFromHuggingFace: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
data class Benchmark(
|
||||
val engineIdle: Boolean,
|
||||
val onRerun: () -> Unit,
|
||||
val onShare: () -> Unit,
|
||||
) : BottomBarConfig()
|
||||
|
||||
data class Conversation(
|
||||
val isEnabled: Boolean,
|
||||
val textFieldState: TextFieldState,
|
||||
val onSendClick: () -> Unit,
|
||||
val onAttachPhotoClick: () -> Unit,
|
||||
val onAttachFileClick: () -> Unit,
|
||||
val onAudioInputClick: () -> Unit,
|
||||
) : BottomBarConfig()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModelSelectionBottomBar(
|
||||
search: BottomBarConfig.ModelSelection.SearchConfig,
|
||||
sorting: BottomBarConfig.ModelSelection.SortingConfig,
|
||||
filtering: BottomBarConfig.ModelSelection.FilteringConfig,
|
||||
runAction: BottomBarConfig.ModelSelection.RunActionConfig
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (search.isActive) {
|
||||
// Quit search action
|
||||
IconButton(onClick = { search.onToggleSearch(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SearchOff,
|
||||
contentDescription = "Quit search mode"
|
||||
)
|
||||
}
|
||||
|
||||
// Clear query action
|
||||
IconButton(onClick = { search.textFieldState.clearText() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Backspace,
|
||||
contentDescription = "Clear query text"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Enter search action
|
||||
IconButton(onClick = { search.onToggleSearch(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting action
|
||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = sorting.isMenuVisible,
|
||||
onDismissRequest = { sorting.toggleMenu(false) }
|
||||
) {
|
||||
val sortOptions = listOf(
|
||||
Triple(ModelSortOrder.NAME_ASC, "Name (A-Z)", "Sort by name in ascending order"),
|
||||
Triple(ModelSortOrder.NAME_DESC, "Name (Z-A)", "Sort by name in descending order"),
|
||||
Triple(ModelSortOrder.SIZE_ASC, "Size (Smallest first)", "Sort by size in ascending order"),
|
||||
Triple(ModelSortOrder.SIZE_DESC, "Size (Largest first)", "Sort by size in descending order"),
|
||||
Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used")
|
||||
)
|
||||
|
||||
sortOptions.forEach { (order, label, description) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
trailingIcon = {
|
||||
if (sorting.currentOrder == order)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "$description, selected"
|
||||
)
|
||||
},
|
||||
onClick = { sorting.selectOrder(order) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter action
|
||||
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (filtering.isActive) Icons.Default.FilterAlt
|
||||
else Icons.Outlined.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
// Filter dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = filtering.isMenuVisible,
|
||||
onDismissRequest = { filtering.toggleMenu(false) }
|
||||
) {
|
||||
Text(
|
||||
text = "Filter by",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
filtering.filters.forEach { (filter, isEnabled) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.displayName) },
|
||||
leadingIcon = {
|
||||
Checkbox(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
onClick = { filtering.onToggleFilter(filter, !isEnabled) }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear filters") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAltOff,
|
||||
contentDescription = "Clear all filters"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
filtering.onClearFilters()
|
||||
filtering.toggleMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
// Only show FAB if a model is selected
|
||||
AnimatedVisibility(
|
||||
visible = runAction.selectedModel != null,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut()
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = { runAction.selectedModel?.let { runAction.onRun(it) } },
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Run with selected model"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModelsManagementBottomBar(
|
||||
sorting: BottomBarConfig.ModelsManagement.SortingConfig,
|
||||
filtering: BottomBarConfig.ModelsManagement.FilteringConfig,
|
||||
selection: BottomBarConfig.ModelsManagement.SelectionConfig,
|
||||
importing: BottomBarConfig.ModelsManagement.ImportConfig
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (selection.isActive) {
|
||||
/* Multi-selection mode actions */
|
||||
IconButton(
|
||||
onClick = selection.deleteSelected,
|
||||
enabled = selection.selectedModels.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete selected",
|
||||
tint = if (selection.selectedModels.isNotEmpty())
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Default mode actions */
|
||||
|
||||
// Multi-selection action
|
||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting action
|
||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = sorting.isMenuVisible,
|
||||
onDismissRequest = { sorting.toggleMenu(false) }
|
||||
) {
|
||||
val sortOptions = listOf(
|
||||
Triple(ModelSortOrder.NAME_ASC, "Name (A-Z)", "Sort by name in ascending order"),
|
||||
Triple(ModelSortOrder.NAME_DESC, "Name (Z-A)", "Sort by name in descending order"),
|
||||
Triple(ModelSortOrder.SIZE_ASC, "Size (Smallest first)", "Sort by size in ascending order"),
|
||||
Triple(ModelSortOrder.SIZE_DESC, "Size (Largest first)", "Sort by size in descending order"),
|
||||
Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used")
|
||||
)
|
||||
|
||||
sortOptions.forEach { (order, label, description) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
trailingIcon = {
|
||||
if (sorting.currentOrder == order)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "$description, selected"
|
||||
)
|
||||
},
|
||||
onClick = { sorting.selectOrder(order) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Filtering action
|
||||
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (filtering.isActive) Icons.Default.FilterAlt
|
||||
else Icons.Outlined.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
// Filter dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = filtering.isMenuVisible,
|
||||
onDismissRequest = { filtering.toggleMenu(false) }
|
||||
) {
|
||||
Text(
|
||||
text = "Filter by",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
filtering.filters.forEach { (filter, isEnabled) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.displayName) },
|
||||
leadingIcon = {
|
||||
Checkbox(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
onClick = { filtering.onToggleFilter(filter, !isEnabled) }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear filters") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAltOff,
|
||||
contentDescription = "Clear all filters"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
filtering.onClearFilters()
|
||||
filtering.toggleMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu(true) },
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add,
|
||||
contentDescription = if (selection.isActive) "Exit selection mode" else "Add model"
|
||||
)
|
||||
}
|
||||
|
||||
// Add model dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = importing.isMenuVisible,
|
||||
onDismissRequest = { importing.toggleMenu(false) }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Import local model") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = "Import a local model on the device"
|
||||
)
|
||||
},
|
||||
onClick = importing.importFromLocal
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Download from HuggingFace") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.logo_huggingface),
|
||||
contentDescription = "Browse and download a model from HuggingFace",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
onClick = importing.importFromHuggingFace
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BenchmarkBottomBar(
|
||||
engineIdle: Boolean,
|
||||
onRerun: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = onRerun) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Replay,
|
||||
contentDescription = "Run the benchmark again",
|
||||
tint =
|
||||
if (engineIdle) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
// Only show FAB if the benchmark result is ready
|
||||
AnimatedVisibility(
|
||||
visible = engineIdle,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut()
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = onShare,
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "Share the benchmark results"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConversationBottomBar(
|
||||
textFieldState: TextFieldState,
|
||||
isReady: Boolean,
|
||||
onSendClick: () -> Unit,
|
||||
onAttachPhotoClick: () -> Unit,
|
||||
onAttachFileClick: () -> Unit,
|
||||
onAudioInputClick: () -> Unit,
|
||||
) {
|
||||
val placeholder = if (isReady) "Message $APP_NAME..." else "Please wait for $APP_NAME to finish"
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = BottomAppBarDefaults.containerColor,
|
||||
tonalElevation = BottomAppBarDefaults.ContainerElevation,
|
||||
shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 16.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
state = textFieldState,
|
||||
modifier = Modifier.fillMaxWidth().padding(end = 8.dp),
|
||||
enabled = isReady,
|
||||
placeholder = { Text(placeholder) },
|
||||
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceDim,
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||
onKeyboardAction = { if (isReady) { onSendClick() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = onAttachPhotoClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddPhotoAlternate,
|
||||
contentDescription = "Attach a photo",
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAttachFileClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AttachFile,
|
||||
contentDescription = "Attach a file",
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAudioInputClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Mic,
|
||||
contentDescription = "Input with voice",
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { if (isReady) { onSendClick() } },
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
if (isReady) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||
contentDescription = "Send message",
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeCap = StrokeCap.Round,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.example.llama.revamp.ui.scaffold.bottombar
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun BenchmarkBottomBar(
|
||||
engineIdle: Boolean,
|
||||
onRerun: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = onRerun) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Replay,
|
||||
contentDescription = "Run the benchmark again",
|
||||
tint =
|
||||
if (engineIdle) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
// Only show FAB if the benchmark result is ready
|
||||
AnimatedVisibility(
|
||||
visible = engineIdle,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut()
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = onShare,
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "Share the benchmark results"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.example.llama.revamp.ui.scaffold.bottombar
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import com.example.llama.revamp.data.model.ModelFilter
|
||||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
||||
|
||||
/**
|
||||
* [BottomAppBar] configurations
|
||||
*/
|
||||
sealed class BottomBarConfig {
|
||||
|
||||
object None : BottomBarConfig()
|
||||
|
||||
data class ModelSelection(
|
||||
val search: SearchConfig,
|
||||
val sorting: SortingConfig,
|
||||
val filtering: FilteringConfig,
|
||||
val runAction: RunActionConfig
|
||||
) : BottomBarConfig() {
|
||||
data class SearchConfig(
|
||||
val isActive: Boolean,
|
||||
val onToggleSearch: (Boolean) -> Unit,
|
||||
val textFieldState: TextFieldState,
|
||||
val onSearch: (String) -> Unit,
|
||||
)
|
||||
|
||||
data class SortingConfig(
|
||||
val currentOrder: ModelSortOrder,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val selectOrder: (ModelSortOrder) -> Unit
|
||||
)
|
||||
|
||||
data class FilteringConfig(
|
||||
val isActive: Boolean,
|
||||
val filters: Map<ModelFilter, Boolean>,
|
||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||
val onClearFilters: () -> Unit,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
data class RunActionConfig(
|
||||
val selectedModel: ModelInfo?,
|
||||
val onRun: (ModelInfo) -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
data class ModelsManagement(
|
||||
val sorting: SortingConfig,
|
||||
val filtering: FilteringConfig,
|
||||
val selection: SelectionConfig,
|
||||
val importing: ImportConfig
|
||||
) : BottomBarConfig() {
|
||||
data class SortingConfig(
|
||||
val currentOrder: ModelSortOrder,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val selectOrder: (ModelSortOrder) -> Unit
|
||||
)
|
||||
|
||||
data class FilteringConfig(
|
||||
val isActive: Boolean,
|
||||
val filters: Map<ModelFilter, Boolean>,
|
||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||
val onClearFilters: () -> Unit,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
data class SelectionConfig(
|
||||
val isActive: Boolean,
|
||||
val toggleMode: (Boolean) -> Unit,
|
||||
val selectedModels: Map<String, ModelInfo>,
|
||||
val toggleAllSelection: (Boolean) -> Unit,
|
||||
val deleteSelected: () -> Unit
|
||||
)
|
||||
|
||||
data class ImportConfig(
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit,
|
||||
val importFromLocal: () -> Unit,
|
||||
val importFromHuggingFace: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
data class Benchmark(
|
||||
val engineIdle: Boolean,
|
||||
val onRerun: () -> Unit,
|
||||
val onShare: () -> Unit,
|
||||
) : BottomBarConfig()
|
||||
|
||||
data class Conversation(
|
||||
val isEnabled: Boolean,
|
||||
val textFieldState: TextFieldState,
|
||||
val onSendClick: () -> Unit,
|
||||
val onAttachPhotoClick: () -> Unit,
|
||||
val onAttachFileClick: () -> Unit,
|
||||
val onAudioInputClick: () -> Unit,
|
||||
) : BottomBarConfig()
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.example.llama.revamp.ui.scaffold.bottombar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.outlined.AddPhotoAlternate
|
||||
import androidx.compose.material.icons.outlined.AttachFile
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.BottomAppBarDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.revamp.APP_NAME
|
||||
|
||||
@Composable
|
||||
fun ConversationBottomBar(
|
||||
textFieldState: TextFieldState,
|
||||
isReady: Boolean,
|
||||
onSendClick: () -> Unit,
|
||||
onAttachPhotoClick: () -> Unit,
|
||||
onAttachFileClick: () -> Unit,
|
||||
onAudioInputClick: () -> Unit,
|
||||
) {
|
||||
val placeholder = if (isReady) "Message ${APP_NAME}..." else "Please wait for ${APP_NAME} to finish"
|
||||
|
||||
Column(
|
||||
modifier = Modifier.Companion.fillMaxWidth()
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
color = BottomAppBarDefaults.containerColor,
|
||||
tonalElevation = BottomAppBarDefaults.ContainerElevation,
|
||||
shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.Companion.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 16.dp, end = 16.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
state = textFieldState,
|
||||
modifier = Modifier.Companion.fillMaxWidth().padding(end = 8.dp),
|
||||
enabled = isReady,
|
||||
placeholder = { Text(placeholder) },
|
||||
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceDim,
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Companion.Send),
|
||||
onKeyboardAction = {
|
||||
if (isReady) {
|
||||
onSendClick()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = onAttachPhotoClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddPhotoAlternate,
|
||||
contentDescription = "Attach a photo",
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAttachFileClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AttachFile,
|
||||
contentDescription = "Attach a file",
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onAudioInputClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Mic,
|
||||
contentDescription = "Input with voice",
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (isReady) {
|
||||
onSendClick()
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
if (isReady) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||
contentDescription = "Send message",
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.Companion.size(24.dp),
|
||||
strokeCap = StrokeCap.Companion.Round,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.example.llama.revamp.ui.scaffold.bottombar
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.automirrored.outlined.Backspace
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.FilterAltOff
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
||||
|
||||
@Composable
|
||||
fun ModelSelectionBottomBar(
|
||||
search: BottomBarConfig.ModelSelection.SearchConfig,
|
||||
sorting: BottomBarConfig.ModelSelection.SortingConfig,
|
||||
filtering: BottomBarConfig.ModelSelection.FilteringConfig,
|
||||
runAction: BottomBarConfig.ModelSelection.RunActionConfig
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (search.isActive) {
|
||||
// Quit search action
|
||||
IconButton(onClick = { search.onToggleSearch(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SearchOff,
|
||||
contentDescription = "Quit search mode"
|
||||
)
|
||||
}
|
||||
|
||||
// Clear query action
|
||||
IconButton(onClick = { search.textFieldState.clearText() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Backspace,
|
||||
contentDescription = "Clear query text"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Enter search action
|
||||
IconButton(onClick = { search.onToggleSearch(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting action
|
||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = sorting.isMenuVisible,
|
||||
onDismissRequest = { sorting.toggleMenu(false) }
|
||||
) {
|
||||
val sortOptions = listOf(
|
||||
Triple(
|
||||
ModelSortOrder.NAME_ASC,
|
||||
"Name (A-Z)",
|
||||
"Sort by name in ascending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.NAME_DESC,
|
||||
"Name (Z-A)",
|
||||
"Sort by name in descending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.SIZE_ASC,
|
||||
"Size (Smallest first)",
|
||||
"Sort by size in ascending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.SIZE_DESC,
|
||||
"Size (Largest first)",
|
||||
"Sort by size in descending order"
|
||||
),
|
||||
Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used")
|
||||
)
|
||||
|
||||
sortOptions.forEach { (order, label, description) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
trailingIcon = {
|
||||
if (sorting.currentOrder == order)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "$description, selected"
|
||||
)
|
||||
},
|
||||
onClick = { sorting.selectOrder(order) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter action
|
||||
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (filtering.isActive) Icons.Default.FilterAlt
|
||||
else Icons.Outlined.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
// Filter dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = filtering.isMenuVisible,
|
||||
onDismissRequest = { filtering.toggleMenu(false) }
|
||||
) {
|
||||
Text(
|
||||
text = "Filter by",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.Companion.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
filtering.filters.forEach { (filter, isEnabled) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.displayName) },
|
||||
leadingIcon = {
|
||||
Checkbox(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
onClick = { filtering.onToggleFilter(filter, !isEnabled) }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear filters") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAltOff,
|
||||
contentDescription = "Clear all filters"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
filtering.onClearFilters()
|
||||
filtering.toggleMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
// Only show FAB if a model is selected
|
||||
AnimatedVisibility(
|
||||
visible = runAction.selectedModel != null,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut()
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = { runAction.selectedModel?.let { runAction.onRun(it) } },
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Run with selected model"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
package com.example.llama.revamp.ui.scaffold.bottombar
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.FilterAltOff
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.R
|
||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
||||
|
||||
@Composable
|
||||
fun ModelsManagementBottomBar(
|
||||
sorting: BottomBarConfig.ModelsManagement.SortingConfig,
|
||||
filtering: BottomBarConfig.ModelsManagement.FilteringConfig,
|
||||
selection: BottomBarConfig.ModelsManagement.SelectionConfig,
|
||||
importing: BottomBarConfig.ModelsManagement.ImportConfig
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (selection.isActive) {
|
||||
/* Multi-selection mode actions */
|
||||
IconButton(
|
||||
onClick = selection.deleteSelected,
|
||||
enabled = selection.selectedModels.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete selected",
|
||||
tint = if (selection.selectedModels.isNotEmpty())
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Default mode actions */
|
||||
|
||||
// Multi-selection action
|
||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting action
|
||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = sorting.isMenuVisible,
|
||||
onDismissRequest = { sorting.toggleMenu(false) }
|
||||
) {
|
||||
val sortOptions = listOf(
|
||||
Triple(
|
||||
ModelSortOrder.NAME_ASC,
|
||||
"Name (A-Z)",
|
||||
"Sort by name in ascending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.NAME_DESC,
|
||||
"Name (Z-A)",
|
||||
"Sort by name in descending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.SIZE_ASC,
|
||||
"Size (Smallest first)",
|
||||
"Sort by size in ascending order"
|
||||
),
|
||||
Triple(
|
||||
ModelSortOrder.SIZE_DESC,
|
||||
"Size (Largest first)",
|
||||
"Sort by size in descending order"
|
||||
),
|
||||
Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used")
|
||||
)
|
||||
|
||||
sortOptions.forEach { (order, label, description) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
trailingIcon = {
|
||||
if (sorting.currentOrder == order)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "$description, selected"
|
||||
)
|
||||
},
|
||||
onClick = { sorting.selectOrder(order) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Filtering action
|
||||
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (filtering.isActive) Icons.Default.FilterAlt
|
||||
else Icons.Outlined.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
// Filter dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = filtering.isMenuVisible,
|
||||
onDismissRequest = { filtering.toggleMenu(false) }
|
||||
) {
|
||||
Text(
|
||||
text = "Filter by",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.Companion.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
filtering.filters.forEach { (filter, isEnabled) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.displayName) },
|
||||
leadingIcon = {
|
||||
Checkbox(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
onClick = { filtering.onToggleFilter(filter, !isEnabled) }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear filters") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAltOff,
|
||||
contentDescription = "Clear all filters"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
filtering.onClearFilters()
|
||||
filtering.toggleMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu(
|
||||
true
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add,
|
||||
contentDescription = if (selection.isActive) "Exit selection mode" else "Add model"
|
||||
)
|
||||
}
|
||||
|
||||
// Add model dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = importing.isMenuVisible,
|
||||
onDismissRequest = { importing.toggleMenu(false) }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Import local model") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = "Import a local model on the device"
|
||||
)
|
||||
},
|
||||
onClick = importing.importFromLocal
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Download from HuggingFace") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.logo_huggingface),
|
||||
contentDescription = "Browse and download a model from HuggingFace",
|
||||
modifier = Modifier.Companion.size(24.dp),
|
||||
tint = Color.Companion.Unspecified,
|
||||
)
|
||||
},
|
||||
onClick = importing.importFromHuggingFace
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue