HapeLee пре 5 месеци
родитељ
комит
f411b7136e

+ 2 - 1
.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)"
     ]
   }
 }

+ 9 - 0
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<MainDestination>(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
+    }

+ 20 - 61
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)

+ 5 - 3
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> = MainDestination.mainDestinations,
     val defaultHomePage: String = "bookshelf",

+ 2 - 0
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 = ""

+ 31 - 25
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,20 +97,32 @@ fun RssScreen(
                 is RssEffect.OpenExternalUrl -> {
                     currentContext.openUrl(effect.url)
                 }
-            }
-        }
-    }
 
-    val edit: (RssSource) -> Unit = { rssSource ->
-        context.startActivity<RssSourceEditActivity> {
-            putExtra("sourceUrl", rssSource.sourceUrl)
-        }
-    }
+                is RssEffect.OpenSourceEdit -> {
+                    currentContext.startActivity<RssSourceEditActivity> {
+                        putExtra("sourceUrl", effect.sourceUrl)
+                    }
+                }
+
+                is RssEffect.Login -> {
+                    currentContext.startActivity<SourceLoginActivity> {
+                        putExtra("type", "rssSource")
+                        putExtra("key", effect.sourceUrl)
+                    }
+                }
+
+                RssEffect.OpenRuleSub -> {
+                    currentContext.startActivity<RuleSubActivity>()
+                }
+
+                RssEffect.OpenFavorites -> {
+                    currentContext.startActivity<RssFavoritesActivity>()
+                }
 
-    val login: (RssSource) -> Unit = { rssSource ->
-        context.startActivity<SourceLoginActivity> {
-            putExtra("type", "rssSource")
-            putExtra("key", rssSource.sourceUrl)
+                RssEffect.OpenSourceManage -> {
+                    currentContext.startActivity<RssSourceActivity>()
+                }
+            }
         }
     }
 
@@ -130,12 +136,12 @@ fun RssScreen(
         searchPlaceholder = stringResource(R.string.search_rss_source),
         topBarActions = {
             TopBarActionButton(
-                onClick = { context.startActivity<RuleSubActivity>() },
+                onClick = { viewModel.openRuleSub() },
                 imageVector = Icons.Default.Subscriptions,
                 contentDescription = stringResource(R.string.rule_subscription)
             )
             TopBarActionButton(
-                onClick = { context.startActivity<RssFavoritesActivity>() },
+                onClick = { viewModel.openFavorites() },
                 imageVector = Icons.Default.Star,
                 contentDescription = stringResource(R.string.favorite)
             )
@@ -143,7 +149,7 @@ fun RssScreen(
         dropDownMenuContent = { dismiss ->
             RoundDropdownMenuItem(
                 onClick = {
-                    context.startActivity<RssSourceActivity>()
+                    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) }
                 )
             }
         }

+ 2 - 0
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<RssSource> = persistentListOf(),
     override val selectedIds: ImmutableSet<String> = persistentSetOf(),

+ 25 - 0
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
 }