diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index f2b6d4612..e97e06191 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/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" diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt index 9ba4308cb..32ac28071 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt +++ b/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 ) }, diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index d8ebe4be7..97f165200 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -537,7 +537,7 @@ open class MainActivity : VMBaseActivity(), idBookshelf4 -> BookshelfFragment4(position) idExplore -> ExploreFragment(position) idRss -> RssFragment(position) - else -> MyFragment(position) + else -> MyFragment.newInstance(position) } fragmentMap[getFragmentId(position)] = fragment diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt index 9f00ff398..41e5de6bf 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt +++ b/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" - 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("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(EventBus.WEB_SERVICE) { - findPreference(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() - "replaceManage" -> startActivity() - "dictRuleManage" -> startActivity() - "txtTocRuleManage" -> startActivity() - "bookmark" -> startActivity() - "setting" -> startActivity { - putExtra("configTag", ConfigTag.OTHER_CONFIG) - } - - "web_dav_setting" -> startActivity { - putExtra("configTag", ConfigTag.BACKUP_CONFIG) - } - - "theme_setting" -> startActivity { - putExtra("configTag", ConfigTag.THEME_CONFIG) - } - - "read_setting" -> startActivity { - putExtra("configTag", ConfigTag.READ_CONFIG) - } - - "fileManage" -> startActivity() - "readRecord" -> startActivity() - "about" -> startActivity() - "exit" -> activity?.finish() - } - return super.onPreferenceTreeClick(preference) - } - - } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt b/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt new file mode 100644 index 000000000..473c0358c --- /dev/null +++ b/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) + } + ) + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt b/app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt new file mode 100644 index 000000000..7f93bebf1 --- /dev/null +++ b/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 = _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 "" + ) + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt index a964acb8f..7aea6540f 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt +++ b/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 -> { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt index 64f85fe05..24f361de4 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt +++ b/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) ) }