diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0c0be999a..288c37efb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -81,7 +81,8 @@ "Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\")", "Bash(findstr /i \"miuix.*\\\\.kt\")", "Bash(findstr /v \"\\\\\\\\build\\\\\\\\\")", - "Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")" + "Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")", + "Skill(legado-compose-review)" ] } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainDestination.kt b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt index 5cdfdac8a..e271d2e83 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainDestination.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt @@ -2,6 +2,7 @@ package io.legado.app.ui.main import androidx.annotation.StringRes import io.legado.app.R +import io.legado.app.ui.config.themeConfig.ThemeConfig import kotlinx.collections.immutable.persistentListOf sealed class MainDestination( @@ -32,3 +33,11 @@ sealed class MainDestination( val mainDestinations = persistentListOf(Bookshelf, Explore, Rss, My) } } + +val MainDestination.customIconPath: String + get() = when (this) { + MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf + MainDestination.Explore -> ThemeConfig.navIconExplore + MainDestination.Rss -> ThemeConfig.navIconRss + MainDestination.My -> ThemeConfig.navIconMy + } diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index 9bbe91b8f..8354fba85 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.main import android.content.Intent -import android.net.Uri import android.os.Build import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity @@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -52,24 +50,22 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.graphics.Color +import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeSource -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter import io.legado.app.R import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.main.bookshelf.BookshelfScreen @@ -78,7 +74,6 @@ import io.legado.app.ui.main.explore.ExploreScreen import io.legado.app.ui.main.my.MyScreen import io.legado.app.ui.main.my.PrefClickEvent import io.legado.app.ui.main.rss.RssScreen -import io.legado.app.ui.theme.regularHazeEffect import io.legado.app.ui.widget.components.AppNavigationBar import io.legado.app.ui.widget.components.AppNavigationBarItem import io.legado.app.ui.widget.components.AppScaffold @@ -95,6 +90,7 @@ import io.legado.app.utils.sendToClip import io.legado.app.utils.showDialogFragment import io.legado.app.utils.startActivityForBook import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.androidx.compose.koinViewModel @@ -124,20 +120,21 @@ fun MainScreen( val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val mainUiState by viewModel.uiState.collectAsStateWithLifecycle() + val defaultHelpTitle = stringResource(R.string.help) LaunchedEffect(viewModel, context) { - viewModel.effects.collect { effect -> + viewModel.effects.collectLatest { effect -> when (effect) { is MainEffect.OpenUrl -> { context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(effect.url)) + Intent(Intent.ACTION_VIEW, effect.url.toUri()) ) } is MainEffect.CopyUrl -> context.sendToClip(effect.url) is MainEffect.ShowMarkdown -> { - val activity = context as? AppCompatActivity ?: return@collect - val title = effect.title.ifBlank { context.getString(R.string.help) } + val activity = context as? AppCompatActivity ?: return@collectLatest + val title = effect.title.ifBlank { defaultHelpTitle } val mdText = withContext(Dispatchers.IO) { context.assets .open("web/help/md/${effect.path}.md") @@ -288,12 +285,7 @@ fun MainScreen( } }, label = if (labelVisibilityMode != "unlabeled") { - val hasCustomIcon = when (destination) { - MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty() - MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty() - MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty() - MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty() - } + val hasCustomIcon = destination.customIconPath.isNotEmpty() if (hasCustomIcon) null else {{ AppText(stringResource(destination.labelId)) }} } else null ) @@ -308,12 +300,7 @@ fun MainScreen( AppNavigationBar() { destinations.forEachIndexed { index, destination -> val selected = pagerState.targetPage == index - val customIconPath = when (destination) { - MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf - MainDestination.Explore -> ThemeConfig.navIconExplore - MainDestination.Rss -> ThemeConfig.navIconRss - MainDestination.My -> ThemeConfig.navIconMy - } + val customIconPath = destination.customIconPath AppNavigationBarItem( modifier = Modifier.semantics(mergeDescendants = true) { contentDescription = "nav_${destination.route}" @@ -442,22 +429,12 @@ fun MainScreen( tabsCount = destinations.size, isBlurEnabled = useLiquidGlass, hasCustomIcons = destinations.any { dest -> - when (dest) { - MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty() - MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty() - MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty() - MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty() - } + dest.customIconPath.isNotEmpty() } ) { destinations.forEachIndexed { index, destination -> val selected = pagerState.targetPage == index - val hasCustomIcon = when (destination) { - MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty() - MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty() - MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty() - MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty() - } + val hasCustomIcon = destination.customIconPath.isNotEmpty() FloatingBottomBarItem( onClick = { coroutineScope.launch { @@ -536,31 +513,13 @@ private fun NavigationIcon( selected: Boolean, modifier: Modifier = Modifier ) { - val customIconPath = when (destination) { - MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf - MainDestination.Explore -> ThemeConfig.navIconExplore - MainDestination.Rss -> ThemeConfig.navIconRss - MainDestination.My -> ThemeConfig.navIconMy - } + val customIconPath = destination.customIconPath if (customIconPath.isNotEmpty()) { - val context = LocalContext.current - val bitmap = remember(customIconPath) { - kotlin.runCatching { - android.graphics.BitmapFactory.decodeFile(customIconPath) - }.getOrNull() - } - if (bitmap != null) { - Image( - painter = remember(bitmap) { - BitmapPainter(bitmap.asImageBitmap()) - }, - contentDescription = null, - modifier = modifier.size(40.dp) - ) - } else { - val icon = AppIcons.mainDestination(destination, selected) - AppIcon(icon, contentDescription = null, modifier = modifier) - } + AsyncImage( + model = customIconPath, + contentDescription = null, + modifier = modifier.size(40.dp) + ) } else { val icon = AppIcons.mainDestination(destination, selected) AppIcon(icon, contentDescription = null, modifier = modifier) diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index d0dbf6cc2..889e6a026 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -2,9 +2,10 @@ package io.legado.app.ui.main import android.app.Application import android.content.SharedPreferences +import androidx.compose.runtime.Stable import io.legado.app.base.BaseViewModel -import io.legado.app.constant.PreferKey import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase import io.legado.app.domain.usecase.WebDavBackupUseCase import io.legado.app.ui.config.themeConfig.ThemeConfig @@ -13,13 +14,13 @@ import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.eventBus.FlowEventBus import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefString +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList class MainViewModel( application: Application, @@ -133,6 +134,7 @@ sealed interface MainEffect { data object NavigateToReadRecord : MainEffect } +@Stable data class MainUiState( val destinations: ImmutableList = MainDestination.mainDestinations, val defaultHomePage: String = "bookshelf", 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 index 290b73b3b..17d1717a4 100644 --- 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 @@ -1,6 +1,7 @@ package io.legado.app.ui.main.my import android.app.Application +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.EventBus @@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +@Stable data class MyUiState( val isWebServiceRun: Boolean = false, val webServiceAddress: String = "" diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt index 1a8352873..de843d4eb 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -18,27 +17,22 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Login -import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Group -import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Subscriptions import androidx.compose.material.icons.filled.VerticalAlignTop -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -59,7 +53,6 @@ import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.widget.components.SourceIcon import io.legado.app.ui.widget.components.alert.AppAlertDialog -import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillHeaderDivider import io.legado.app.ui.widget.components.list.ListScaffold @@ -67,6 +60,7 @@ import io.legado.app.ui.widget.components.menuItem.MenuItemIcon import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.text.AppText +import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.utils.openUrl import io.legado.app.utils.startActivity import kotlinx.coroutines.flow.collectLatest @@ -103,23 +97,35 @@ fun RssScreen( is RssEffect.OpenExternalUrl -> { currentContext.openUrl(effect.url) } + + is RssEffect.OpenSourceEdit -> { + currentContext.startActivity { + putExtra("sourceUrl", effect.sourceUrl) + } + } + + is RssEffect.Login -> { + currentContext.startActivity { + putExtra("type", "rssSource") + putExtra("key", effect.sourceUrl) + } + } + + RssEffect.OpenRuleSub -> { + currentContext.startActivity() + } + + RssEffect.OpenFavorites -> { + currentContext.startActivity() + } + + RssEffect.OpenSourceManage -> { + currentContext.startActivity() + } } } } - val edit: (RssSource) -> Unit = { rssSource -> - context.startActivity { - putExtra("sourceUrl", rssSource.sourceUrl) - } - } - - val login: (RssSource) -> Unit = { rssSource -> - context.startActivity { - putExtra("type", "rssSource") - putExtra("key", rssSource.sourceUrl) - } - } - ListScaffold( title = stringResource(R.string.rss), state = uiState, @@ -130,12 +136,12 @@ fun RssScreen( searchPlaceholder = stringResource(R.string.search_rss_source), topBarActions = { TopBarActionButton( - onClick = { context.startActivity() }, + onClick = { viewModel.openRuleSub() }, imageVector = Icons.Default.Subscriptions, contentDescription = stringResource(R.string.rule_subscription) ) TopBarActionButton( - onClick = { context.startActivity() }, + onClick = { viewModel.openFavorites() }, imageVector = Icons.Default.Star, contentDescription = stringResource(R.string.favorite) ) @@ -143,7 +149,7 @@ fun RssScreen( dropDownMenuContent = { dismiss -> RoundDropdownMenuItem( onClick = { - context.startActivity() + viewModel.openSourceManage() dismiss() }, text = stringResource(R.string.rss_feed_management), @@ -183,10 +189,10 @@ fun RssScreen( source = source, onClick = { viewModel.openSource(source) }, onTop = { viewModel.topSource(source) }, - onEdit = { edit(source) }, + onEdit = { viewModel.openSourceEdit(source) }, onDelete = { sourceToDeleteUrl = source.sourceUrl }, onDisable = { viewModel.disable(source) }, - onLogin = { login(source) } + onLogin = { viewModel.login(source) } ) } } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssUiState.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssUiState.kt index 3955e7808..a0e76a96f 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssUiState.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssUiState.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.main.rss +import androidx.compose.runtime.Stable import io.legado.app.data.entities.RssSource import io.legado.app.ui.widget.components.list.ListUiState import kotlinx.collections.immutable.ImmutableList @@ -7,6 +8,7 @@ import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf +@Stable data class RssUiState( override val items: ImmutableList = persistentListOf(), override val selectedIds: ImmutableSet = persistentSetOf(), diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt index 092e488e0..10af900ef 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt @@ -116,6 +116,26 @@ class RssViewModel( } } + fun openSourceEdit(rssSource: RssSource) { + _effects.tryEmit(RssEffect.OpenSourceEdit(rssSource.sourceUrl)) + } + + fun login(rssSource: RssSource) { + _effects.tryEmit(RssEffect.Login(rssSource.sourceUrl)) + } + + fun openRuleSub() { + _effects.tryEmit(RssEffect.OpenRuleSub) + } + + fun openFavorites() { + _effects.tryEmit(RssEffect.OpenFavorites) + } + + fun openSourceManage() { + _effects.tryEmit(RssEffect.OpenSourceManage) + } + fun openSource(rssSource: RssSource) { if (!rssSource.singleUrl) { _effects.tryEmit(RssEffect.OpenSort(rssSource.sourceUrl, null, null)) @@ -186,4 +206,9 @@ sealed interface RssEffect { ) : RssEffect data class OpenExternalUrl(val url: String) : RssEffect + data class OpenSourceEdit(val sourceUrl: String) : RssEffect + data class Login(val sourceUrl: String) : RssEffect + data object OpenRuleSub : RssEffect + data object OpenFavorites : RssEffect + data object OpenSourceManage : RssEffect }