新的独属于MD3的雷霆主题管理与分享系统
This commit is contained in:
11 files changed
+466
-1081
No files matched your search
@@ -1,109 +0,0 @@
|
|||||||
package io.legado.app.help.config
|
|
||||||
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|
||||||
|
|
||||||
data class ThemeAppearanceConfig(
|
|
||||||
var enableDeepPersonalization: Boolean = false,
|
|
||||||
var themeColor: Int = 0,
|
|
||||||
var secondaryThemeColor: Int = 0,
|
|
||||||
var primaryTextColor: Int = 0,
|
|
||||||
var secondaryTextColor: Int = 0,
|
|
||||||
var backgroundColor: Int = 0,
|
|
||||||
var labelContainerColor: Int = 0,
|
|
||||||
var bookInfoInputColor: Int = 0,
|
|
||||||
var appFontPath: String? = null,
|
|
||||||
var enableContainerBorder: Boolean = false,
|
|
||||||
var containerBorderWidth: Float = 1f,
|
|
||||||
var containerBorderStyle: String = "solid",
|
|
||||||
var containerBorderDashWidth: Float = 4f,
|
|
||||||
var containerBorderColor: Int = 0,
|
|
||||||
var enableItemDivider: Boolean = true,
|
|
||||||
var itemDividerWidth: Float = 1f,
|
|
||||||
var itemDividerLength: Float = 80f,
|
|
||||||
var itemDividerColor: Int = 0,
|
|
||||||
var enableCustomTagColors: Boolean = false,
|
|
||||||
var customTagColorsJson: String? = null,
|
|
||||||
var enableBlur: Boolean = false,
|
|
||||||
var topBarBlurRadius: Int = 24,
|
|
||||||
var bottomBarBlurRadius: Int = 8,
|
|
||||||
var topBarBlurAlpha: Int = 73,
|
|
||||||
var bottomBarBlurAlpha: Int = 40,
|
|
||||||
var bottomBarLensRadius: Float = 24f
|
|
||||||
) {
|
|
||||||
|
|
||||||
val hasDeepColorOverrides: Boolean
|
|
||||||
get() = themeColor != 0 ||
|
|
||||||
secondaryThemeColor != 0 ||
|
|
||||||
primaryTextColor != 0 ||
|
|
||||||
secondaryTextColor != 0 ||
|
|
||||||
backgroundColor != 0 ||
|
|
||||||
labelContainerColor != 0
|
|
||||||
|
|
||||||
fun applyToThemeConfig(appFontPathOverride: String? = appFontPath) {
|
|
||||||
ThemeConfig.enableDeepPersonalization = enableDeepPersonalization
|
|
||||||
ThemeConfig.themeColor = themeColor
|
|
||||||
ThemeConfig.secondaryThemeColor = secondaryThemeColor
|
|
||||||
ThemeConfig.primaryTextColor = primaryTextColor
|
|
||||||
ThemeConfig.secondaryTextColor = secondaryTextColor
|
|
||||||
ThemeConfig.themeBackgroundColor = backgroundColor
|
|
||||||
ThemeConfig.labelContainerColor = labelContainerColor
|
|
||||||
|
|
||||||
ThemeConfig.bookInfoInputColor = bookInfoInputColor
|
|
||||||
|
|
||||||
ThemeConfig.appFontPath = appFontPathOverride
|
|
||||||
|
|
||||||
ThemeConfig.enableContainerBorder = enableContainerBorder
|
|
||||||
ThemeConfig.containerBorderWidth = containerBorderWidth
|
|
||||||
ThemeConfig.containerBorderStyle = containerBorderStyle.ifBlank { "solid" }
|
|
||||||
ThemeConfig.containerBorderDashWidth = containerBorderDashWidth
|
|
||||||
ThemeConfig.containerBorderColor = containerBorderColor
|
|
||||||
|
|
||||||
ThemeConfig.enableItemDivider = enableItemDivider
|
|
||||||
ThemeConfig.itemDividerWidth = itemDividerWidth
|
|
||||||
ThemeConfig.itemDividerLength = itemDividerLength
|
|
||||||
ThemeConfig.itemDividerColor = itemDividerColor
|
|
||||||
|
|
||||||
ThemeConfig.enableCustomTagColors = enableCustomTagColors
|
|
||||||
ThemeConfig.customTagColorsJson = ThemeAppearanceJson.convertTagColorsFromHex(customTagColorsJson)
|
|
||||||
|
|
||||||
ThemeConfig.enableBlur = enableBlur
|
|
||||||
ThemeConfig.topBarBlurRadius = topBarBlurRadius
|
|
||||||
ThemeConfig.bottomBarBlurRadius = bottomBarBlurRadius
|
|
||||||
ThemeConfig.topBarBlurAlpha = topBarBlurAlpha
|
|
||||||
ThemeConfig.bottomBarBlurAlpha = bottomBarBlurAlpha
|
|
||||||
ThemeConfig.bottomBarLensRadius = bottomBarLensRadius
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromCurrent(): ThemeAppearanceConfig {
|
|
||||||
return ThemeAppearanceConfig(
|
|
||||||
enableDeepPersonalization = ThemeConfig.enableDeepPersonalization,
|
|
||||||
themeColor = ThemeConfig.themeColor,
|
|
||||||
secondaryThemeColor = ThemeConfig.secondaryThemeColor,
|
|
||||||
primaryTextColor = ThemeConfig.primaryTextColor,
|
|
||||||
secondaryTextColor = ThemeConfig.secondaryTextColor,
|
|
||||||
backgroundColor = ThemeConfig.themeBackgroundColor,
|
|
||||||
labelContainerColor = ThemeConfig.labelContainerColor,
|
|
||||||
bookInfoInputColor = ThemeConfig.bookInfoInputColor,
|
|
||||||
appFontPath = ThemeConfig.appFontPath,
|
|
||||||
enableContainerBorder = ThemeConfig.enableContainerBorder,
|
|
||||||
containerBorderWidth = ThemeConfig.containerBorderWidth,
|
|
||||||
containerBorderStyle = ThemeConfig.containerBorderStyle,
|
|
||||||
containerBorderDashWidth = ThemeConfig.containerBorderDashWidth,
|
|
||||||
containerBorderColor = ThemeConfig.containerBorderColor,
|
|
||||||
enableItemDivider = ThemeConfig.enableItemDivider,
|
|
||||||
itemDividerWidth = ThemeConfig.itemDividerWidth,
|
|
||||||
itemDividerLength = ThemeConfig.itemDividerLength,
|
|
||||||
itemDividerColor = ThemeConfig.itemDividerColor,
|
|
||||||
enableCustomTagColors = ThemeConfig.enableCustomTagColors,
|
|
||||||
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
|
||||||
enableBlur = ThemeConfig.enableBlur,
|
|
||||||
topBarBlurRadius = ThemeConfig.topBarBlurRadius,
|
|
||||||
bottomBarBlurRadius = ThemeConfig.bottomBarBlurRadius,
|
|
||||||
topBarBlurAlpha = ThemeConfig.topBarBlurAlpha,
|
|
||||||
bottomBarBlurAlpha = ThemeConfig.bottomBarBlurAlpha,
|
|
||||||
bottomBarLensRadius = ThemeConfig.bottomBarLensRadius
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package io.legado.app.help.config
|
|
||||||
|
|
||||||
import androidx.core.graphics.toColorInt
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import io.legado.app.ui.config.themeConfig.TagColorPair
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.getBoolean
|
|
||||||
import io.legado.app.utils.getFloat
|
|
||||||
import io.legado.app.utils.getString
|
|
||||||
|
|
||||||
object ThemeAppearanceJson {
|
|
||||||
|
|
||||||
fun writeTo(obj: JsonObject, src: ThemeAppearanceConfig) {
|
|
||||||
obj.addProperty("enableDeepPersonalization", src.enableDeepPersonalization)
|
|
||||||
obj.addProperty("themeColor", colorToHex(src.themeColor))
|
|
||||||
obj.addProperty("secondaryThemeColor", colorToHex(src.secondaryThemeColor))
|
|
||||||
obj.addProperty("primaryTextColor", colorToHex(src.primaryTextColor))
|
|
||||||
obj.addProperty("secondaryTextColor", colorToHex(src.secondaryTextColor))
|
|
||||||
obj.addProperty("backgroundColor", colorToHex(src.backgroundColor))
|
|
||||||
obj.addProperty("labelContainerColor", colorToHex(src.labelContainerColor))
|
|
||||||
obj.addProperty("bookInfoInputColor", colorToHex(src.bookInfoInputColor))
|
|
||||||
obj.addProperty("appFontPath", src.appFontPath)
|
|
||||||
obj.addProperty("enableContainerBorder", src.enableContainerBorder)
|
|
||||||
obj.addProperty("containerBorderWidth", src.containerBorderWidth)
|
|
||||||
obj.addProperty("containerBorderStyle", src.containerBorderStyle)
|
|
||||||
obj.addProperty("containerBorderDashWidth", src.containerBorderDashWidth)
|
|
||||||
obj.addProperty("containerBorderColor", colorToHex(src.containerBorderColor))
|
|
||||||
obj.addProperty("enableItemDivider", src.enableItemDivider)
|
|
||||||
obj.addProperty("itemDividerWidth", src.itemDividerWidth)
|
|
||||||
obj.addProperty("itemDividerLength", src.itemDividerLength)
|
|
||||||
obj.addProperty("itemDividerColor", colorToHex(src.itemDividerColor))
|
|
||||||
obj.addProperty("enableCustomTagColors", src.enableCustomTagColors)
|
|
||||||
obj.addProperty("customTagColorsJson", convertTagColorsToHex(src.customTagColorsJson))
|
|
||||||
obj.addProperty("enableBlur", src.enableBlur)
|
|
||||||
obj.addProperty("topBarBlurRadius", src.topBarBlurRadius)
|
|
||||||
obj.addProperty("bottomBarBlurRadius", src.bottomBarBlurRadius)
|
|
||||||
obj.addProperty("topBarBlurAlpha", src.topBarBlurAlpha)
|
|
||||||
obj.addProperty("bottomBarBlurAlpha", src.bottomBarBlurAlpha)
|
|
||||||
obj.addProperty("bottomBarLensRadius", src.bottomBarLensRadius)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readFrom(obj: JsonObject): ThemeAppearanceConfig {
|
|
||||||
val appearance = ThemeAppearanceConfig(
|
|
||||||
themeColor = readColor(obj, "themeColor", "md3Primary"),
|
|
||||||
secondaryThemeColor = readColor(obj, "secondaryThemeColor", "md3Secondary", "md3OnPrimary"),
|
|
||||||
primaryTextColor = readColor(obj, "primaryTextColor", "md3OnSurface"),
|
|
||||||
secondaryTextColor = readColor(obj, "secondaryTextColor", "md3OnPrimaryContainer"),
|
|
||||||
backgroundColor = readColor(obj, "backgroundColor", "md3Background"),
|
|
||||||
labelContainerColor = readColor(obj, "labelContainerColor", "md3SurfaceContainerLow"),
|
|
||||||
bookInfoInputColor = readColor(obj, "bookInfoInputColor"),
|
|
||||||
appFontPath = obj.getString("appFontPath", null),
|
|
||||||
enableContainerBorder = obj.getBoolean("enableContainerBorder", false),
|
|
||||||
containerBorderWidth = obj.getFloat("containerBorderWidth", 1f),
|
|
||||||
containerBorderStyle = obj.getString("containerBorderStyle", "solid") ?: "solid",
|
|
||||||
containerBorderDashWidth = obj.getFloat("containerBorderDashWidth", 4f),
|
|
||||||
containerBorderColor = readColor(obj, "containerBorderColor"),
|
|
||||||
enableItemDivider = obj.getBoolean("enableItemDivider", true),
|
|
||||||
itemDividerWidth = obj.getFloat("itemDividerWidth", 1f),
|
|
||||||
itemDividerLength = obj.getFloat("itemDividerLength", 80f),
|
|
||||||
itemDividerColor = readColor(obj, "itemDividerColor"),
|
|
||||||
enableCustomTagColors = obj.getBoolean("enableCustomTagColors", false),
|
|
||||||
customTagColorsJson = obj.getString("customTagColorsJson", null),
|
|
||||||
enableBlur = obj.getBoolean("enableBlur", false),
|
|
||||||
topBarBlurRadius = obj.getInt("topBarBlurRadius", 24),
|
|
||||||
bottomBarBlurRadius = obj.getInt("bottomBarBlurRadius", 8),
|
|
||||||
topBarBlurAlpha = obj.getInt("topBarBlurAlpha", 73),
|
|
||||||
bottomBarBlurAlpha = obj.getInt("bottomBarBlurAlpha", 40),
|
|
||||||
bottomBarLensRadius = obj.getFloat("bottomBarLensRadius", 24f)
|
|
||||||
)
|
|
||||||
appearance.enableDeepPersonalization = if (obj.has("enableDeepPersonalization")) {
|
|
||||||
obj.getBoolean("enableDeepPersonalization", false)
|
|
||||||
} else {
|
|
||||||
appearance.hasDeepColorOverrides
|
|
||||||
}
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertTagColorsFromHex(json: String?): String? {
|
|
||||||
if (json.isNullOrBlank()) return json
|
|
||||||
return kotlin.runCatching {
|
|
||||||
val root = JsonParser.parseString(json)
|
|
||||||
if (root.isJsonArray) {
|
|
||||||
val arr = root.asJsonArray
|
|
||||||
val converted = arr.map { elem ->
|
|
||||||
val obj = elem.asJsonObject
|
|
||||||
val textColor = parseColorFromJson(obj.get("textColor"))
|
|
||||||
val bgColor = parseColorFromJson(obj.get("bgColor"))
|
|
||||||
"""{"textColor":$textColor,"bgColor":$bgColor}"""
|
|
||||||
}
|
|
||||||
"[\n ${converted.joinToString(",\n ")}\n]"
|
|
||||||
} else json
|
|
||||||
}.getOrNull() ?: json
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readColor(obj: JsonObject, vararg names: String): Int {
|
|
||||||
for (name in names) {
|
|
||||||
val color = obj.getInt(name, Int.MIN_VALUE)
|
|
||||||
if (color != Int.MIN_VALUE) return color
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun colorToHex(color: Int): String = "#${String.format("%08X", color)}"
|
|
||||||
|
|
||||||
private fun convertTagColorsToHex(json: String?): String? {
|
|
||||||
if (json.isNullOrBlank()) return json
|
|
||||||
return kotlin.runCatching {
|
|
||||||
val colors = GSON.fromJson(json, Array<TagColorPair>::class.java)
|
|
||||||
val converted = colors.map { pair ->
|
|
||||||
val textHex = colorToHex(pair.textColor)
|
|
||||||
val bgHex = colorToHex(pair.bgColor)
|
|
||||||
"""{"textColor":"$textHex","bgColor":"$bgHex"}"""
|
|
||||||
}
|
|
||||||
"[\n ${converted.joinToString(",\n ")}\n]"
|
|
||||||
}.getOrNull() ?: json
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseColorFromJson(elem: JsonElement?): Int {
|
|
||||||
if (elem == null || elem.isJsonNull) return 0
|
|
||||||
val prim = elem.asJsonPrimitive
|
|
||||||
return when {
|
|
||||||
prim.isNumber -> prim.asNumber.toInt()
|
|
||||||
prim.isString -> parseColorString(prim.asString)
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JsonObject.getInt(name: String, default: Int): Int {
|
|
||||||
val elem = get(name)
|
|
||||||
return when {
|
|
||||||
elem == null || elem.isJsonNull -> default
|
|
||||||
elem.isJsonPrimitive -> {
|
|
||||||
val prim = elem.asJsonPrimitive
|
|
||||||
when {
|
|
||||||
prim.isNumber -> prim.asNumber.toInt()
|
|
||||||
prim.isString -> parseColorString(prim.asString)
|
|
||||||
else -> default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseColorString(colorStr: String): Int {
|
|
||||||
val text = colorStr.trim()
|
|
||||||
if (text.isEmpty()) return 0
|
|
||||||
return kotlin.runCatching {
|
|
||||||
if (text.startsWith("#")) {
|
|
||||||
text.substring(1).toLong(16).toInt()
|
|
||||||
} else {
|
|
||||||
text.toLong().toInt()
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
|
||||||
kotlin.runCatching { text.toColorInt() }.getOrDefault(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,17 @@ object ThemeImportExport {
|
|||||||
*/
|
*/
|
||||||
fun saveCurrentAsTheme(name: String): SavedTheme {
|
fun saveCurrentAsTheme(name: String): SavedTheme {
|
||||||
val data = exportFromCurrent()
|
val data = exportFromCurrent()
|
||||||
|
return saveThemeData(name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存指定数据为新主题
|
||||||
|
*/
|
||||||
|
fun saveCurrentAsTheme(name: String, data: ThemeExportData): SavedTheme {
|
||||||
|
return saveThemeData(name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveThemeData(name: String, data: ThemeExportData): SavedTheme {
|
||||||
val file = File(baseDir, "$name.json")
|
val file = File(baseDir, "$name.json")
|
||||||
baseDir.mkdirs()
|
baseDir.mkdirs()
|
||||||
file.writeText(EXPORT_GSON.toJson(data))
|
file.writeText(EXPORT_GSON.toJson(data))
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
package io.legado.app.help.config
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
|
||||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|
||||||
import io.legado.app.utils.FileUtils
|
|
||||||
import io.legado.app.utils.externalFiles
|
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
object ThemePackConfig {
|
|
||||||
|
|
||||||
private const val CONFIG_FILE = "theme.json"
|
|
||||||
private const val DIR_NAME = "theme_packs"
|
|
||||||
private val baseDir get() = File(appCtx.filesDir, DIR_NAME)
|
|
||||||
|
|
||||||
private val PACK_GSON: Gson by lazy {
|
|
||||||
GsonBuilder()
|
|
||||||
.registerTypeAdapter(ThemePack::class.java, ThemePackSerializer())
|
|
||||||
.setPrettyPrinting()
|
|
||||||
.disableHtmlEscaping()
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _packList = mutableListOf<ThemePack>()
|
|
||||||
val packList: List<ThemePack> get() = _packList
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadAll() {
|
|
||||||
_packList.clear()
|
|
||||||
baseDir.mkdirs()
|
|
||||||
baseDir.listFiles()?.forEach { dir ->
|
|
||||||
if (dir.isDirectory) {
|
|
||||||
val configFile = File(dir, CONFIG_FILE)
|
|
||||||
if (configFile.exists()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val json = configFile.readText()
|
|
||||||
val pack = PACK_GSON.fromJson(json, ThemePack::class.java)
|
|
||||||
if (pack.name.isNotEmpty()) {
|
|
||||||
pack.folderName = dir.name
|
|
||||||
_packList.add(pack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reload() {
|
|
||||||
loadAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackDir(pack: ThemePack): File {
|
|
||||||
return File(baseDir, pack.folderName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePack(pack: ThemePack) {
|
|
||||||
val dir = getPackDir(pack)
|
|
||||||
dir.mkdirs()
|
|
||||||
val configFile = File(dir, CONFIG_FILE)
|
|
||||||
val json = PACK_GSON.toJson(pack)
|
|
||||||
FileUtils.createFileIfNotExist(configFile.absolutePath).writeText(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deletePack(pack: ThemePack) {
|
|
||||||
val dir = getPackDir(pack)
|
|
||||||
if (dir.exists()) {
|
|
||||||
dir.deleteRecursively()
|
|
||||||
}
|
|
||||||
_packList.remove(pack)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createFromCurrent(name: String): ThemePack {
|
|
||||||
val folderName = System.currentTimeMillis().toString()
|
|
||||||
val pack = ThemePack(
|
|
||||||
name = name,
|
|
||||||
folderName = folderName,
|
|
||||||
appearance = ThemeAppearanceConfig.fromCurrent(),
|
|
||||||
bgImageLight = ThemeConfig.bgImageLight,
|
|
||||||
bgImageDark = ThemeConfig.bgImageDark,
|
|
||||||
bgImageBlurring = ThemeConfig.bgImageBlurring,
|
|
||||||
bgImageNBlurring = ThemeConfig.bgImageNBlurring,
|
|
||||||
navIconBookshelf = MainConfig.navIconBookshelf,
|
|
||||||
navIconExplore = MainConfig.navIconExplore,
|
|
||||||
navIconRss = MainConfig.navIconRss,
|
|
||||||
navIconMy = MainConfig.navIconMy,
|
|
||||||
coverTextColor = CoverConfig.coverTextColor,
|
|
||||||
coverShadowColor = CoverConfig.coverShadowColor,
|
|
||||||
coverTextColorN = CoverConfig.coverTextColorN,
|
|
||||||
coverShadowColorN = CoverConfig.coverShadowColorN,
|
|
||||||
coverShowName = CoverConfig.coverShowName,
|
|
||||||
coverShowAuthor = CoverConfig.coverShowAuthor,
|
|
||||||
coverShowNameN = CoverConfig.coverShowNameN,
|
|
||||||
coverShowAuthorN = CoverConfig.coverShowAuthorN,
|
|
||||||
coverDefaultColor = CoverConfig.coverDefaultColor,
|
|
||||||
coverShowShadow = CoverConfig.coverShowShadow,
|
|
||||||
coverShowStroke = CoverConfig.coverShowStroke,
|
|
||||||
coverInfoOrientation = CoverConfig.coverInfoOrientation,
|
|
||||||
defaultCover = CoverConfig.defaultCover,
|
|
||||||
defaultCoverDark = CoverConfig.defaultCoverDark
|
|
||||||
)
|
|
||||||
|
|
||||||
val dir = File(baseDir, folderName)
|
|
||||||
dir.mkdirs()
|
|
||||||
|
|
||||||
copyResourceFile(pack.appearance.appFontPath, dir, "app_font")
|
|
||||||
copyResourceFile(pack.bgImageLight, dir, "bg_light")
|
|
||||||
copyResourceFile(pack.bgImageDark, dir, "bg_dark")
|
|
||||||
copyResourceFile(pack.navIconBookshelf, dir, "nav_bookshelf")
|
|
||||||
copyResourceFile(pack.navIconExplore, dir, "nav_explore")
|
|
||||||
copyResourceFile(pack.navIconRss, dir, "nav_rss")
|
|
||||||
copyResourceFile(pack.navIconMy, dir, "nav_my")
|
|
||||||
|
|
||||||
copyCoverImages(pack.defaultCover, dir, "cover_day")
|
|
||||||
copyCoverImages(pack.defaultCoverDark, dir, "cover_night")
|
|
||||||
|
|
||||||
savePack(pack)
|
|
||||||
_packList.add(pack)
|
|
||||||
return pack
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyPack(pack: ThemePack) {
|
|
||||||
val dir = getPackDir(pack)
|
|
||||||
val restoredAppearance = pack.appearance.copy(
|
|
||||||
appFontPath = restoreResourcePath(dir, "app_font", pack.appearance.appFontPath)
|
|
||||||
)
|
|
||||||
restoredAppearance.applyToThemeConfig()
|
|
||||||
|
|
||||||
ThemeConfig.bgImageLight = restoreResourcePath(dir, "bg_light", pack.bgImageLight)
|
|
||||||
ThemeConfig.bgImageDark = restoreResourcePath(dir, "bg_dark", pack.bgImageDark)
|
|
||||||
ThemeConfig.bgImageBlurring = pack.bgImageBlurring
|
|
||||||
ThemeConfig.bgImageNBlurring = pack.bgImageNBlurring
|
|
||||||
|
|
||||||
MainConfig.navIconBookshelf = restoreResourcePath(dir, "nav_bookshelf", pack.navIconBookshelf) ?: ""
|
|
||||||
MainConfig.navIconExplore = restoreResourcePath(dir, "nav_explore", pack.navIconExplore) ?: ""
|
|
||||||
MainConfig.navIconRss = restoreResourcePath(dir, "nav_rss", pack.navIconRss) ?: ""
|
|
||||||
MainConfig.navIconMy = restoreResourcePath(dir, "nav_my", pack.navIconMy) ?: ""
|
|
||||||
|
|
||||||
CoverConfig.coverTextColor = pack.coverTextColor
|
|
||||||
CoverConfig.coverShadowColor = pack.coverShadowColor
|
|
||||||
CoverConfig.coverTextColorN = pack.coverTextColorN
|
|
||||||
CoverConfig.coverShadowColorN = pack.coverShadowColorN
|
|
||||||
CoverConfig.coverShowName = pack.coverShowName
|
|
||||||
CoverConfig.coverShowAuthor = pack.coverShowAuthor
|
|
||||||
CoverConfig.coverShowNameN = pack.coverShowNameN
|
|
||||||
CoverConfig.coverShowAuthorN = pack.coverShowAuthorN
|
|
||||||
CoverConfig.coverDefaultColor = pack.coverDefaultColor
|
|
||||||
CoverConfig.coverShowShadow = pack.coverShowShadow
|
|
||||||
CoverConfig.coverShowStroke = pack.coverShowStroke
|
|
||||||
CoverConfig.coverInfoOrientation = pack.coverInfoOrientation
|
|
||||||
CoverConfig.defaultCover = restoreCoverImages(dir, "cover_day", pack.defaultCover)
|
|
||||||
CoverConfig.defaultCoverDark = restoreCoverImages(dir, "cover_night", pack.defaultCoverDark)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exportToZip(pack: ThemePack, destFile: File) {
|
|
||||||
val dir = getPackDir(pack)
|
|
||||||
if (!dir.exists()) return
|
|
||||||
ZipOutputStream(FileOutputStream(destFile)).use { zos ->
|
|
||||||
dir.walkTopDown().forEach { file ->
|
|
||||||
if (file.isFile) {
|
|
||||||
val entryName = file.relativeTo(dir).path.replace('\\', '/')
|
|
||||||
zos.putNextEntry(ZipEntry(entryName))
|
|
||||||
FileInputStream(file).use { fis ->
|
|
||||||
fis.copyTo(zos)
|
|
||||||
}
|
|
||||||
zos.closeEntry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importFromZip(zipFile: File): ThemePack? {
|
|
||||||
val folderName = System.currentTimeMillis().toString()
|
|
||||||
val dir = File(baseDir, folderName)
|
|
||||||
dir.mkdirs()
|
|
||||||
|
|
||||||
return kotlin.runCatching {
|
|
||||||
ZipInputStream(FileInputStream(zipFile)).use { zis ->
|
|
||||||
var entry = zis.nextEntry
|
|
||||||
while (entry != null) {
|
|
||||||
val outFile = resolveZipOutputFile(dir, entry)
|
|
||||||
if (entry.isDirectory) {
|
|
||||||
outFile.mkdirs()
|
|
||||||
} else {
|
|
||||||
outFile.parentFile?.mkdirs()
|
|
||||||
FileOutputStream(outFile).use { fos ->
|
|
||||||
zis.copyTo(fos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zis.closeEntry()
|
|
||||||
entry = zis.nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val configFile = File(dir, CONFIG_FILE)
|
|
||||||
if (!configFile.exists()) {
|
|
||||||
dir.deleteRecursively()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val json = configFile.readText()
|
|
||||||
val pack = PACK_GSON.fromJson(json, ThemePack::class.java)
|
|
||||||
pack.folderName = folderName
|
|
||||||
_packList.add(pack)
|
|
||||||
pack
|
|
||||||
}.onFailure {
|
|
||||||
dir.deleteRecursively()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveZipOutputFile(dir: File, entry: ZipEntry): File {
|
|
||||||
val outFile = File(dir, entry.name).canonicalFile
|
|
||||||
val canonicalDir = dir.canonicalFile
|
|
||||||
if (outFile.path == canonicalDir.path ||
|
|
||||||
outFile.path.startsWith(canonicalDir.path + File.separator)
|
|
||||||
) {
|
|
||||||
return outFile
|
|
||||||
}
|
|
||||||
throw IllegalArgumentException("Invalid theme pack entry: ${entry.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyResourceFile(sourcePath: String?, destDir: File, prefix: String) {
|
|
||||||
if (sourcePath.isNullOrEmpty()) return
|
|
||||||
val sourceUri = Uri.parse(sourcePath)
|
|
||||||
val sourceName = sourceUri.lastPathSegment ?: sourcePath
|
|
||||||
val ext = sourceName.substringAfterLast(".", "dat").ifBlank { "dat" }
|
|
||||||
val destFile = File(destDir, "$prefix.$ext")
|
|
||||||
|
|
||||||
if (sourceUri.scheme == "content") {
|
|
||||||
appCtx.contentResolver.openInputStream(sourceUri)?.use { input ->
|
|
||||||
FileOutputStream(destFile).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val sourceFile = File(sourcePath)
|
|
||||||
if (!sourceFile.exists()) return
|
|
||||||
sourceFile.copyTo(destFile, overwrite = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreResourcePath(packDir: File, prefix: String, originalPath: String?): String? {
|
|
||||||
if (originalPath.isNullOrEmpty()) return null
|
|
||||||
val candidates = packDir.listFiles { file ->
|
|
||||||
file.name.startsWith(prefix) && file.isFile
|
|
||||||
}
|
|
||||||
if (candidates.isNullOrEmpty()) return null
|
|
||||||
val sourceFile = candidates.first()
|
|
||||||
val targetDir = File(appCtx.filesDir, "theme_pack_resources")
|
|
||||||
targetDir.mkdirs()
|
|
||||||
val targetFile = File(targetDir, "${System.currentTimeMillis()}_${sourceFile.name}")
|
|
||||||
sourceFile.copyTo(targetFile, overwrite = true)
|
|
||||||
return targetFile.absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyCoverImages(coverPaths: String, destDir: File, prefix: String) {
|
|
||||||
val paths = coverPaths.split(",").filter { it.isNotBlank() }
|
|
||||||
paths.forEachIndexed { index, path ->
|
|
||||||
copyResourceFile(path, destDir, "${prefix}_$index")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCoverImages(packDir: File, prefix: String, originalPaths: String): String {
|
|
||||||
if (originalPaths.isEmpty()) return ""
|
|
||||||
val candidates = packDir.listFiles { file ->
|
|
||||||
file.name.startsWith(prefix + "_") && file.isFile
|
|
||||||
}
|
|
||||||
if (candidates.isNullOrEmpty()) return originalPaths
|
|
||||||
val sortedCandidates = candidates.sortedBy { file ->
|
|
||||||
file.name.substringAfterLast("_").substringBefore(".").toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
val coverDir = File(appCtx.externalFiles, "covers")
|
|
||||||
coverDir.mkdirs()
|
|
||||||
val restoredPaths = mutableListOf<String>()
|
|
||||||
sortedCandidates.forEach { sourceFile ->
|
|
||||||
val targetFile = File(coverDir, sourceFile.name)
|
|
||||||
sourceFile.copyTo(targetFile, overwrite = true)
|
|
||||||
restoredPaths.add(targetFile.absolutePath)
|
|
||||||
}
|
|
||||||
return if (restoredPaths.isEmpty()) originalPaths else restoredPaths.joinToString(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ThemePack(
|
|
||||||
var name: String = "",
|
|
||||||
@Transient
|
|
||||||
var folderName: String = "",
|
|
||||||
var appearance: ThemeAppearanceConfig = ThemeAppearanceConfig(),
|
|
||||||
var bgImageLight: String? = null,
|
|
||||||
var bgImageDark: String? = null,
|
|
||||||
var bgImageBlurring: Int = 0,
|
|
||||||
var bgImageNBlurring: Int = 0,
|
|
||||||
var navIconBookshelf: String = "",
|
|
||||||
var navIconExplore: String = "",
|
|
||||||
var navIconRss: String = "",
|
|
||||||
var navIconMy: String = "",
|
|
||||||
var coverTextColor: Int = -16777216,
|
|
||||||
var coverShadowColor: Int = -16777216,
|
|
||||||
var coverTextColorN: Int = -1,
|
|
||||||
var coverShadowColorN: Int = -1,
|
|
||||||
var coverShowName: Boolean = true,
|
|
||||||
var coverShowAuthor: Boolean = true,
|
|
||||||
var coverShowNameN: Boolean = true,
|
|
||||||
var coverShowAuthorN: Boolean = true,
|
|
||||||
var coverDefaultColor: Boolean = true,
|
|
||||||
var coverShowShadow: Boolean = false,
|
|
||||||
var coverShowStroke: Boolean = true,
|
|
||||||
var coverInfoOrientation: String = "0",
|
|
||||||
var defaultCover: String = "",
|
|
||||||
var defaultCoverDark: String = ""
|
|
||||||
) {
|
|
||||||
val themeColor: Int get() = appearance.themeColor
|
|
||||||
val appFontPath: String? get() = appearance.appFontPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package io.legado.app.help.config
|
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import io.legado.app.utils.getBoolean
|
|
||||||
import io.legado.app.utils.getInt
|
|
||||||
import io.legado.app.utils.getString
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class ThemePackSerializer : JsonSerializer<ThemePackConfig.ThemePack>,
|
|
||||||
JsonDeserializer<ThemePackConfig.ThemePack> {
|
|
||||||
|
|
||||||
override fun serialize(
|
|
||||||
src: ThemePackConfig.ThemePack,
|
|
||||||
typeOfSrc: Type,
|
|
||||||
context: JsonSerializationContext
|
|
||||||
): JsonElement {
|
|
||||||
val obj = JsonObject()
|
|
||||||
obj.addProperty("name", src.name)
|
|
||||||
ThemeAppearanceJson.writeTo(obj, src.appearance)
|
|
||||||
obj.addProperty("bgImageLight", src.bgImageLight)
|
|
||||||
obj.addProperty("bgImageDark", src.bgImageDark)
|
|
||||||
obj.addProperty("bgImageBlurring", src.bgImageBlurring)
|
|
||||||
obj.addProperty("bgImageNBlurring", src.bgImageNBlurring)
|
|
||||||
obj.addProperty("navIconBookshelf", src.navIconBookshelf)
|
|
||||||
obj.addProperty("navIconExplore", src.navIconExplore)
|
|
||||||
obj.addProperty("navIconRss", src.navIconRss)
|
|
||||||
obj.addProperty("navIconMy", src.navIconMy)
|
|
||||||
obj.addProperty("coverTextColor", ThemeAppearanceJson.colorToHex(src.coverTextColor))
|
|
||||||
obj.addProperty("coverShadowColor", ThemeAppearanceJson.colorToHex(src.coverShadowColor))
|
|
||||||
obj.addProperty("coverTextColorN", ThemeAppearanceJson.colorToHex(src.coverTextColorN))
|
|
||||||
obj.addProperty("coverShadowColorN", ThemeAppearanceJson.colorToHex(src.coverShadowColorN))
|
|
||||||
obj.addProperty("coverShowName", src.coverShowName)
|
|
||||||
obj.addProperty("coverShowAuthor", src.coverShowAuthor)
|
|
||||||
obj.addProperty("coverShowNameN", src.coverShowNameN)
|
|
||||||
obj.addProperty("coverShowAuthorN", src.coverShowAuthorN)
|
|
||||||
obj.addProperty("coverDefaultColor", src.coverDefaultColor)
|
|
||||||
obj.addProperty("coverShowShadow", src.coverShowShadow)
|
|
||||||
obj.addProperty("coverShowStroke", src.coverShowStroke)
|
|
||||||
obj.addProperty("coverInfoOrientation", src.coverInfoOrientation)
|
|
||||||
obj.addProperty("defaultCover", src.defaultCover)
|
|
||||||
obj.addProperty("defaultCoverDark", src.defaultCoverDark)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(
|
|
||||||
json: JsonElement,
|
|
||||||
typeOfT: Type,
|
|
||||||
context: JsonDeserializationContext
|
|
||||||
): ThemePackConfig.ThemePack {
|
|
||||||
val obj = json.asJsonObject
|
|
||||||
return ThemePackConfig.ThemePack(
|
|
||||||
name = obj.getString("name", "") ?: "",
|
|
||||||
appearance = ThemeAppearanceJson.readFrom(obj),
|
|
||||||
bgImageLight = obj.getString("bgImageLight", null),
|
|
||||||
bgImageDark = obj.getString("bgImageDark", null),
|
|
||||||
bgImageBlurring = obj.getInt("bgImageBlurring", 0),
|
|
||||||
bgImageNBlurring = obj.getInt("bgImageNBlurring", 0),
|
|
||||||
navIconBookshelf = obj.getString("navIconBookshelf", "") ?: "",
|
|
||||||
navIconExplore = obj.getString("navIconExplore", "") ?: "",
|
|
||||||
navIconRss = obj.getString("navIconRss", "") ?: "",
|
|
||||||
navIconMy = obj.getString("navIconMy", "") ?: "",
|
|
||||||
coverTextColor = ThemeAppearanceJson.readColor(obj, "coverTextColor").takeIf { it != 0 } ?: -16777216,
|
|
||||||
coverShadowColor = ThemeAppearanceJson.readColor(obj, "coverShadowColor").takeIf { it != 0 } ?: -16777216,
|
|
||||||
coverTextColorN = ThemeAppearanceJson.readColor(obj, "coverTextColorN").takeIf { it != 0 } ?: -1,
|
|
||||||
coverShadowColorN = ThemeAppearanceJson.readColor(obj, "coverShadowColorN").takeIf { it != 0 } ?: -1,
|
|
||||||
coverShowName = obj.getBoolean("coverShowName", true),
|
|
||||||
coverShowAuthor = obj.getBoolean("coverShowAuthor", true),
|
|
||||||
coverShowNameN = obj.getBoolean("coverShowNameN", true),
|
|
||||||
coverShowAuthorN = obj.getBoolean("coverShowAuthorN", true),
|
|
||||||
coverDefaultColor = obj.getBoolean("coverDefaultColor", true),
|
|
||||||
coverShowShadow = obj.getBoolean("coverShowShadow", false),
|
|
||||||
coverShowStroke = obj.getBoolean("coverShowStroke", true),
|
|
||||||
coverInfoOrientation = obj.getString("coverInfoOrientation", "0") ?: "0",
|
|
||||||
defaultCover = obj.getString("defaultCover", "") ?: "",
|
|
||||||
defaultCoverDark = obj.getString("defaultCoverDark", "") ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -172,6 +172,22 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||||
|
|
||||||
|
var navExtended by prefDelegate("navExtended", false)
|
||||||
|
|
||||||
|
var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
|
||||||
|
|
||||||
|
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
|
||||||
|
|
||||||
|
var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)
|
||||||
|
|
||||||
|
var navIconBookshelf by prefDelegate(PreferKey.navIconBookshelf, "")
|
||||||
|
|
||||||
|
var navIconExplore by prefDelegate(PreferKey.navIconExplore, "")
|
||||||
|
|
||||||
|
var navIconRss by prefDelegate(PreferKey.navIconRss, "")
|
||||||
|
|
||||||
|
var navIconMy by prefDelegate(PreferKey.navIconMy, "")
|
||||||
|
|
||||||
fun hasImageBg(isDark: Boolean): Boolean =
|
fun hasImageBg(isDark: Boolean): Boolean =
|
||||||
!(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
|
!(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ import com.google.android.material.color.DynamicColorsOptions
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.AppContextWrapper
|
import io.legado.app.base.AppContextWrapper
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.help.LauncherIconHelp
|
import io.legado.app.help.LauncherIconHelp
|
||||||
import io.legado.app.help.loadFontFiles
|
import io.legado.app.help.loadFontFiles
|
||||||
@@ -124,6 +123,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
|||||||
fun ThemeConfigScreen(
|
fun ThemeConfigScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onNavigateToCustomTheme: () -> Unit,
|
onNavigateToCustomTheme: () -> Unit,
|
||||||
|
onNavigateToThemeManage: () -> Unit,
|
||||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
@@ -412,7 +412,7 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
AnimatedVisibility(visible = ThemeConfig.useFloatingBottomBar) {
|
AnimatedVisibility(visible = ThemeConfig.useFloatingBottomBar) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.floating_bottom_bar_liquid_glass),
|
title = stringResource(R.string.floating_bottom_bar_liquid_glass),
|
||||||
@@ -716,10 +716,10 @@ fun ThemeConfigScreen(
|
|||||||
item {
|
item {
|
||||||
SplicedColumnGroup(title = "导航栏图标设置") {
|
SplicedColumnGroup(title = "导航栏图标设置") {
|
||||||
val customCount = listOf(
|
val customCount = listOf(
|
||||||
MainConfig.navIconBookshelf,
|
ThemeConfig.navIconBookshelf,
|
||||||
MainConfig.navIconExplore,
|
ThemeConfig.navIconExplore,
|
||||||
MainConfig.navIconRss,
|
ThemeConfig.navIconRss,
|
||||||
MainConfig.navIconMy
|
ThemeConfig.navIconMy
|
||||||
).count { it.isNotEmpty() }
|
).count { it.isNotEmpty() }
|
||||||
ClickableSettingItem(
|
ClickableSettingItem(
|
||||||
title = "导航栏图标",
|
title = "导航栏图标",
|
||||||
@@ -728,6 +728,17 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Theme management
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup(title = "主题管理") {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "主题管理",
|
||||||
|
description = "保存、导入、导出主题配置",
|
||||||
|
onClick = onNavigateToThemeManage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
package io.legado.app.ui.config.themeManage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Done
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.help.config.ThemeExportData
|
||||||
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
|
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||||
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditThemeSheet(
|
||||||
|
show: Boolean,
|
||||||
|
themeData: ThemeExportData?,
|
||||||
|
themeName: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSave: (newName: String, newData: ThemeExportData) -> Unit
|
||||||
|
) {
|
||||||
|
if (!show || themeData == null) return
|
||||||
|
|
||||||
|
var data by remember(themeData) { mutableStateOf(themeData) }
|
||||||
|
var name by remember(themeName) { mutableStateOf(themeName) }
|
||||||
|
var showColorPicker by remember { mutableStateOf(false) }
|
||||||
|
var currentColorKey by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "编辑主题",
|
||||||
|
endAction = {
|
||||||
|
MediumIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
onSave(name, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Done,
|
||||||
|
contentDescription = "Save"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(bottom = 32.dp)
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
AppTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
placeholder = { },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Basic settings
|
||||||
|
SectionTitle("基础设置")
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "主题模式",
|
||||||
|
selectedValue = data.themeMode,
|
||||||
|
displayEntries = stringArrayResource(R.array.theme_mode),
|
||||||
|
entryValues = stringArrayResource(R.array.theme_mode_v),
|
||||||
|
onValueChange = { data = data.copy(themeMode = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "调色板风格",
|
||||||
|
selectedValue = data.paletteStyle,
|
||||||
|
displayEntries = stringArrayResource(R.array.paletteStyle),
|
||||||
|
entryValues = stringArrayResource(R.array.paletteStyle_value),
|
||||||
|
onValueChange = { data = data.copy(paletteStyle = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "Material 版本",
|
||||||
|
selectedValue = data.materialVersion,
|
||||||
|
displayEntries = stringArrayResource(R.array.materialVersion),
|
||||||
|
entryValues = stringArrayResource(R.array.materialVersion_value),
|
||||||
|
onValueChange = { data = data.copy(materialVersion = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "对比度偏好",
|
||||||
|
selectedValue = data.customContrast,
|
||||||
|
displayEntries = stringArrayResource(R.array.customContrast),
|
||||||
|
entryValues = stringArrayResource(R.array.customContrast_value),
|
||||||
|
onValueChange = { data = data.copy(customContrast = it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
SectionTitle("颜色设置")
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "使用色板生成颜色",
|
||||||
|
checked = !data.enableDeepPersonalization,
|
||||||
|
onCheckedChange = { data = data.copy(enableDeepPersonalization = !it) }
|
||||||
|
)
|
||||||
|
if (data.enableDeepPersonalization) {
|
||||||
|
ColorItem("主题色", data.themeColor) {
|
||||||
|
currentColorKey = "themeColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("次要主题色", data.secondaryThemeColor) {
|
||||||
|
currentColorKey = "secondaryThemeColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("主要字体色", data.primaryTextColor) {
|
||||||
|
currentColorKey = "primaryTextColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("次要字体色", data.secondaryTextColor) {
|
||||||
|
currentColorKey = "secondaryTextColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("背景色", data.themeBackgroundColor) {
|
||||||
|
currentColorKey = "themeBackgroundColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("标签容器色", data.labelContainerColor) {
|
||||||
|
currentColorKey = "labelContainerColor"; showColorPicker = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ColorItem("日间种子色", data.cPrimary) {
|
||||||
|
currentColorKey = "cPrimary"; showColorPicker = true
|
||||||
|
}
|
||||||
|
ColorItem("夜间种子色", data.cNPrimary) {
|
||||||
|
currentColorKey = "cNPrimary"; showColorPicker = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Interface layout
|
||||||
|
SectionTitle("界面布局")
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "发现",
|
||||||
|
checked = data.showDiscovery,
|
||||||
|
onCheckedChange = { data = data.copy(showDiscovery = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "RSS",
|
||||||
|
checked = data.showRss,
|
||||||
|
onCheckedChange = { data = data.copy(showRss = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "显示底栏",
|
||||||
|
checked = data.showBottomView,
|
||||||
|
onCheckedChange = { data = data.copy(showBottomView = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "浮动底栏",
|
||||||
|
checked = data.useFloatingBottomBar,
|
||||||
|
onCheckedChange = { data = data.copy(useFloatingBottomBar = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "状态栏",
|
||||||
|
checked = data.showStatusBar,
|
||||||
|
onCheckedChange = { data = data.copy(showStatusBar = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "翻页动画",
|
||||||
|
checked = data.swipeAnimation,
|
||||||
|
onCheckedChange = { data = data.copy(swipeAnimation = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "平板模式",
|
||||||
|
selectedValue = data.tabletInterface,
|
||||||
|
displayEntries = stringArrayResource(R.array.tabletInterface),
|
||||||
|
entryValues = stringArrayResource(R.array.tabletInterface_value),
|
||||||
|
onValueChange = { data = data.copy(tabletInterface = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "标签显示",
|
||||||
|
selectedValue = data.labelVisibilityMode,
|
||||||
|
displayEntries = stringArrayResource(R.array.label_vis_mode),
|
||||||
|
entryValues = stringArrayResource(R.array.label_vis_mode_value),
|
||||||
|
onValueChange = { data = data.copy(labelVisibilityMode = it) }
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "默认主页",
|
||||||
|
selectedValue = data.defaultHomePage,
|
||||||
|
displayEntries = stringArrayResource(R.array.default_home_page),
|
||||||
|
entryValues = stringArrayResource(R.array.default_home_page_value),
|
||||||
|
onValueChange = { data = data.copy(defaultHomePage = it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Blur
|
||||||
|
SectionTitle("模糊效果")
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "启用模糊",
|
||||||
|
checked = data.enableBlur,
|
||||||
|
onCheckedChange = { data = data.copy(enableBlur = it) }
|
||||||
|
)
|
||||||
|
if (data.enableBlur) {
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "顶栏模糊半径",
|
||||||
|
value = data.topBarBlurRadius.toFloat(),
|
||||||
|
valueRange = 1f..60f,
|
||||||
|
onValueChange = { data = data.copy(topBarBlurRadius = it.toInt()) }
|
||||||
|
)
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "底栏模糊半径",
|
||||||
|
value = data.bottomBarBlurRadius.toFloat(),
|
||||||
|
valueRange = 1f..60f,
|
||||||
|
onValueChange = { data = data.copy(bottomBarBlurRadius = it.toInt()) }
|
||||||
|
)
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "顶栏透明度",
|
||||||
|
value = data.topBarBlurAlpha.toFloat(),
|
||||||
|
valueRange = 0f..255f,
|
||||||
|
onValueChange = { data = data.copy(topBarBlurAlpha = it.toInt()) }
|
||||||
|
)
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "底栏透明度",
|
||||||
|
value = data.bottomBarBlurAlpha.toFloat(),
|
||||||
|
valueRange = 0f..255f,
|
||||||
|
onValueChange = { data = data.copy(bottomBarBlurAlpha = it.toInt()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Opacity
|
||||||
|
SectionTitle("透明度")
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "顶栏透明度",
|
||||||
|
value = data.topBarOpacity.toFloat(),
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { data = data.copy(topBarOpacity = it.toInt()) }
|
||||||
|
)
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "底栏透明度",
|
||||||
|
value = data.bottomBarOpacity.toFloat(),
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { data = data.copy(bottomBarOpacity = it.toInt()) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Container
|
||||||
|
SectionTitle("容器设置")
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "容器不透明度",
|
||||||
|
value = data.containerOpacity.toFloat(),
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { data = data.copy(containerOpacity = it.toInt()) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "容器边框",
|
||||||
|
checked = data.enableContainerBorder,
|
||||||
|
onCheckedChange = { data = data.copy(enableContainerBorder = it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Other
|
||||||
|
SectionTitle("其他")
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "纯黑模式",
|
||||||
|
checked = data.isPureBlack,
|
||||||
|
onCheckedChange = { data = data.copy(isPureBlack = it) }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "弹性顶栏",
|
||||||
|
checked = data.useFlexibleTopAppBar,
|
||||||
|
onCheckedChange = { data = data.copy(useFlexibleTopAppBar = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerSheet(
|
||||||
|
show = showColorPicker,
|
||||||
|
initialColor = when (currentColorKey) {
|
||||||
|
"themeColor" -> data.themeColor
|
||||||
|
"secondaryThemeColor" -> data.secondaryThemeColor
|
||||||
|
"primaryTextColor" -> data.primaryTextColor
|
||||||
|
"secondaryTextColor" -> data.secondaryTextColor
|
||||||
|
"themeBackgroundColor" -> data.themeBackgroundColor
|
||||||
|
"labelContainerColor" -> data.labelContainerColor
|
||||||
|
"cPrimary" -> data.cPrimary
|
||||||
|
"cNPrimary" -> data.cNPrimary
|
||||||
|
else -> 0
|
||||||
|
},
|
||||||
|
onDismissRequest = { showColorPicker = false },
|
||||||
|
onColorSelected = { color ->
|
||||||
|
data = when (currentColorKey) {
|
||||||
|
"themeColor" -> data.copy(themeColor = color)
|
||||||
|
"secondaryThemeColor" -> data.copy(secondaryThemeColor = color)
|
||||||
|
"primaryTextColor" -> data.copy(primaryTextColor = color)
|
||||||
|
"secondaryTextColor" -> data.copy(secondaryTextColor = color)
|
||||||
|
"themeBackgroundColor" -> data.copy(themeBackgroundColor = color)
|
||||||
|
"labelContainerColor" -> data.copy(labelContainerColor = color)
|
||||||
|
"cPrimary" -> data.copy(cPrimary = color)
|
||||||
|
"cNPrimary" -> data.copy(cNPrimary = color)
|
||||||
|
else -> data
|
||||||
|
}
|
||||||
|
showColorPicker = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionTitle(text: String) {
|
||||||
|
AppText(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColorItem(title: String, colorValue: Int, onClick: () -> Unit) {
|
||||||
|
CompactClickableSettingItem(
|
||||||
|
title = title,
|
||||||
|
description = if (colorValue != 0) {
|
||||||
|
"#${Integer.toHexString(colorValue).uppercase()}"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
trailingContent = {
|
||||||
|
if (colorValue != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(colorValue))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -68,6 +69,7 @@ fun ThemeManageScreen(
|
|||||||
var deleteTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
var deleteTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
||||||
var applyTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
var applyTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
||||||
var exportTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
var exportTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
||||||
|
var editTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
||||||
var showRestartDialog by remember { mutableStateOf(false) }
|
var showRestartDialog by remember { mutableStateOf(false) }
|
||||||
var savedThemesVersion by remember { mutableIntStateOf(0) }
|
var savedThemesVersion by remember { mutableIntStateOf(0) }
|
||||||
val savedThemes = remember(savedThemesVersion) { ThemeImportExport.savedThemes.toList() }
|
val savedThemes = remember(savedThemesVersion) { ThemeImportExport.savedThemes.toList() }
|
||||||
@@ -166,6 +168,7 @@ fun ThemeManageScreen(
|
|||||||
SavedThemeItem(
|
SavedThemeItem(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
onApply = { applyTarget = theme },
|
onApply = { applyTarget = theme },
|
||||||
|
onEdit = { editTarget = theme },
|
||||||
onExport = {
|
onExport = {
|
||||||
exportTarget = theme
|
exportTarget = theme
|
||||||
exportLauncher.launch("${theme.name}.json")
|
exportLauncher.launch("${theme.name}.json")
|
||||||
@@ -257,12 +260,29 @@ fun ThemeManageScreen(
|
|||||||
onDismiss = { deleteTarget = null },
|
onDismiss = { deleteTarget = null },
|
||||||
text = "确定删除主题「${deleteTarget?.name}」?此操作不可恢复。"
|
text = "确定删除主题「${deleteTarget?.name}」?此操作不可恢复。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Edit theme sheet
|
||||||
|
EditThemeSheet(
|
||||||
|
show = editTarget != null,
|
||||||
|
themeData = editTarget?.data,
|
||||||
|
themeName = editTarget?.name ?: "",
|
||||||
|
onDismissRequest = { editTarget = null },
|
||||||
|
onSave = { newName, newData ->
|
||||||
|
editTarget?.let { old ->
|
||||||
|
ThemeImportExport.deleteSavedTheme(old)
|
||||||
|
}
|
||||||
|
ThemeImportExport.saveCurrentAsTheme(newName, newData)
|
||||||
|
savedThemesVersion++
|
||||||
|
editTarget = null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SavedThemeItem(
|
private fun SavedThemeItem(
|
||||||
theme: SavedTheme,
|
theme: SavedTheme,
|
||||||
onApply: () -> Unit,
|
onApply: () -> Unit,
|
||||||
|
onEdit: () -> Unit,
|
||||||
onExport: () -> Unit,
|
onExport: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -320,6 +340,13 @@ private fun SavedThemeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onEdit) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = "编辑",
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(onClick = onExport) {
|
IconButton(onClick = onExport) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Share,
|
imageVector = Icons.Default.Share,
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
package io.legado.app.ui.config.themePack
|
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.help.config.ThemePackConfig
|
|
||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
|
||||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
|
||||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ThemePackScreen(
|
|
||||||
onBackClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
var listVersion by remember { mutableIntStateOf(0) }
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
|
||||||
var newThemeName by remember { mutableStateOf("") }
|
|
||||||
var deleteTarget by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
|
||||||
var applyTarget by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
|
||||||
var exportingPack by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
|
||||||
|
|
||||||
val packList = remember(listVersion) { ThemePackConfig.packList.toList() }
|
|
||||||
|
|
||||||
val exportLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/zip")
|
|
||||||
) { uri ->
|
|
||||||
uri?.let {
|
|
||||||
val pack = exportingPack ?: return@let
|
|
||||||
exportingPack = null
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val tempFile = File(context.cacheDir, "theme_export_${pack.name}.zip")
|
|
||||||
ThemePackConfig.exportToZip(pack, tempFile)
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { os ->
|
|
||||||
tempFile.inputStream().use { it.copyTo(os) }
|
|
||||||
}
|
|
||||||
tempFile.delete()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("导出成功")
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("导出失败: ${it.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val importLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.OpenDocument()
|
|
||||||
) { uri ->
|
|
||||||
uri?.let {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val tempFile = File(context.cacheDir, "theme_import_${System.currentTimeMillis()}.zip")
|
|
||||||
context.contentResolver.openInputStream(it)?.use { input ->
|
|
||||||
tempFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val pack = ThemePackConfig.importFromZip(tempFile)
|
|
||||||
tempFile.delete()
|
|
||||||
if (pack != null) {
|
|
||||||
listVersion++
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("导入成功: ${pack.name}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("导入失败: 无效的主题包")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("导入失败: ${it.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppScaffold(
|
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
topBar = {
|
|
||||||
GlassMediumFlexibleTopAppBar(
|
|
||||||
title = stringResource(R.string.theme_pack),
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
navigationIcon = {
|
|
||||||
TopBarNavigationButton(onClick = onBackClick)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = adaptiveContentPadding(
|
|
||||||
top = paddingValues.calculateTopPadding(),
|
|
||||||
bottom = 120.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
SplicedColumnGroup {
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = "新增主题",
|
|
||||||
description = "保存当前设置为新主题",
|
|
||||||
onClick = {
|
|
||||||
newThemeName = ""
|
|
||||||
showAddDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = "导入主题",
|
|
||||||
description = "从zip包导入主题",
|
|
||||||
onClick = {
|
|
||||||
importLauncher.launch(arrayOf("application/zip", "application/octet-stream"))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packList.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
AppText(
|
|
||||||
text = "主题列表",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(packList, key = { it.folderName }) { pack ->
|
|
||||||
ThemePackItem(
|
|
||||||
pack = pack,
|
|
||||||
onApply = { applyTarget = pack },
|
|
||||||
onExport = { exportingPack = pack },
|
|
||||||
onDelete = { deleteTarget = pack }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppAlertDialog(
|
|
||||||
data = if (showAddDialog) "add" else null,
|
|
||||||
onDismissRequest = { showAddDialog = false },
|
|
||||||
title = "新增主题",
|
|
||||||
confirmText = "保存",
|
|
||||||
onConfirm = {
|
|
||||||
if (newThemeName.isNotBlank()) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
ThemePackConfig.createFromCurrent(newThemeName)
|
|
||||||
listVersion++
|
|
||||||
}
|
|
||||||
showAddDialog = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissText = "取消",
|
|
||||||
onDismiss = { showAddDialog = false },
|
|
||||||
content = {
|
|
||||||
io.legado.app.ui.widget.components.AppTextField(
|
|
||||||
value = newThemeName,
|
|
||||||
onValueChange = { newThemeName = it },
|
|
||||||
placeholder = { AppText(text = "请输入主题名称") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
AppAlertDialog(
|
|
||||||
data = applyTarget,
|
|
||||||
onDismissRequest = { applyTarget = null },
|
|
||||||
title = "应用主题",
|
|
||||||
confirmText = "应用",
|
|
||||||
onConfirm = { pack ->
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
ThemePackConfig.applyPack(pack)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context.toastOnUi("主题已应用,请重启生效")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applyTarget = null
|
|
||||||
},
|
|
||||||
dismissText = "取消",
|
|
||||||
onDismiss = { applyTarget = null },
|
|
||||||
text = "确定应用主题「${applyTarget?.name}」?应用后需要重启才能完全生效。"
|
|
||||||
)
|
|
||||||
|
|
||||||
AppAlertDialog(
|
|
||||||
data = deleteTarget,
|
|
||||||
onDismissRequest = { deleteTarget = null },
|
|
||||||
title = "删除主题",
|
|
||||||
confirmText = "删除",
|
|
||||||
onConfirm = { pack ->
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
ThemePackConfig.deletePack(pack)
|
|
||||||
listVersion++
|
|
||||||
}
|
|
||||||
deleteTarget = null
|
|
||||||
},
|
|
||||||
dismissText = "取消",
|
|
||||||
onDismiss = { deleteTarget = null },
|
|
||||||
text = "确定删除主题「${deleteTarget?.name}」?此操作不可恢复。"
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(exportingPack) {
|
|
||||||
exportingPack?.let { pack ->
|
|
||||||
kotlin.runCatching {
|
|
||||||
exportLauncher.launch("${pack.name}.zip")
|
|
||||||
}.onFailure {
|
|
||||||
exportingPack = null
|
|
||||||
context.toastOnUi("导出失败: ${it.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ThemePackItem(
|
|
||||||
pack: ThemePackConfig.ThemePack,
|
|
||||||
onApply: () -> Unit,
|
|
||||||
onExport: () -> Unit,
|
|
||||||
onDelete: () -> Unit
|
|
||||||
) {
|
|
||||||
GlassCard(
|
|
||||||
onClick = onApply,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
cornerRadius = 20.dp
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
val packDir = remember { ThemePackConfig.getPackDir(pack) }
|
|
||||||
val bgFile = remember(pack.folderName) {
|
|
||||||
packDir.listFiles()?.firstOrNull {
|
|
||||||
it.isFile && (it.name.startsWith("bg_light") || it.name.startsWith("bg_dark"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val coverFile = remember(pack.folderName) {
|
|
||||||
packDir.listFiles()?.firstOrNull {
|
|
||||||
it.isFile && it.name.startsWith("cover_day_")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val previewFile = bgFile ?: coverFile
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(120.dp)
|
|
||||||
.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
|
|
||||||
.background(
|
|
||||||
if (pack.themeColor != 0) Color(pack.themeColor)
|
|
||||||
else MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (previewFile != null) {
|
|
||||||
AsyncImage(
|
|
||||||
model = previewFile,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
AppText(
|
|
||||||
text = pack.name,
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
val hasCustomIcon = pack.navIconBookshelf.isNotEmpty() ||
|
|
||||||
pack.navIconExplore.isNotEmpty() ||
|
|
||||||
pack.navIconRss.isNotEmpty() ||
|
|
||||||
pack.navIconMy.isNotEmpty()
|
|
||||||
val hasCover = pack.defaultCover.isNotEmpty() || pack.defaultCoverDark.isNotEmpty()
|
|
||||||
val features = mutableListOf<String>()
|
|
||||||
if (pack.appFontPath != null) features.add("应用字体")
|
|
||||||
if (pack.bgImageLight != null || pack.bgImageDark != null) features.add("应用背景")
|
|
||||||
if (hasCover) features.add("封面图片")
|
|
||||||
if (hasCustomIcon) features.add("图标")
|
|
||||||
if (features.isNotEmpty()) {
|
|
||||||
AppText(
|
|
||||||
text = features.joinToString(" · "),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = onExport) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Share,
|
|
||||||
contentDescription = "导出",
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = onDelete) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = "删除",
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,6 @@ import dev.chrisbanes.haze.hazeSource
|
|||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
||||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||||
@@ -114,6 +113,7 @@ fun MainScreen(
|
|||||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
|
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
|
||||||
|
onNavigateToReadRecord: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
@@ -150,6 +150,7 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||||
|
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,10 +282,10 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
label = if (labelVisibilityMode != "unlabeled") {
|
label = if (labelVisibilityMode != "unlabeled") {
|
||||||
val hasCustomIcon = when (destination) {
|
val hasCustomIcon = when (destination) {
|
||||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||||
}
|
}
|
||||||
if (hasCustomIcon) null else {{ AppText(stringResource(destination.labelId)) }}
|
if (hasCustomIcon) null else {{ AppText(stringResource(destination.labelId)) }}
|
||||||
} else null
|
} else null
|
||||||
@@ -301,10 +302,10 @@ fun MainScreen(
|
|||||||
destinations.forEachIndexed { index, destination ->
|
destinations.forEachIndexed { index, destination ->
|
||||||
val selected = pagerState.targetPage == index
|
val selected = pagerState.targetPage == index
|
||||||
val customIconPath = when (destination) {
|
val customIconPath = when (destination) {
|
||||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf
|
||||||
MainDestination.Explore -> MainConfig.navIconExplore
|
MainDestination.Explore -> ThemeConfig.navIconExplore
|
||||||
MainDestination.Rss -> MainConfig.navIconRss
|
MainDestination.Rss -> ThemeConfig.navIconRss
|
||||||
MainDestination.My -> MainConfig.navIconMy
|
MainDestination.My -> ThemeConfig.navIconMy
|
||||||
}
|
}
|
||||||
AppNavigationBarItem(
|
AppNavigationBarItem(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
@@ -390,10 +391,10 @@ fun MainScreen(
|
|||||||
MainDestination.My -> MyScreen(
|
MainDestination.My -> MyScreen(
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onNavigate = { event ->
|
onNavigate = { event ->
|
||||||
if (event == PrefClickEvent.OpenBookCacheManage) {
|
when (event) {
|
||||||
onNavigateToBookCacheManage()
|
PrefClickEvent.OpenBookCacheManage -> onNavigateToBookCacheManage()
|
||||||
} else {
|
PrefClickEvent.OpenReadRecord -> onNavigateToReadRecord()
|
||||||
viewModel.onPrefClickEvent(event)
|
else -> viewModel.onPrefClickEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -432,20 +433,20 @@ fun MainScreen(
|
|||||||
isBlurEnabled = useLiquidGlass,
|
isBlurEnabled = useLiquidGlass,
|
||||||
hasCustomIcons = destinations.any { dest ->
|
hasCustomIcons = destinations.any { dest ->
|
||||||
when (dest) {
|
when (dest) {
|
||||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
destinations.forEachIndexed { index, destination ->
|
destinations.forEachIndexed { index, destination ->
|
||||||
val selected = pagerState.targetPage == index
|
val selected = pagerState.targetPage == index
|
||||||
val hasCustomIcon = when (destination) {
|
val hasCustomIcon = when (destination) {
|
||||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||||
}
|
}
|
||||||
FloatingBottomBarItem(
|
FloatingBottomBarItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -522,10 +523,10 @@ private fun NavigationIcon(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val customIconPath = when (destination) {
|
val customIconPath = when (destination) {
|
||||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf
|
||||||
MainDestination.Explore -> MainConfig.navIconExplore
|
MainDestination.Explore -> ThemeConfig.navIconExplore
|
||||||
MainDestination.Rss -> MainConfig.navIconRss
|
MainDestination.Rss -> ThemeConfig.navIconRss
|
||||||
MainDestination.My -> MainConfig.navIconMy
|
MainDestination.My -> ThemeConfig.navIconMy
|
||||||
}
|
}
|
||||||
if (customIconPath.isNotEmpty()) {
|
if (customIconPath.isNotEmpty()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
Reference in new issue
Block a user