[优化] 一些界面问题
This commit is contained in:
@@ -16,6 +16,7 @@ import io.legado.app.constant.NotificationId
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.receiver.NetworkChangedListener
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import io.legado.app.utils.eventBus.FlowEventBus
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefInt
|
||||
import io.legado.app.utils.postEvent
|
||||
@@ -108,6 +109,7 @@ class WebService : BaseService() {
|
||||
}
|
||||
startForegroundNotification()
|
||||
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
||||
FlowEventBus.post(EventBus.WEB_SERVICE, hostAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +143,7 @@ class WebService : BaseService() {
|
||||
webSocketServer?.stop()
|
||||
}
|
||||
postEvent(EventBus.WEB_SERVICE, "")
|
||||
FlowEventBus.post(EventBus.WEB_SERVICE, "")
|
||||
upTile(false)
|
||||
}
|
||||
|
||||
@@ -170,6 +173,7 @@ class WebService : BaseService() {
|
||||
hostAddress = notificationList.first()
|
||||
isRun = true
|
||||
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
||||
FlowEventBus.post(EventBus.WEB_SERVICE, hostAddress)
|
||||
startForegroundNotification()
|
||||
} catch (e: IOException) {
|
||||
toastOnUi(e.localizedMessage ?: "")
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Source
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material.icons.filled.Web
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -37,9 +36,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -67,7 +63,6 @@ fun MyScreen(
|
||||
) {
|
||||
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
@@ -126,35 +121,26 @@ fun MyScreen(
|
||||
}
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onEvent(PrefClickEvent.ToggleWebService)
|
||||
},
|
||||
onLongClick = {
|
||||
if (uiState.isWebServiceRun) {
|
||||
showMenu = true
|
||||
dropdownMenu = if (uiState.isWebServiceRun) {
|
||||
{ onDismiss ->
|
||||
DropdownMenuItem(
|
||||
text = { Text("复制地址") },
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("浏览器打开") },
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
onClick = { }
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("复制地址") },
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("浏览器打开") },
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SplicedColumnGroup(
|
||||
|
||||
@@ -2,10 +2,15 @@ package io.legado.app.ui.main.my
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.utils.eventBus.FlowEventBus
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class MyUiState(
|
||||
val isWebServiceRun: Boolean = false,
|
||||
@@ -17,7 +22,6 @@ sealed class PrefClickEvent {
|
||||
data class CopyUrl(val url: String) : PrefClickEvent()
|
||||
data class ShowMd(val title: String, val path: String) : PrefClickEvent()
|
||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||
object ShowWebServiceMenu : PrefClickEvent()
|
||||
object ToggleWebService : PrefClickEvent()
|
||||
object ExitApp : PrefClickEvent()
|
||||
}
|
||||
@@ -29,40 +33,40 @@ class MyViewModel(
|
||||
private val _uiState = MutableStateFlow(
|
||||
MyUiState(
|
||||
isWebServiceRun = WebService.isRun,
|
||||
webServiceAddress = if (WebService.isRun) {
|
||||
WebService.hostAddress
|
||||
} else ""
|
||||
webServiceAddress = WebService.hostAddress
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
FlowEventBus.with<String>(EventBus.WEB_SERVICE)
|
||||
.collect { address ->
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isWebServiceRun = address.isNotEmpty(),
|
||||
webServiceAddress = address
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onEvent(event: PrefClickEvent) {
|
||||
when (event) {
|
||||
|
||||
PrefClickEvent.ToggleWebService -> {
|
||||
val newRun = !_uiState.value.isWebServiceRun
|
||||
if (newRun) {
|
||||
val currentIsRun = _uiState.value.isWebServiceRun
|
||||
|
||||
if (!currentIsRun) {
|
||||
WebService.start(getApplication())
|
||||
} else {
|
||||
WebService.stop(getApplication())
|
||||
_uiState.update { it.copy(isWebServiceRun = false, webServiceAddress = "") }
|
||||
}
|
||||
updateWebServiceState()
|
||||
}
|
||||
|
||||
PrefClickEvent.ShowWebServiceMenu -> {
|
||||
// UI 自己处理 showMenu
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWebServiceState() {
|
||||
_uiState.value = MyUiState(
|
||||
isWebServiceRun = WebService.isRun,
|
||||
webServiceAddress = if (WebService.isRun) {
|
||||
WebService.hostAddress
|
||||
} else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.ui.replace.edit.ReplaceEditActivity
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.AnimatedText
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
import io.legado.app.ui.widget.components.SearchBarSection
|
||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||
@@ -494,139 +495,146 @@ fun ReplaceRuleScreen(
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 120.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(rules, key = { it.id }) { ui ->
|
||||
val isSelected = selectedRuleIds.contains(ui.id)
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = ui.id
|
||||
) { isDragging ->
|
||||
if (rules.isEmpty()) {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = "没有替换规则!"
|
||||
)
|
||||
} else {
|
||||
FastScrollLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 120.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(rules, key = { it.id }) { ui ->
|
||||
val isSelected = selectedRuleIds.contains(ui.id)
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = ui.id
|
||||
) { isDragging ->
|
||||
|
||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||
ReplaceRuleItem(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.zIndex(if (isDragging) 1f else 0f)
|
||||
.shadow(
|
||||
elevation = elevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
clip = false
|
||||
)
|
||||
.then(
|
||||
if (canReorder) {
|
||||
Modifier.longPressDraggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.animateItem(),
|
||||
name = ui.name,
|
||||
isEnabled = ui.isEnabled,
|
||||
isSelected = isSelected,
|
||||
inSelectionMode = inSelectionMode,
|
||||
onEnabledChange = { enabled ->
|
||||
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
||||
},
|
||||
onDelete = { showDeleteRuleDialog = ui.rule },
|
||||
onToTop = { viewModel.toTop(ui.rule) },
|
||||
onToBottom = { viewModel.toBottom(ui.rule) },
|
||||
onToggleSelection = {
|
||||
viewModel.toggleSelection(ui.id)
|
||||
},
|
||||
onClickEdit = {
|
||||
context.startActivity(
|
||||
ReplaceEditActivity.startIntent(context, ui.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||
ReplaceRuleItem(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.zIndex(if (isDragging) 1f else 0f)
|
||||
.shadow(
|
||||
elevation = elevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
clip = false
|
||||
)
|
||||
.then(
|
||||
if (canReorder) {
|
||||
Modifier.longPressDraggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.animateItem(),
|
||||
name = ui.name,
|
||||
isEnabled = ui.isEnabled,
|
||||
isSelected = isSelected,
|
||||
inSelectionMode = inSelectionMode,
|
||||
onEnabledChange = { enabled ->
|
||||
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
||||
},
|
||||
onDelete = { showDeleteRuleDialog = ui.rule },
|
||||
onToTop = { viewModel.toTop(ui.rule) },
|
||||
onToBottom = { viewModel.toBottom(ui.rule) },
|
||||
onToggleSelection = {
|
||||
viewModel.toggleSelection(ui.id)
|
||||
},
|
||||
onClickEdit = {
|
||||
context.startActivity(
|
||||
ReplaceEditActivity.startIntent(context, ui.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inSelectionMode) {
|
||||
DraggableSelectionHandler(
|
||||
listState = listState,
|
||||
rules = rules,
|
||||
selectedRuleIds = selectedRuleIds,
|
||||
onSelectionChange = viewModel::setSelection,
|
||||
haptic = haptic,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(60.dp)
|
||||
.align(Alignment.TopStart)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = inSelectionMode,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset).zIndex(1f),
|
||||
enter = slideInVertically { it } + fadeIn(),
|
||||
exit = slideOutVertically { it } + fadeOut()
|
||||
) {
|
||||
SelectionBottomBar(
|
||||
onSelectAll = {
|
||||
viewModel.setSelection(rules.map { it.id }.toSet())
|
||||
},
|
||||
onSelectInvert = {
|
||||
val allIds = rules.map { it.id }.toSet()
|
||||
viewModel.setSelection(allIds - selectedRuleIds)
|
||||
},
|
||||
primaryAction = ActionItem(
|
||||
text = stringResource(R.string.delete),
|
||||
icon = { Icon(Icons.Default.Delete, null) },
|
||||
onClick = { showDeleteSelectedDialog = true }
|
||||
),
|
||||
secondaryActions = listOf(
|
||||
ActionItem(
|
||||
text = stringResource(R.string.enable),
|
||||
onClick = {
|
||||
viewModel.enableSelectionByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
if (inSelectionMode) {
|
||||
DraggableSelectionHandler(
|
||||
listState = listState,
|
||||
rules = rules,
|
||||
selectedRuleIds = selectedRuleIds,
|
||||
onSelectionChange = viewModel::setSelection,
|
||||
haptic = haptic,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(60.dp)
|
||||
.align(Alignment.TopStart)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = inSelectionMode,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset).zIndex(1f),
|
||||
enter = slideInVertically { it } + fadeIn(),
|
||||
exit = slideOutVertically { it } + fadeOut()
|
||||
) {
|
||||
SelectionBottomBar(
|
||||
onSelectAll = {
|
||||
viewModel.setSelection(rules.map { it.id }.toSet())
|
||||
},
|
||||
onSelectInvert = {
|
||||
val allIds = rules.map { it.id }.toSet()
|
||||
viewModel.setSelection(allIds - selectedRuleIds)
|
||||
},
|
||||
primaryAction = ActionItem(
|
||||
text = stringResource(R.string.delete),
|
||||
icon = { Icon(Icons.Default.Delete, null) },
|
||||
onClick = { showDeleteSelectedDialog = true }
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.disable_selection),
|
||||
onClick = {
|
||||
viewModel.disableSelectionByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = {
|
||||
viewModel.topSelectByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_bottom),
|
||||
onClick = {
|
||||
viewModel.bottomSelectByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.export),
|
||||
onClick = { showFilePickerSheet = true }
|
||||
secondaryActions = listOf(
|
||||
ActionItem(
|
||||
text = stringResource(R.string.enable),
|
||||
onClick = {
|
||||
viewModel.enableSelectionByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.disable_selection),
|
||||
onClick = {
|
||||
viewModel.disableSelectionByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = {
|
||||
viewModel.topSelectByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_bottom),
|
||||
onClick = {
|
||||
viewModel.bottomSelectByIds(selectedRuleIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.export),
|
||||
onClick = { showFilePickerSheet = true }
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -101,65 +97,82 @@ fun SettingItem(
|
||||
description: String? = null,
|
||||
option: String? = null,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
leadingContent = {
|
||||
when {
|
||||
painter != null -> {
|
||||
Icon(
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Box ( modifier = Modifier.fillMaxWidth() ){
|
||||
ListItem(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
if (dropdownMenu != null) {
|
||||
showMenu = true
|
||||
onLongClick?.invoke()
|
||||
}
|
||||
}
|
||||
),
|
||||
leadingContent = {
|
||||
when {
|
||||
painter != null -> Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
imageVector != null -> {
|
||||
Icon(
|
||||
imageVector != null -> Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
option?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = trailingContent,
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
option?.let {
|
||||
AnimatedTextLine(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (dropdownMenu != null) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
dropdownMenu {
|
||||
showMenu = false
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = trailingContent,
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.legado.app.utils.eventBus
|
||||
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object FlowEventBus {
|
||||
// 核心存储结构:Tag -> SharedFlow
|
||||
private val events = ConcurrentHashMap<String, MutableSharedFlow<Any>>()
|
||||
|
||||
// 获取或创建对应 Tag 的 Flow
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> with(tag: String): MutableSharedFlow<T> {
|
||||
return events.getOrPut(tag) {
|
||||
MutableSharedFlow<Any>(
|
||||
replay = 1, // 相当于 LiveData 的粘性,确保新订阅者能收到最后一次通知
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
} as MutableSharedFlow<T>
|
||||
}
|
||||
|
||||
// 快捷发送
|
||||
fun post(tag: String, value: Any) {
|
||||
MainScope().launch {
|
||||
with<Any>(tag).emit(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.legado.app.utils.eventBus
|
||||
|
||||
import io.legado.app.utils.eventBus.FlowEventBus.with
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FlowEventBusExt {
|
||||
|
||||
// 在协程中直接调用
|
||||
suspend fun <T> post(tag: String, value: T) {
|
||||
with<T>(tag).emit(value)
|
||||
}
|
||||
|
||||
// 在非协程环境调用
|
||||
fun <T> postSync(tag: String, value: T) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
with<T>(tag).emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user