[新增] 由Compose重写的“我的”界面

This commit is contained in:
HapeLee
2025-12-29 02:27:31 +08:00
parent 8e56c49538
commit 8133b9ac7e
8 changed files with 408 additions and 127 deletions
@@ -4,6 +4,7 @@ package io.legado.app.constant
object PreferKey {
const val language = "language"
const val fontScale = "fontScale"
const val appTheme = "app_theme"
const val themeMode = "themeMode"
const val userAgent = "userAgent"
const val showUnread = "showUnread"
@@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection
@@ -121,7 +122,7 @@ fun SearchContentScreen(
} else {
"搜索内容"
}
AnimatedTextLine(
AnimatedText(
text = title
)
},
@@ -537,7 +537,7 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
idBookshelf4 -> BookshelfFragment4(position)
idExplore -> ExploreFragment(position)
idRss -> RssFragment(position)
else -> MyFragment(position)
else -> MyFragment.newInstance(position)
}
fragmentMap[getFragmentId(position)] = fragment
@@ -1,13 +1,20 @@
package io.legado.app.ui.main.my
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import io.legado.app.R
import io.legado.app.ui.theme.AppTheme
import io.legado.app.base.BaseFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
@@ -26,6 +33,7 @@ import io.legado.app.ui.config.ConfigTag
import io.legado.app.ui.dict.rule.DictRuleActivity
import io.legado.app.ui.file.FileManageActivity
import io.legado.app.ui.main.MainFragmentInterface
import io.legado.app.ui.main.explore.ExploreFragment
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefBoolean
@@ -36,136 +44,80 @@ import io.legado.app.utils.sendToClip
import io.legado.app.utils.showHelp
import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class MyFragment() : BaseFragment(R.layout.fragment_my_config), MainFragmentInterface {
class MyFragment : Fragment(), MainFragmentInterface {
constructor(position: Int) : this() {
val bundle = Bundle()
bundle.putInt("position", position)
arguments = bundle
}
companion object {
private const val ARG_POSITION = "position"
override val position: Int? get() = arguments?.getInt("position")
private val binding by viewBinding(FragmentMyConfigBinding::bind)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
setSupportToolbar(binding.topBar)
val fragmentTag = "prefFragment"
var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment()
childFragmentManager.beginTransaction()
.replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit()
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
binding.titleBar.fitsSystemWindows = true
}
override fun onCompatCreateOptionsMenu(menu: Menu) {
menuInflater.inflate(R.menu.main_my, menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem) {
when (item.itemId) {
R.id.menu_help -> showHelp("appHelp")
fun newInstance(position: Int): MyFragment {
return MyFragment().apply {
arguments = Bundle().apply {
putInt(ARG_POSITION, position)
}
}
}
}
/**
* 配置
*/
class MyPreferenceFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
override val position: Int by lazy {
arguments?.getInt(ARG_POSITION) ?: 0
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean(PreferKey.webService, WebService.isRun)
addPreferencesFromResource(R.xml.pref_main)
findPreference<SwitchPreference>("webService")?.onLongClick {
if (!WebService.isRun) {
return@onLongClick false
private val viewModel: MyViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
AppTheme{
MyScreen(
viewModel = viewModel,
onNavigate = { event ->
handleEvent(event)
}
)
}
context?.selector(arrayListOf("复制地址", "浏览器打开")) { _, i ->
when (i) {
0 -> context?.sendToClip(it.summary.toString())
1 -> context?.openUrl(it.summary.toString())
}
}
}
private fun handleEvent(event: PrefClickEvent) {
when (event) {
is PrefClickEvent.StartActivity -> {
startActivity(
Intent(requireContext(), event.destination).apply {
event.configTag?.let {
putExtra("configTag", it)
}
}
}
true
}
observeEventSticky<String>(EventBus.WEB_SERVICE) {
findPreference<SwitchPreference>(PreferKey.webService)?.let {
it.isChecked = WebService.isRun
it.summary = if (WebService.isRun) {
WebService.hostAddress
} else {
getString(R.string.web_service_desc)
}
}
)
}
is PrefClickEvent.OpenUrl ->
requireContext().openUrl(event.url)
is PrefClickEvent.CopyUrl ->
requireContext().sendToClip(event.url)
is PrefClickEvent.ShowMd ->
showHelp(event.title)
PrefClickEvent.ExitApp ->
requireActivity().finish()
else -> Unit
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//listView.setEdgeEffectColor(primaryColor)
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
when (key) {
PreferKey.webService -> {
if (requireContext().getPrefBoolean("webService")) {
WebService.start(requireContext())
} else {
WebService.stop(requireContext())
}
}
"recordLog" -> LogUtils.upLevel()
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
"bookSourceManage" -> startActivity<BookSourceActivity>()
"replaceManage" -> startActivity<ReplaceRuleActivity>()
"dictRuleManage" -> startActivity<DictRuleActivity>()
"txtTocRuleManage" -> startActivity<TxtTocRuleActivity>()
"bookmark" -> startActivity<AllBookmarkActivity>()
"setting" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.OTHER_CONFIG)
}
"web_dav_setting" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.BACKUP_CONFIG)
}
"theme_setting" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.THEME_CONFIG)
}
"read_setting" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.READ_CONFIG)
}
"fileManage" -> startActivity<FileManageActivity>()
"readRecord" -> startActivity<ReadRecordActivity>()
"about" -> startActivity<AboutActivity>()
"exit" -> activity?.finish()
}
return super.onPreferenceTreeClick(preference)
}
}
}
}
@@ -0,0 +1,255 @@
package io.legado.app.ui.main.my
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.automirrored.filled.Rule
import androidx.compose.material.icons.filled.Backup
import androidx.compose.material.icons.filled.FindReplace
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Rule
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
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.about.AboutActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigTag
import io.legado.app.ui.dict.rule.DictRuleActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.widget.components.SettingItem
import io.legado.app.ui.widget.components.SplicedColumnGroup
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(
viewModel: MyViewModel,
onNavigate: (PrefClickEvent) -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
var showMenu by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
contentWindowInsets = WindowInsets.systemBars
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
topBar = {
MediumTopAppBar(
title = {
Text(
text = stringResource(R.string.my)
)
},
actions = {
IconButton(
onClick = { onNavigate(PrefClickEvent.ShowMd("appHelp", "xxx")) }
) {Icon(
Icons.AutoMirrored.Filled.HelpOutline, null)
}
},
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
) {
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = ""
) {
SettingItem(
title = stringResource(R.string.book_source_manage),
description = stringResource(R.string.book_source_manage_desc),
imageVector = Icons.Default.Source,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(BookSourceActivity::class.java)
)
}
)
SettingItem(
title = stringResource(R.string.web_service),
description = if (uiState.isWebServiceRun)
uiState.webServiceAddress
else
stringResource(R.string.web_service_desc),
imageVector = Icons.Default.Web,
trailingContent = {
Switch(
checked = uiState.isWebServiceRun,
onCheckedChange = {
viewModel.onEvent(PrefClickEvent.ToggleWebService)
}
)
},
onClick = {
viewModel.onEvent(PrefClickEvent.ToggleWebService)
},
onLongClick = {
if (uiState.isWebServiceRun) {
showMenu = true
}
}
)
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(
modifier = Modifier.padding(horizontal = 16.dp),
title = stringResource(R.string.setting)) {
SettingItem(
title = stringResource(R.string.backup_restore),
description = stringResource(R.string.web_dav_set_import_old),
imageVector = Icons.Default.Backup,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.BACKUP_CONFIG
)
)
}
)
SettingItem(
title = stringResource(R.string.theme_setting),
description = stringResource(R.string.theme_setting_s),
imageVector = Icons.Default.Palette,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.THEME_CONFIG
)
)
}
)
SettingItem(
title = stringResource(R.string.read),
description = stringResource(R.string.global_read_setting),
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.READ_CONFIG
)
)
}
)
}
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = "规则"
) {
SettingItem(
title = stringResource(R.string.replace_purify),
imageVector = Icons.Default.FindReplace,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(ReplaceRuleActivity::class.java)
)
}
)
SettingItem(
title = stringResource(R.string.txt_toc_rule),
imageVector = Icons.AutoMirrored.Filled.Rule,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(TxtTocRuleActivity::class.java)
)
}
)
SettingItem(
title = stringResource(R.string.dict_rule),
imageVector = Icons.Default.Translate,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(DictRuleActivity::class.java)
)
}
)
}
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = stringResource(R.string.other)) {
SettingItem(
title = stringResource(R.string.about),
imageVector = Icons.Default.Info,
onClick = {
onNavigate(PrefClickEvent.StartActivity(AboutActivity::class.java))
}
)
SettingItem(
title = stringResource(R.string.exit),
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
onClick = {
onNavigate(PrefClickEvent.ExitApp)
}
)
}
}
}
}
@@ -0,0 +1,68 @@
package io.legado.app.ui.main.my
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import io.legado.app.service.WebService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class MyUiState(
val isWebServiceRun: Boolean = false,
val webServiceAddress: String = ""
)
sealed class PrefClickEvent {
data class OpenUrl(val url: String) : 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()
}
class MyViewModel(
application: Application
) : AndroidViewModel(application) {
private val _uiState = MutableStateFlow(
MyUiState(
isWebServiceRun = WebService.isRun,
webServiceAddress = if (WebService.isRun) {
WebService.hostAddress
} else ""
)
)
val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
fun onEvent(event: PrefClickEvent) {
when (event) {
PrefClickEvent.ToggleWebService -> {
val newRun = !_uiState.value.isWebServiceRun
if (newRun) {
WebService.start(getApplication())
} else {
WebService.stop(getApplication())
}
updateWebServiceState()
}
PrefClickEvent.ShowWebServiceMenu -> {
// UI 自己处理 showMenu
}
else -> Unit
}
}
private fun updateWebServiceState() {
_uiState.value = MyUiState(
isWebServiceRun = WebService.isRun,
webServiceAddress = if (WebService.isRun) {
WebService.hostAddress
} else ""
)
}
}
@@ -2,6 +2,7 @@ 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
@@ -31,7 +32,6 @@ 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
import io.legado.app.base.AppTypography
@Preview(showBackground = true)
@Composable
@@ -101,12 +101,16 @@ fun SettingItem(
description: String? = null,
option: String? = null,
trailingContent: @Composable (() -> Unit)? = null,
onClick: () -> Unit
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) {
ListItem(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onClick),
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
),
leadingContent = {
when {
painter != null -> {
@@ -35,7 +35,7 @@ fun SplicedColumnGroup(
text = title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
modifier = Modifier.padding(start = 12.dp, bottom = 8.dp)
)
}