Просмотр исходного кода

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

HapeLee 10 месяцев назад
Родитель
Сommit
8133b9ac7e

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -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"

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -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
                         )
                     },

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -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

+ 62 - 110
app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt

@@ -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"
+
+        fun newInstance(position: Int): MyFragment {
+            return MyFragment().apply {
+                arguments = Bundle().apply {
+                    putInt(ARG_POSITION, position)
+                }
+            }
+        }
     }
 
-    override val position: Int? get() = arguments?.getInt("position")
+    override val position: Int by lazy {
+        arguments?.getInt(ARG_POSITION) ?: 0
+    }
 
-    private val binding by viewBinding(FragmentMyConfigBinding::bind)
+    private val viewModel: MyViewModel by viewModel()
 
-    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 onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
 
-    override fun onCompatCreateOptionsMenu(menu: Menu) {
-        menuInflater.inflate(R.menu.main_my, menu)
-    }
+        return ComposeView(requireContext()).apply {
 
-    override fun onCompatOptionsItemSelected(item: MenuItem) {
-        when (item.itemId) {
-            R.id.menu_help -> showHelp("appHelp")
-        }
-    }
+            setViewCompositionStrategy(
+                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+            )
 
-    /**
-     * 配置
-     */
-    class MyPreferenceFragment : PreferenceFragment(),
-        SharedPreferences.OnSharedPreferenceChangeListener {
-
-        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
-                }
-                context?.selector(arrayListOf("复制地址", "浏览器打开")) { _, i ->
-                    when (i) {
-                        0 -> context?.sendToClip(it.summary.toString())
-                        1 -> context?.openUrl(it.summary.toString())
-                    }
+            setContent {
+                AppTheme{
+                    MyScreen(
+                        viewModel = viewModel,
+                        onNavigate = { event ->
+                            handleEvent(event)
+                        }
+                    )
                 }
-                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)
-                    }
-                }
-            }
-        }
-
-        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()
-        }
+    private fun handleEvent(event: PrefClickEvent) {
+        when (event) {
 
-        override fun onSharedPreferenceChanged(
-            sharedPreferences: SharedPreferences?,
-            key: String?
-        ) {
-            when (key) {
-                PreferKey.webService -> {
-                    if (requireContext().getPrefBoolean("webService")) {
-                        WebService.start(requireContext())
-                    } else {
-                        WebService.stop(requireContext())
+            is PrefClickEvent.StartActivity -> {
+                startActivity(
+                    Intent(requireContext(), event.destination).apply {
+                        event.configTag?.let {
+                            putExtra("configTag", it)
+                        }
                     }
-                }
-
-                "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)
-                }
+            is PrefClickEvent.OpenUrl ->
+                requireContext().openUrl(event.url)
 
-                "web_dav_setting" -> startActivity<ConfigActivity> {
-                    putExtra("configTag", ConfigTag.BACKUP_CONFIG)
-                }
+            is PrefClickEvent.CopyUrl ->
+                requireContext().sendToClip(event.url)
 
-                "theme_setting" -> startActivity<ConfigActivity> {
-                    putExtra("configTag", ConfigTag.THEME_CONFIG)
-                }
+            is PrefClickEvent.ShowMd ->
+                showHelp(event.title)
 
-                "read_setting" -> startActivity<ConfigActivity> {
-                    putExtra("configTag", ConfigTag.READ_CONFIG)
-                }
+            PrefClickEvent.ExitApp ->
+                requireActivity().finish()
 
-                "fileManage" -> startActivity<FileManageActivity>()
-                "readRecord" -> startActivity<ReadRecordActivity>()
-                "about" -> startActivity<AboutActivity>()
-                "exit" -> activity?.finish()
-            }
-            return super.onPreferenceTreeClick(preference)
+            else -> Unit
         }
-
-
     }
-}
+}

+ 255 - 0
app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt

@@ -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)
+                    }
+                )
+            }
+        }
+    }
+}

+ 68 - 0
app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt

@@ -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 ""
+        )
+    }
+}

+ 7 - 3
app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt

@@ -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 -> {

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt

@@ -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)
             )
         }