[优化] 一些界面问题
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.constant.PreferKey
|
||||||
import io.legado.app.receiver.NetworkChangedListener
|
import io.legado.app.receiver.NetworkChangedListener
|
||||||
import io.legado.app.utils.NetworkUtils
|
import io.legado.app.utils.NetworkUtils
|
||||||
|
import io.legado.app.utils.eventBus.FlowEventBus
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.getPrefInt
|
import io.legado.app.utils.getPrefInt
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
@@ -108,6 +109,7 @@ class WebService : BaseService() {
|
|||||||
}
|
}
|
||||||
startForegroundNotification()
|
startForegroundNotification()
|
||||||
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
||||||
|
FlowEventBus.post(EventBus.WEB_SERVICE, hostAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +143,7 @@ class WebService : BaseService() {
|
|||||||
webSocketServer?.stop()
|
webSocketServer?.stop()
|
||||||
}
|
}
|
||||||
postEvent(EventBus.WEB_SERVICE, "")
|
postEvent(EventBus.WEB_SERVICE, "")
|
||||||
|
FlowEventBus.post(EventBus.WEB_SERVICE, "")
|
||||||
upTile(false)
|
upTile(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +173,7 @@ class WebService : BaseService() {
|
|||||||
hostAddress = notificationList.first()
|
hostAddress = notificationList.first()
|
||||||
isRun = true
|
isRun = true
|
||||||
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
postEvent(EventBus.WEB_SERVICE, hostAddress)
|
||||||
|
FlowEventBus.post(EventBus.WEB_SERVICE, hostAddress)
|
||||||
startForegroundNotification()
|
startForegroundNotification()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
toastOnUi(e.localizedMessage ?: "")
|
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.Source
|
||||||
import androidx.compose.material.icons.filled.Translate
|
import androidx.compose.material.icons.filled.Translate
|
||||||
import androidx.compose.material.icons.filled.Web
|
import androidx.compose.material.icons.filled.Web
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -37,9 +36,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -67,7 +63,6 @@ fun MyScreen(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -126,35 +121,26 @@ fun MyScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
dropdownMenu = if (uiState.isWebServiceRun) {
|
||||||
viewModel.onEvent(PrefClickEvent.ToggleWebService)
|
{ onDismiss ->
|
||||||
},
|
DropdownMenuItem(
|
||||||
onLongClick = {
|
text = { Text("复制地址") },
|
||||||
if (uiState.isWebServiceRun) {
|
onClick = {
|
||||||
showMenu = true
|
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(
|
SplicedColumnGroup(
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package io.legado.app.ui.main.my
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.service.WebService
|
import io.legado.app.service.WebService
|
||||||
|
import io.legado.app.utils.eventBus.FlowEventBus
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class MyUiState(
|
data class MyUiState(
|
||||||
val isWebServiceRun: Boolean = false,
|
val isWebServiceRun: Boolean = false,
|
||||||
@@ -17,7 +22,6 @@ sealed class PrefClickEvent {
|
|||||||
data class CopyUrl(val url: String) : PrefClickEvent()
|
data class CopyUrl(val url: String) : PrefClickEvent()
|
||||||
data class ShowMd(val title: String, val path: String) : PrefClickEvent()
|
data class ShowMd(val title: String, val path: String) : PrefClickEvent()
|
||||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||||
object ShowWebServiceMenu : PrefClickEvent()
|
|
||||||
object ToggleWebService : PrefClickEvent()
|
object ToggleWebService : PrefClickEvent()
|
||||||
object ExitApp : PrefClickEvent()
|
object ExitApp : PrefClickEvent()
|
||||||
}
|
}
|
||||||
@@ -29,40 +33,40 @@ class MyViewModel(
|
|||||||
private val _uiState = MutableStateFlow(
|
private val _uiState = MutableStateFlow(
|
||||||
MyUiState(
|
MyUiState(
|
||||||
isWebServiceRun = WebService.isRun,
|
isWebServiceRun = WebService.isRun,
|
||||||
webServiceAddress = if (WebService.isRun) {
|
webServiceAddress = WebService.hostAddress
|
||||||
WebService.hostAddress
|
|
||||||
} else ""
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
|
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) {
|
fun onEvent(event: PrefClickEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
||||||
PrefClickEvent.ToggleWebService -> {
|
PrefClickEvent.ToggleWebService -> {
|
||||||
val newRun = !_uiState.value.isWebServiceRun
|
val currentIsRun = _uiState.value.isWebServiceRun
|
||||||
if (newRun) {
|
|
||||||
|
if (!currentIsRun) {
|
||||||
WebService.start(getApplication())
|
WebService.start(getApplication())
|
||||||
} else {
|
} else {
|
||||||
WebService.stop(getApplication())
|
WebService.stop(getApplication())
|
||||||
|
_uiState.update { it.copy(isWebServiceRun = false, webServiceAddress = "") }
|
||||||
}
|
}
|
||||||
updateWebServiceState()
|
|
||||||
}
|
|
||||||
|
|
||||||
PrefClickEvent.ShowWebServiceMenu -> {
|
|
||||||
// UI 自己处理 showMenu
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
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.replace.edit.ReplaceEditActivity
|
||||||
import io.legado.app.ui.widget.components.ActionItem
|
import io.legado.app.ui.widget.components.ActionItem
|
||||||
import io.legado.app.ui.widget.components.AnimatedText
|
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.lazylist.FastScrollLazyColumn
|
||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
@@ -494,139 +495,146 @@ fun ReplaceRuleScreen(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
FastScrollLazyColumn(
|
if (rules.isEmpty()) {
|
||||||
state = listState,
|
EmptyMessageView(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
message = "没有替换规则!"
|
||||||
contentPadding = PaddingValues(
|
)
|
||||||
top = 8.dp,
|
} else {
|
||||||
bottom = 120.dp
|
FastScrollLazyColumn(
|
||||||
),
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
modifier = Modifier
|
||||||
) {
|
.fillMaxSize(),
|
||||||
items(rules, key = { it.id }) { ui ->
|
contentPadding = PaddingValues(
|
||||||
val isSelected = selectedRuleIds.contains(ui.id)
|
top = 8.dp,
|
||||||
ReorderableItem(
|
bottom = 120.dp
|
||||||
state = reorderableState,
|
),
|
||||||
key = ui.id
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) { isDragging ->
|
) {
|
||||||
|
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)
|
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||||
ReplaceRuleItem(
|
ReplaceRuleItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.zIndex(if (isDragging) 1f else 0f)
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = elevation,
|
elevation = elevation,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
clip = false
|
clip = false
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (canReorder) {
|
if (canReorder) {
|
||||||
Modifier.longPressDraggableHandle(
|
Modifier.longPressDraggableHandle(
|
||||||
onDragStarted = {
|
onDragStarted = {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||||
},
|
},
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
name = ui.name,
|
name = ui.name,
|
||||||
isEnabled = ui.isEnabled,
|
isEnabled = ui.isEnabled,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
inSelectionMode = inSelectionMode,
|
inSelectionMode = inSelectionMode,
|
||||||
onEnabledChange = { enabled ->
|
onEnabledChange = { enabled ->
|
||||||
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
||||||
},
|
},
|
||||||
onDelete = { showDeleteRuleDialog = ui.rule },
|
onDelete = { showDeleteRuleDialog = ui.rule },
|
||||||
onToTop = { viewModel.toTop(ui.rule) },
|
onToTop = { viewModel.toTop(ui.rule) },
|
||||||
onToBottom = { viewModel.toBottom(ui.rule) },
|
onToBottom = { viewModel.toBottom(ui.rule) },
|
||||||
onToggleSelection = {
|
onToggleSelection = {
|
||||||
viewModel.toggleSelection(ui.id)
|
viewModel.toggleSelection(ui.id)
|
||||||
},
|
},
|
||||||
onClickEdit = {
|
onClickEdit = {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
ReplaceEditActivity.startIntent(context, ui.id)
|
ReplaceEditActivity.startIntent(context, ui.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (inSelectionMode) {
|
||||||
if (inSelectionMode) {
|
DraggableSelectionHandler(
|
||||||
DraggableSelectionHandler(
|
listState = listState,
|
||||||
listState = listState,
|
rules = rules,
|
||||||
rules = rules,
|
selectedRuleIds = selectedRuleIds,
|
||||||
selectedRuleIds = selectedRuleIds,
|
onSelectionChange = viewModel::setSelection,
|
||||||
onSelectionChange = viewModel::setSelection,
|
haptic = haptic,
|
||||||
haptic = haptic,
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxHeight()
|
||||||
.fillMaxHeight()
|
.width(60.dp)
|
||||||
.width(60.dp)
|
.align(Alignment.TopStart)
|
||||||
.align(Alignment.TopStart)
|
)
|
||||||
)
|
}
|
||||||
}
|
AnimatedVisibility(
|
||||||
AnimatedVisibility(
|
visible = inSelectionMode,
|
||||||
visible = inSelectionMode,
|
modifier =
|
||||||
modifier =
|
Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset).zIndex(1f),
|
||||||
Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset).zIndex(1f),
|
enter = slideInVertically { it } + fadeIn(),
|
||||||
enter = slideInVertically { it } + fadeIn(),
|
exit = slideOutVertically { it } + fadeOut()
|
||||||
exit = slideOutVertically { it } + fadeOut()
|
) {
|
||||||
) {
|
SelectionBottomBar(
|
||||||
SelectionBottomBar(
|
onSelectAll = {
|
||||||
onSelectAll = {
|
viewModel.setSelection(rules.map { it.id }.toSet())
|
||||||
viewModel.setSelection(rules.map { it.id }.toSet())
|
},
|
||||||
},
|
onSelectInvert = {
|
||||||
onSelectInvert = {
|
val allIds = rules.map { it.id }.toSet()
|
||||||
val allIds = rules.map { it.id }.toSet()
|
viewModel.setSelection(allIds - selectedRuleIds)
|
||||||
viewModel.setSelection(allIds - selectedRuleIds)
|
},
|
||||||
},
|
primaryAction = ActionItem(
|
||||||
primaryAction = ActionItem(
|
text = stringResource(R.string.delete),
|
||||||
text = stringResource(R.string.delete),
|
icon = { Icon(Icons.Default.Delete, null) },
|
||||||
icon = { Icon(Icons.Default.Delete, null) },
|
onClick = { showDeleteSelectedDialog = true }
|
||||||
onClick = { showDeleteSelectedDialog = true }
|
|
||||||
),
|
|
||||||
secondaryActions = listOf(
|
|
||||||
ActionItem(
|
|
||||||
text = stringResource(R.string.enable),
|
|
||||||
onClick = {
|
|
||||||
viewModel.enableSelectionByIds(selectedRuleIds)
|
|
||||||
viewModel.setSelection(emptySet())
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
ActionItem(
|
secondaryActions = listOf(
|
||||||
text = stringResource(R.string.disable_selection),
|
ActionItem(
|
||||||
onClick = {
|
text = stringResource(R.string.enable),
|
||||||
viewModel.disableSelectionByIds(selectedRuleIds)
|
onClick = {
|
||||||
viewModel.setSelection(emptySet())
|
viewModel.enableSelectionByIds(selectedRuleIds)
|
||||||
}
|
viewModel.setSelection(emptySet())
|
||||||
),
|
}
|
||||||
ActionItem(
|
),
|
||||||
text = stringResource(R.string.to_top),
|
ActionItem(
|
||||||
onClick = {
|
text = stringResource(R.string.disable_selection),
|
||||||
viewModel.topSelectByIds(selectedRuleIds)
|
onClick = {
|
||||||
viewModel.setSelection(emptySet())
|
viewModel.disableSelectionByIds(selectedRuleIds)
|
||||||
}
|
viewModel.setSelection(emptySet())
|
||||||
),
|
}
|
||||||
ActionItem(
|
),
|
||||||
text = stringResource(R.string.to_bottom),
|
ActionItem(
|
||||||
onClick = {
|
text = stringResource(R.string.to_top),
|
||||||
viewModel.bottomSelectByIds(selectedRuleIds)
|
onClick = {
|
||||||
viewModel.setSelection(emptySet())
|
viewModel.topSelectByIds(selectedRuleIds)
|
||||||
}
|
viewModel.setSelection(emptySet())
|
||||||
),
|
}
|
||||||
ActionItem(
|
),
|
||||||
text = stringResource(R.string.export),
|
ActionItem(
|
||||||
onClick = { showFilePickerSheet = true }
|
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
|
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.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ChevronRight
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -101,65 +97,82 @@ fun SettingItem(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
option: String? = null,
|
option: String? = null,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
modifier = modifier
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
Box ( modifier = Modifier.fillMaxWidth() ){
|
||||||
.combinedClickable(
|
ListItem(
|
||||||
onClick = onClick,
|
modifier = modifier
|
||||||
onLongClick = onLongClick
|
.clip(RoundedCornerShape(4.dp))
|
||||||
),
|
.combinedClickable(
|
||||||
leadingContent = {
|
onClick = onClick,
|
||||||
when {
|
onLongClick = {
|
||||||
painter != null -> {
|
if (dropdownMenu != null) {
|
||||||
Icon(
|
showMenu = true
|
||||||
|
onLongClick?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
leadingContent = {
|
||||||
|
when {
|
||||||
|
painter != null -> Icon(
|
||||||
painter = painter,
|
painter = painter,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
imageVector != null -> Icon(
|
||||||
|
|
||||||
imageVector != null -> {
|
|
||||||
Icon(
|
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
headlineContent = {
|
||||||
headlineContent = {
|
Text(
|
||||||
Text(
|
text = title,
|
||||||
text = title,
|
style = MaterialTheme.typography.titleMedium
|
||||||
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 {
|
if (dropdownMenu != null) {
|
||||||
AnimatedTextLine(
|
DropdownMenu(
|
||||||
text = it,
|
expanded = showMenu,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
onDismissRequest = { showMenu = false }
|
||||||
color = MaterialTheme.colorScheme.primary,
|
) {
|
||||||
maxLines = 3,
|
dropdownMenu {
|
||||||
overflow = TextOverflow.Ellipsis
|
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