[优化] 一些界面问题

This commit is contained in:
HapeLee
2026-01-04 03:39:58 +08:00
parent 8362a0fe8b
commit bdb7c77442
7 changed files with 302 additions and 234 deletions
@@ -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 ->
},
onLongClick = {
if (uiState.isWebServiceRun) {
showMenu = true
}
}
)
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("复制地址") }, text = { Text("复制地址") },
onClick = { onClick = {
onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress)) onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
showMenu = false onDismiss()
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("浏览器打开") }, text = { Text("浏览器打开") },
onClick = { onClick = {
onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress)) onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
showMenu = false onDismiss()
} }
) )
} }
} else null,
onClick = { }
)
} }
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,6 +495,12 @@ fun ReplaceRuleScreen(
.padding(padding) .padding(padding)
.fillMaxSize() .fillMaxSize()
) { ) {
if (rules.isEmpty()) {
EmptyMessageView(
modifier = Modifier.fillMaxSize(),
message = "没有替换规则!"
)
} else {
FastScrollLazyColumn( FastScrollLazyColumn(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
@@ -630,6 +637,7 @@ fun ReplaceRuleScreen(
} }
} }
} }
}
} }
@Composable @Composable
@@ -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,34 +97,38 @@ 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,
) { ) {
var showMenu by remember { mutableStateOf(false) }
Box ( modifier = Modifier.fillMaxWidth() ){
ListItem( ListItem(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick onLongClick = {
if (dropdownMenu != null) {
showMenu = true
onLongClick?.invoke()
}
}
), ),
leadingContent = { leadingContent = {
when { when {
painter != null -> { painter != null -> Icon(
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(
@@ -137,7 +137,9 @@ fun SettingItem(
) )
}, },
supportingContent = { supportingContent = {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
description?.let { description?.let {
Text( Text(
text = it, text = it,
@@ -145,9 +147,8 @@ fun SettingItem(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
option?.let { option?.let {
AnimatedTextLine( Text(
text = it, text = it,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
@@ -162,4 +163,16 @@ fun SettingItem(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) )
) )
if (dropdownMenu != null) {
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
dropdownMenu {
showMenu = false
}
}
}
}
} }
@@ -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)
}
}
}