Przeglądaj źródła

feat: 底栏图标相关

HapeLee 3 miesięcy temu
rodzic
commit
f3e99654a9

+ 10 - 0
app/src/main/java/io/legado/app/data/repository/ThemePackageSettingsRepository.kt

@@ -94,6 +94,11 @@ class ThemePackageSettingsRepository : ThemePackageSettingsGateway {
         navIconExplore = string(PreferKey.navIconExplore, ""),
         navIconRss = string(PreferKey.navIconRss, ""),
         navIconMy = string(PreferKey.navIconMy, ""),
+        navIconHomeSelected = string(PreferKey.navIconHomeSelected, ""),
+        navIconBookshelfSelected = string(PreferKey.navIconBookshelfSelected, ""),
+        navIconExploreSelected = string(PreferKey.navIconExploreSelected, ""),
+        navIconRssSelected = string(PreferKey.navIconRssSelected, ""),
+        navIconMySelected = string(PreferKey.navIconMySelected, ""),
         useMiuixMonet = boolean(PreferKey.useMiuixMonet, false),
         useFlexibleTopAppBar = boolean(PreferKey.useFlexibleTopAppBar, true),
         topBarButtonStyle = string(PreferKey.topBarButtonStyle, "tonal"),
@@ -225,6 +230,11 @@ internal fun ThemeExportData.toPreferenceValues(): Map<String, Any?> {
                 PreferKey.navIconExplore to data.navIconExplore,
                 PreferKey.navIconRss to data.navIconRss,
                 PreferKey.navIconMy to data.navIconMy,
+        PreferKey.navIconHomeSelected to data.navIconHomeSelected,
+        PreferKey.navIconBookshelfSelected to data.navIconBookshelfSelected,
+        PreferKey.navIconExploreSelected to data.navIconExploreSelected,
+        PreferKey.navIconRssSelected to data.navIconRssSelected,
+        PreferKey.navIconMySelected to data.navIconMySelected,
                 PreferKey.useMiuixMonet to data.useMiuixMonet,
                 PreferKey.useFlexibleTopAppBar to data.useFlexibleTopAppBar,
         PreferKey.topBarButtonStyle to data.topBarButtonStyle,

+ 5 - 0
app/src/main/java/io/legado/app/domain/model/settings/ThemeExportData.kt

@@ -77,6 +77,11 @@ data class ThemeExportData(
     val navIconExplore: String = "",
     val navIconRss: String = "",
     val navIconMy: String = "",
+    val navIconHomeSelected: String = "",
+    val navIconBookshelfSelected: String = "",
+    val navIconExploreSelected: String = "",
+    val navIconRssSelected: String = "",
+    val navIconMySelected: String = "",
     val useMiuixMonet: Boolean = false,
     val useFlexibleTopAppBar: Boolean = true,
     val topBarButtonStyle: String = "tonal",

+ 15 - 0
app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt

@@ -39,6 +39,11 @@ object ThemeImportExport {
             "navIconExplore" to data.navIconExplore,
             "navIconRss" to data.navIconRss,
             "navIconMy" to data.navIconMy,
+            "navIconHomeSelected" to data.navIconHomeSelected,
+            "navIconBookshelfSelected" to data.navIconBookshelfSelected,
+            "navIconExploreSelected" to data.navIconExploreSelected,
+            "navIconRssSelected" to data.navIconRssSelected,
+            "navIconMySelected" to data.navIconMySelected,
             "appFontPath" to data.appFontPath,
         )
 
@@ -129,6 +134,16 @@ object ThemeImportExport {
                 navIconExplore = embeddedAssets.paths["navIconExplore"] ?: data.navIconExplore,
                 navIconRss = embeddedAssets.paths["navIconRss"] ?: data.navIconRss,
                 navIconMy = embeddedAssets.paths["navIconMy"] ?: data.navIconMy,
+                navIconHomeSelected = embeddedAssets.paths["navIconHomeSelected"]
+                    ?: data.navIconHomeSelected,
+                navIconBookshelfSelected = embeddedAssets.paths["navIconBookshelfSelected"]
+                    ?: data.navIconBookshelfSelected,
+                navIconExploreSelected = embeddedAssets.paths["navIconExploreSelected"]
+                    ?: data.navIconExploreSelected,
+                navIconRssSelected = embeddedAssets.paths["navIconRssSelected"]
+                    ?: data.navIconRssSelected,
+                navIconMySelected = embeddedAssets.paths["navIconMySelected"]
+                    ?: data.navIconMySelected,
                 appFontPath = embeddedAssets.paths["appFontPath"] ?: data.appFontPath,
                 coverDefaultImage = appliedAssets.lightCoverPaths.joinToString(","),
                 coverDefaultImageDark = appliedAssets.darkCoverPaths.joinToString(","),

+ 89 - 1
app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt

@@ -63,6 +63,11 @@ class ThemePackageManager(
         private const val ASSET_NAV_EXPLORE = "navigation.explore"
         private const val ASSET_NAV_RSS = "navigation.rss"
         private const val ASSET_NAV_MY = "navigation.my"
+        private const val ASSET_NAV_HOME_SELECTED = "navigation.home.selected"
+        private const val ASSET_NAV_BOOKSHELF_SELECTED = "navigation.bookshelf.selected"
+        private const val ASSET_NAV_EXPLORE_SELECTED = "navigation.explore.selected"
+        private const val ASSET_NAV_RSS_SELECTED = "navigation.rss.selected"
+        private const val ASSET_NAV_MY_SELECTED = "navigation.my.selected"
         private const val ASSET_FONT = "font.app"
     }
 
@@ -152,6 +157,14 @@ class ThemePackageManager(
                             navIconExplore = localAssets[ASSET_NAV_EXPLORE].orEmpty(),
                             navIconRss = localAssets[ASSET_NAV_RSS].orEmpty(),
                             navIconMy = localAssets[ASSET_NAV_MY].orEmpty(),
+                            navIconHomeSelected = localAssets[ASSET_NAV_HOME_SELECTED].orEmpty(),
+                            navIconBookshelfSelected =
+                                localAssets[ASSET_NAV_BOOKSHELF_SELECTED].orEmpty(),
+                            navIconExploreSelected =
+                                localAssets[ASSET_NAV_EXPLORE_SELECTED].orEmpty(),
+                            navIconRssSelected =
+                                localAssets[ASSET_NAV_RSS_SELECTED].orEmpty(),
+                            navIconMySelected = localAssets[ASSET_NAV_MY_SELECTED].orEmpty(),
                             appFontPath = localAssets[ASSET_FONT],
                             coverDefaultImage = "",
                             coverDefaultImageDark = "",
@@ -318,6 +331,12 @@ class ThemePackageManager(
                 navIconExplore = localAssets[ASSET_NAV_EXPLORE].orEmpty(),
                 navIconRss = localAssets[ASSET_NAV_RSS].orEmpty(),
                 navIconMy = localAssets[ASSET_NAV_MY].orEmpty(),
+                navIconHomeSelected = localAssets[ASSET_NAV_HOME_SELECTED].orEmpty(),
+                navIconBookshelfSelected =
+                    localAssets[ASSET_NAV_BOOKSHELF_SELECTED].orEmpty(),
+                navIconExploreSelected = localAssets[ASSET_NAV_EXPLORE_SELECTED].orEmpty(),
+                navIconRssSelected = localAssets[ASSET_NAV_RSS_SELECTED].orEmpty(),
+                navIconMySelected = localAssets[ASSET_NAV_MY_SELECTED].orEmpty(),
                 appFontPath = localAssets[ASSET_FONT],
                 assets = null,
             )
@@ -427,6 +446,12 @@ class ThemePackageManager(
                     navIconExplore = localAssets[ASSET_NAV_EXPLORE].orEmpty(),
                     navIconRss = localAssets[ASSET_NAV_RSS].orEmpty(),
                     navIconMy = localAssets[ASSET_NAV_MY].orEmpty(),
+                    navIconHomeSelected = localAssets[ASSET_NAV_HOME_SELECTED].orEmpty(),
+                    navIconBookshelfSelected =
+                        localAssets[ASSET_NAV_BOOKSHELF_SELECTED].orEmpty(),
+                    navIconExploreSelected = localAssets[ASSET_NAV_EXPLORE_SELECTED].orEmpty(),
+                    navIconRssSelected = localAssets[ASSET_NAV_RSS_SELECTED].orEmpty(),
+                    navIconMySelected = localAssets[ASSET_NAV_MY_SELECTED].orEmpty(),
                     appFontPath = localAssets[ASSET_FONT],
                     coverDefaultImage = "",
                     coverDefaultImageDark = "",
@@ -544,6 +569,31 @@ class ThemePackageManager(
             "navIconExplore" to AssetSource(ASSET_NAV_EXPLORE, null, "assets/navigation/explore"),
             "navIconRss" to AssetSource(ASSET_NAV_RSS, null, "assets/navigation/rss"),
             "navIconMy" to AssetSource(ASSET_NAV_MY, null, "assets/navigation/my"),
+            "navIconHomeSelected" to AssetSource(
+                ASSET_NAV_HOME_SELECTED,
+                null,
+                "assets/navigation/home-selected",
+            ),
+            "navIconBookshelfSelected" to AssetSource(
+                ASSET_NAV_BOOKSHELF_SELECTED,
+                null,
+                "assets/navigation/bookshelf-selected",
+            ),
+            "navIconExploreSelected" to AssetSource(
+                ASSET_NAV_EXPLORE_SELECTED,
+                null,
+                "assets/navigation/explore-selected",
+            ),
+            "navIconRssSelected" to AssetSource(
+                ASSET_NAV_RSS_SELECTED,
+                null,
+                "assets/navigation/rss-selected",
+            ),
+            "navIconMySelected" to AssetSource(
+                ASSET_NAV_MY_SELECTED,
+                null,
+                "assets/navigation/my-selected",
+            ),
             "appFontPath" to AssetSource(ASSET_FONT, null, "assets/fonts/app"),
         )
         config.assets.orEmpty().forEach { (legacyKey, base64) ->
@@ -707,7 +757,10 @@ class ThemePackageManager(
                 }
 
                 ASSET_NAV_HOME, ASSET_NAV_BOOKSHELF, ASSET_NAV_EXPLORE,
-                ASSET_NAV_RSS, ASSET_NAV_MY -> File(context.filesDir, "nav_icons")
+                ASSET_NAV_RSS, ASSET_NAV_MY,
+                ASSET_NAV_HOME_SELECTED, ASSET_NAV_BOOKSHELF_SELECTED,
+                ASSET_NAV_EXPLORE_SELECTED, ASSET_NAV_RSS_SELECTED,
+                ASSET_NAV_MY_SELECTED -> File(context.filesDir, "nav_icons")
 
                 ASSET_FONT -> File(context.filesDir, "fonts")
                 else -> return@mapNotNull null
@@ -938,6 +991,11 @@ class ThemePackageManager(
         navIconExplore = "",
         navIconRss = "",
         navIconMy = "",
+        navIconHomeSelected = "",
+        navIconBookshelfSelected = "",
+        navIconExploreSelected = "",
+        navIconRssSelected = "",
+        navIconMySelected = "",
         appFontPath = null,
         coverDefaultImage = "",
         coverDefaultImageDark = "",
@@ -981,6 +1039,31 @@ class ThemePackageManager(
         AssetSource(ASSET_NAV_EXPLORE, config.navIconExplore, "assets/navigation/explore"),
         AssetSource(ASSET_NAV_RSS, config.navIconRss, "assets/navigation/rss"),
         AssetSource(ASSET_NAV_MY, config.navIconMy, "assets/navigation/my"),
+        AssetSource(
+            ASSET_NAV_HOME_SELECTED,
+            config.navIconHomeSelected,
+            "assets/navigation/home-selected",
+        ),
+        AssetSource(
+            ASSET_NAV_BOOKSHELF_SELECTED,
+            config.navIconBookshelfSelected,
+            "assets/navigation/bookshelf-selected",
+        ),
+        AssetSource(
+            ASSET_NAV_EXPLORE_SELECTED,
+            config.navIconExploreSelected,
+            "assets/navigation/explore-selected",
+        ),
+        AssetSource(
+            ASSET_NAV_RSS_SELECTED,
+            config.navIconRssSelected,
+            "assets/navigation/rss-selected",
+        ),
+        AssetSource(
+            ASSET_NAV_MY_SELECTED,
+            config.navIconMySelected,
+            "assets/navigation/my-selected",
+        ),
         AssetSource(ASSET_FONT, config.appFontPath, "assets/fonts/app"),
     )
 
@@ -1079,6 +1162,11 @@ class ThemePackageManager(
                 ASSET_NAV_EXPLORE -> "navIconExplore"
                 ASSET_NAV_RSS -> "navIconRss"
                 ASSET_NAV_MY -> "navIconMy"
+                ASSET_NAV_HOME_SELECTED -> "navIconHomeSelected"
+                ASSET_NAV_BOOKSHELF_SELECTED -> "navIconBookshelfSelected"
+                ASSET_NAV_EXPLORE_SELECTED -> "navIconExploreSelected"
+                ASSET_NAV_RSS_SELECTED -> "navIconRssSelected"
+                ASSET_NAV_MY_SELECTED -> "navIconMySelected"
                 ASSET_FONT -> "appFontPath"
                 else -> key
             }

+ 48 - 28
app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt

@@ -12,9 +12,12 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
@@ -25,10 +28,11 @@ import io.legado.app.R
 import io.legado.app.domain.model.settings.AppShellSettings
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcons
+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.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 
@@ -145,40 +149,56 @@ private fun NavigationIconSlot(
     onSelect: () -> Unit,
     onClear: () -> Unit,
 ) {
-    NormalCard(
-        onClick = onSelect,
-        cornerRadius = 12.dp,
-        containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        modifier = Modifier.size(40.dp),
-    ) {
-        if (path.isNotEmpty()) {
-            Box(modifier = Modifier.fillMaxSize()) {
+    var menuExpanded by remember { mutableStateOf(false) }
+    Box {
+        NormalCard(
+            onClick = {
+                if (path.isNotEmpty()) menuExpanded = true else onSelect()
+            },
+            cornerRadius = 12.dp,
+            containerColor = LegadoTheme.colorScheme.surfaceContainer,
+            modifier = Modifier.size(40.dp),
+        ) {
+            if (path.isNotEmpty()) {
                 AsyncImage(
                     model = path,
                     contentDescription = label,
                     modifier = Modifier.fillMaxSize(),
                     contentScale = ContentScale.Fit,
                 )
-                SmallTonalButton(
-                    onClick = onClear,
-                    modifier = Modifier
-                        .align(Alignment.TopEnd)
-                        .padding(4.dp)
-                        .size(24.dp),
-                    icon = Icons.Default.Close,
-                    contentDescription = stringResource(R.string.close),
-                )
-            }
-        } else {
-            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                AppIcon(
-                    imageVector = Icons.Default.Add,
-                    contentDescription = stringResource(R.string.theme_config_add_nav_icon, label),
-                    modifier = Modifier.size(24.dp),
-                    tint = LegadoTheme.colorScheme.primary,
-                )
+            } else {
+                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                    AppIcon(
+                        imageVector = Icons.Default.Add,
+                        contentDescription = stringResource(
+                            R.string.theme_config_add_nav_icon,
+                            label
+                        ),
+                        modifier = Modifier.size(24.dp),
+                        tint = LegadoTheme.colorScheme.primary,
+                    )
+                }
             }
         }
+        RoundDropdownMenu(
+            expanded = menuExpanded,
+            onDismissRequest = { menuExpanded = false },
+        ) { dismiss ->
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.theme_config_replace_nav_icon),
+                onClick = {
+                    dismiss()
+                    onSelect()
+                },
+            )
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.delete),
+                onClick = {
+                    dismiss()
+                    onClear()
+                },
+            )
+        }
     }
 }
 

+ 15 - 3
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigRouteScreen.kt

@@ -14,6 +14,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.constant.EventBus
 import io.legado.app.help.LauncherIconHelp
 import io.legado.app.help.config.ThemeConfigStore
+import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
@@ -50,9 +51,20 @@ fun ThemeConfigRouteScreen(
         if (uri != null && destination != null) {
             runCatching {
                 val iconDir = File(context.filesDir, "nav_icons").apply { mkdirs() }
-                val destinationFile = File(iconDir, "${destination.replace(':', '-')}.png")
-                context.contentResolver.openInputStream(uri)?.use { input ->
-                    destinationFile.outputStream().use(input::copyTo)
+                val input = context.contentResolver.openInputStream(uri)
+                val destinationFile = if (input != null) {
+                    // 以内容摘要命名:同一张图重复选择命中同一路径,不同图片路径不同,
+                    // 避免覆盖固定文件名导致 Coil 缓存到旧图、修改后无法即时生效。
+                    val digest = input.use(MD5Utils::md5Encode)
+                    val file = File(iconDir, "$digest.png")
+                    if (!file.exists()) {
+                        context.contentResolver.openInputStream(uri)?.use { stream ->
+                            file.outputStream().use(stream::copyTo)
+                        }
+                    }
+                    file
+                } else {
+                    return@runCatching
                 }
                 viewModel.onIntent(
                     ThemeConfigIntent.SelectNavigationIcon(

+ 43 - 4
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -215,9 +215,8 @@ class ThemeConfigViewModel(
                 .map(::File)
                 .filter { it.absolutePath.startsWith(appCtx.externalFiles.absolutePath) }
                 .forEach { it.delete() }
-            listOf("home", "bookshelf", "explore", "rss", "my")
-                .map { File(appCtx.filesDir, "nav_icons/$it.png") }
-                .forEach { it.delete() }
+            // 图标文件现在按内容摘要命名,直接清空整个目录
+            File(appCtx.filesDir, "nav_icons").deleteRecursively()
             currentTheme.appFontPath?.let { path ->
                 File(path)
                     .takeIf { it.absolutePath.startsWith(appCtx.filesDir.absolutePath) }
@@ -329,6 +328,8 @@ class ThemeConfigViewModel(
     }
 
     private fun selectNavigationIcon(intent: ThemeConfigIntent.SelectNavigationIcon) {
+        val current = appShellSettingsGateway.currentSettings
+        val oldPath = current.navIconPath(intent.destination)
         val transform: (AppShellSettings) -> AppShellSettings = when (intent.destination) {
             MainDestination.Home.route -> { settings ->
                 settings.copy(navIconHome = intent.path)
@@ -366,9 +367,47 @@ class ThemeConfigViewModel(
             }
             else -> return
         }
-        updateAppShell(transform = transform)
+        viewModelScope.launch(Dispatchers.IO) {
+            runCatching {
+                // 文件按内容摘要命名,多个槽位可能引用同一文件;
+                // 只有不再被任何槽位引用时才删除,避免误删共享图片。
+                if (oldPath.isNotEmpty() && oldPath != intent.path &&
+                    current.navIconFileUsage(oldPath) <= 1
+                ) {
+                    File(oldPath).delete()
+                }
+            }
+            appShellSettingsGateway.update(transform)
+        }
     }
 
+    private fun AppShellSettings.navIconPath(destination: String): String = when (destination) {
+        MainDestination.Home.route -> navIconHome
+        MainDestination.Bookshelf.route -> navIconBookshelf
+        MainDestination.Explore.route -> navIconExplore
+        MainDestination.Rss.route -> navIconRss
+        MainDestination.My.route -> navIconMy
+        "${MainDestination.Home.route}:selected" -> navIconHomeSelected
+        "${MainDestination.Bookshelf.route}:selected" -> navIconBookshelfSelected
+        "${MainDestination.Explore.route}:selected" -> navIconExploreSelected
+        "${MainDestination.Rss.route}:selected" -> navIconRssSelected
+        "${MainDestination.My.route}:selected" -> navIconMySelected
+        else -> ""
+    }
+
+    private fun AppShellSettings.navIconFileUsage(path: String): Int = listOf(
+        navIconHome,
+        navIconBookshelf,
+        navIconExplore,
+        navIconRss,
+        navIconMy,
+        navIconHomeSelected,
+        navIconBookshelfSelected,
+        navIconExploreSelected,
+        navIconRssSelected,
+        navIconMySelected,
+    ).count { it == path }
+
     private fun setTime(field: ThemeTimeField, value: String) {
         updateTheme {
             when (field) {

+ 1 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1566,6 +1566,7 @@
     <string name="theme_config_add_nav_icon">添加%1$s图标</string>
     <string name="theme_config_nav_icon_unselected">未选中</string>
     <string name="theme_config_nav_icon_selected">已选中</string>
+    <string name="theme_config_replace_nav_icon">更换图片</string>
     <string name="theme_config_no_font_files">没有字体文件</string>
     <string name="theme_config_reset_defaults">恢复默认主题配置</string>
     <string name="theme_config_reset_defaults_summary">恢复外观页设置,不影响已保存主题和封面</string>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1625,6 +1625,7 @@
     <string name="theme_config_add_nav_icon">Add %1$s icon</string>
     <string name="theme_config_nav_icon_unselected">Unselected</string>
     <string name="theme_config_nav_icon_selected">Selected</string>
+    <string name="theme_config_replace_nav_icon">Replace image</string>
     <string name="theme_config_no_font_files">No font files</string>
     <string name="input_value_range">Enter value (%1$d-%2$d)</string>
     <string name="change_icon_success">Icon Updated</string>