新的独属于MD3的雷霆主题管理与分享系统
This commit is contained in:
@@ -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 {
|
||||
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")
|
||||
baseDir.mkdirs()
|
||||
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 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 =
|
||||
!(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.base.AppContextWrapper
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.help.LauncherIconHelp
|
||||
import io.legado.app.help.loadFontFiles
|
||||
@@ -124,6 +123,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
||||
fun ThemeConfigScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onNavigateToCustomTheme: () -> Unit,
|
||||
onNavigateToThemeManage: () -> Unit,
|
||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
@@ -412,7 +412,7 @@ fun ThemeConfigScreen(
|
||||
)
|
||||
AnimatedVisibility(visible = ThemeConfig.useFloatingBottomBar) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.floating_bottom_bar_liquid_glass),
|
||||
@@ -716,10 +716,10 @@ fun ThemeConfigScreen(
|
||||
item {
|
||||
SplicedColumnGroup(title = "导航栏图标设置") {
|
||||
val customCount = listOf(
|
||||
MainConfig.navIconBookshelf,
|
||||
MainConfig.navIconExplore,
|
||||
MainConfig.navIconRss,
|
||||
MainConfig.navIconMy
|
||||
ThemeConfig.navIconBookshelf,
|
||||
ThemeConfig.navIconExplore,
|
||||
ThemeConfig.navIconRss,
|
||||
ThemeConfig.navIconMy
|
||||
).count { it.isNotEmpty() }
|
||||
ClickableSettingItem(
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -68,6 +69,7 @@ fun ThemeManageScreen(
|
||||
var deleteTarget by remember { mutableStateOf<SavedTheme?>(null) }
|
||||
var applyTarget 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 savedThemesVersion by remember { mutableIntStateOf(0) }
|
||||
val savedThemes = remember(savedThemesVersion) { ThemeImportExport.savedThemes.toList() }
|
||||
@@ -166,6 +168,7 @@ fun ThemeManageScreen(
|
||||
SavedThemeItem(
|
||||
theme = theme,
|
||||
onApply = { applyTarget = theme },
|
||||
onEdit = { editTarget = theme },
|
||||
onExport = {
|
||||
exportTarget = theme
|
||||
exportLauncher.launch("${theme.name}.json")
|
||||
@@ -257,12 +260,29 @@ fun ThemeManageScreen(
|
||||
onDismiss = { deleteTarget = null },
|
||||
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
|
||||
private fun SavedThemeItem(
|
||||
theme: SavedTheme,
|
||||
onApply: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onExport: () -> 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) {
|
||||
Icon(
|
||||
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.painter.BitmapPainter
|
||||
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.main.bookshelf.BookshelfScreen
|
||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||
@@ -114,6 +113,7 @@ fun MainScreen(
|
||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
|
||||
onNavigateToReadRecord: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
@@ -150,6 +150,7 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,10 +282,10 @@ fun MainScreen(
|
||||
},
|
||||
label = if (labelVisibilityMode != "unlabeled") {
|
||||
val hasCustomIcon = when (destination) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||
}
|
||||
if (hasCustomIcon) null else {{ AppText(stringResource(destination.labelId)) }}
|
||||
} else null
|
||||
@@ -301,10 +302,10 @@ fun MainScreen(
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
val customIconPath = when (destination) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
||||
MainDestination.Explore -> MainConfig.navIconExplore
|
||||
MainDestination.Rss -> MainConfig.navIconRss
|
||||
MainDestination.My -> MainConfig.navIconMy
|
||||
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf
|
||||
MainDestination.Explore -> ThemeConfig.navIconExplore
|
||||
MainDestination.Rss -> ThemeConfig.navIconRss
|
||||
MainDestination.My -> ThemeConfig.navIconMy
|
||||
}
|
||||
AppNavigationBarItem(
|
||||
selected = selected,
|
||||
@@ -390,10 +391,10 @@ fun MainScreen(
|
||||
MainDestination.My -> MyScreen(
|
||||
onOpenSettings = onOpenSettings,
|
||||
onNavigate = { event ->
|
||||
if (event == PrefClickEvent.OpenBookCacheManage) {
|
||||
onNavigateToBookCacheManage()
|
||||
} else {
|
||||
viewModel.onPrefClickEvent(event)
|
||||
when (event) {
|
||||
PrefClickEvent.OpenBookCacheManage -> onNavigateToBookCacheManage()
|
||||
PrefClickEvent.OpenReadRecord -> onNavigateToReadRecord()
|
||||
else -> viewModel.onPrefClickEvent(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -432,20 +433,20 @@ fun MainScreen(
|
||||
isBlurEnabled = useLiquidGlass,
|
||||
hasCustomIcons = destinations.any { dest ->
|
||||
when (dest) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||
}
|
||||
}
|
||||
) {
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
val hasCustomIcon = when (destination) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf.isNotEmpty()
|
||||
MainDestination.Explore -> ThemeConfig.navIconExplore.isNotEmpty()
|
||||
MainDestination.Rss -> ThemeConfig.navIconRss.isNotEmpty()
|
||||
MainDestination.My -> ThemeConfig.navIconMy.isNotEmpty()
|
||||
}
|
||||
FloatingBottomBarItem(
|
||||
onClick = {
|
||||
@@ -522,10 +523,10 @@ private fun NavigationIcon(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val customIconPath = when (destination) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
||||
MainDestination.Explore -> MainConfig.navIconExplore
|
||||
MainDestination.Rss -> MainConfig.navIconRss
|
||||
MainDestination.My -> MainConfig.navIconMy
|
||||
MainDestination.Bookshelf -> ThemeConfig.navIconBookshelf
|
||||
MainDestination.Explore -> ThemeConfig.navIconExplore
|
||||
MainDestination.Rss -> ThemeConfig.navIconRss
|
||||
MainDestination.My -> ThemeConfig.navIconMy
|
||||
}
|
||||
if (customIconPath.isNotEmpty()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Reference in New Issue
Block a user