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

This commit is contained in:
HapeLee committed 2025-12-29 02:27:31 +08:00
1 parent 8e56c49538
commit 8133b9ac7e
8 files changed
+407 -126

No files matched your search

@@ -4,6 +4,7 @@ package io.legado.app.constant
object PreferKey { object PreferKey {
const val language = "language" const val language = "language"
const val fontScale = "fontScale" const val fontScale = "fontScale"
const val appTheme = "app_theme"
const val themeMode = "themeMode" const val themeMode = "themeMode"
const val userAgent = "userAgent" const val userAgent = "userAgent"
const val showUnread = "showUnread" 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.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp 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.AnimatedTextLine
import io.legado.app.ui.widget.components.EmptyMessageView import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.SearchBarSection
@@ -121,7 +122,7 @@ fun SearchContentScreen(
} else { } else {
"搜索内容" "搜索内容"
} }
AnimatedTextLine( AnimatedText(
text = title text = title
) )
}, },
@@ -537,7 +537,7 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
idBookshelf4 -> BookshelfFragment4(position) idBookshelf4 -> BookshelfFragment4(position)
idExplore -> ExploreFragment(position) idExplore -> ExploreFragment(position)
idRss -> RssFragment(position) idRss -> RssFragment(position)
else -> MyFragment(position) else -> MyFragment.newInstance(position)
} }
fragmentMap[getFragmentId(position)] = fragment fragmentMap[getFragmentId(position)] = fragment
@@ -1,13 +1,20 @@
package io.legado.app.ui.main.my package io.legado.app.ui.main.my
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View 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 androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.AppTheme
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey 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.dict.rule.DictRuleActivity
import io.legado.app.ui.file.FileManageActivity import io.legado.app.ui.file.FileManageActivity
import io.legado.app.ui.main.MainFragmentInterface 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.ui.replace.ReplaceRuleActivity
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefBoolean 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.showHelp
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding 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() { companion object {
val bundle = Bundle() private const val ARG_POSITION = "position"
bundle.putInt("position", position)
arguments = bundle
}
override val position: Int? get() = arguments?.getInt("position") fun newInstance(position: Int): MyFragment {
return MyFragment().apply {
private val binding by viewBinding(FragmentMyConfigBinding::bind) arguments = Bundle().apply {
putInt(ARG_POSITION, position)
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")
} }
} }
/** override val position: Int by lazy {
* 配置 arguments?.getInt(ARG_POSITION) ?: 0
*/ }
class MyPreferenceFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { private val viewModel: MyViewModel by viewModel()
putPrefBoolean(PreferKey.webService, WebService.isRun)
addPreferencesFromResource(R.xml.pref_main) override fun onCreateView(
findPreference<SwitchPreference>("webService")?.onLongClick { inflater: LayoutInflater,
if (!WebService.isRun) { container: ViewGroup?,
return@onLongClick false 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
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
@@ -31,7 +32,6 @@ 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
import io.legado.app.base.AppTypography
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
@@ -101,12 +101,16 @@ fun SettingItem(
description: String? = null, description: String? = null,
option: String? = null, option: String? = null,
trailingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null,
onClick: () -> Unit onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) { ) {
ListItem( ListItem(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.clickable(onClick = onClick), .combinedClickable(
onClick = onClick,
onLongClick = onLongClick
),
leadingContent = { leadingContent = {
when { when {
painter != null -> { painter != null -> {
@@ -35,7 +35,7 @@ fun SplicedColumnGroup(
text = title, text = title,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp) modifier = Modifier.padding(start = 12.dp, bottom = 8.dp)
) )
} }