fix: 主题相关
This commit is contained in:
@@ -72,6 +72,13 @@ object ThemeImportExport {
|
|||||||
return saveThemeData(name, data)
|
return saveThemeData(name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uniqueSavedThemeName(name: String): String {
|
||||||
|
if (_savedThemes.none { it.name == name }) return name
|
||||||
|
var index = 2
|
||||||
|
while (_savedThemes.any { it.name == "$name $index" }) index++
|
||||||
|
return "$name $index"
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveThemeData(name: String, data: ThemeExportData): SavedTheme {
|
private fun saveThemeData(name: String, data: ThemeExportData): SavedTheme {
|
||||||
val file = File(baseDir, "$name.json")
|
val file = File(baseDir, "$name.json")
|
||||||
baseDir.mkdirs()
|
baseDir.mkdirs()
|
||||||
@@ -82,19 +89,6 @@ object ThemeImportExport {
|
|||||||
return theme
|
return theme
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用已保存的主题
|
|
||||||
*/
|
|
||||||
fun applySavedTheme(theme: SavedTheme): Boolean {
|
|
||||||
return try {
|
|
||||||
applyToThemeConfig(theme.data)
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除已保存的主题
|
* 删除已保存的主题
|
||||||
*/
|
*/
|
||||||
@@ -305,7 +299,10 @@ object ThemeImportExport {
|
|||||||
/**
|
/**
|
||||||
* 将导出数据应用到当前配置
|
* 将导出数据应用到当前配置
|
||||||
*/
|
*/
|
||||||
fun applyToThemeConfig(data: ThemeExportData) {
|
internal fun applyToThemeConfig(
|
||||||
|
data: ThemeExportData,
|
||||||
|
applyEmbeddedCoverAssets: Boolean = true,
|
||||||
|
): AppliedThemeAssets {
|
||||||
// 基础主题设置
|
// 基础主题设置
|
||||||
ThemeConfig.appTheme = data.appTheme
|
ThemeConfig.appTheme = data.appTheme
|
||||||
ThemeConfig.themeMode = data.themeMode
|
ThemeConfig.themeMode = data.themeMode
|
||||||
@@ -416,16 +413,31 @@ object ThemeImportExport {
|
|||||||
CoverConfig.coverInfoOrientation = data.coverInfoOrientation
|
CoverConfig.coverInfoOrientation = data.coverInfoOrientation
|
||||||
|
|
||||||
// 应用嵌入的资源
|
// 应用嵌入的资源
|
||||||
data.assets?.let { assets ->
|
val embeddedAssets = data.assets?.let { assets ->
|
||||||
applyAssets(assets)
|
applyAssets(
|
||||||
}
|
assets = assets,
|
||||||
|
applyCoverAssets = applyEmbeddedCoverAssets,
|
||||||
|
)
|
||||||
|
} ?: AppliedThemeAssets()
|
||||||
|
return AppliedThemeAssets(
|
||||||
|
lightCoverPaths = embeddedAssets.lightCoverPaths.ifEmpty {
|
||||||
|
data.coverDefaultImage.toCoverPaths()
|
||||||
|
},
|
||||||
|
darkCoverPaths = embeddedAssets.darkCoverPaths.ifEmpty {
|
||||||
|
data.coverDefaultImageDark.toCoverPaths()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyAssets(assets: Map<String, String>) {
|
private fun applyAssets(
|
||||||
|
assets: Map<String, String>,
|
||||||
|
applyCoverAssets: Boolean,
|
||||||
|
): AppliedThemeAssets {
|
||||||
val coverPaths = mutableMapOf<String, MutableList<String>>()
|
val coverPaths = mutableMapOf<String, MutableList<String>>()
|
||||||
|
|
||||||
assets.forEach { (key, base64) ->
|
assets.forEach { (key, base64) ->
|
||||||
if (base64.isBlank()) return@forEach
|
if (base64.isBlank()) return@forEach
|
||||||
|
if (!applyCoverAssets && key.startsWith("coverDefaultImage")) return@forEach
|
||||||
try {
|
try {
|
||||||
val bytes = EncoderUtils.base64DecodeToByteArray(base64)
|
val bytes = EncoderUtils.base64DecodeToByteArray(base64)
|
||||||
val destFile = when {
|
val destFile = when {
|
||||||
@@ -493,6 +505,10 @@ object ThemeImportExport {
|
|||||||
coverPaths["coverDefaultImageDark"]?.let { paths ->
|
coverPaths["coverDefaultImageDark"]?.let { paths ->
|
||||||
CoverConfig.defaultCoverDark = paths.joinToString(",")
|
CoverConfig.defaultCoverDark = paths.joinToString(",")
|
||||||
}
|
}
|
||||||
|
return AppliedThemeAssets(
|
||||||
|
lightCoverPaths = coverPaths["coverDefaultImage"].orEmpty(),
|
||||||
|
darkCoverPaths = coverPaths["coverDefaultImageDark"].orEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -507,16 +523,22 @@ object ThemeImportExport {
|
|||||||
* 从JSON字符串导入主题
|
* 从JSON字符串导入主题
|
||||||
*/
|
*/
|
||||||
fun importFromJson(json: String): Boolean {
|
fun importFromJson(json: String): Boolean {
|
||||||
|
return importFromJsonWithAssets(json) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun importFromJsonWithAssets(json: String): AppliedThemeAssets? {
|
||||||
return try {
|
return try {
|
||||||
val data = parseThemeData(json) ?: return false
|
val data = parseThemeData(json) ?: return null
|
||||||
applyToThemeConfig(data)
|
applyToThemeConfig(data)
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
false
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.toCoverPaths(): List<String> =
|
||||||
|
split(",").map(String::trim).filter(String::isNotEmpty)
|
||||||
|
|
||||||
private fun parseThemeData(json: String): ThemeExportData? {
|
private fun parseThemeData(json: String): ThemeExportData? {
|
||||||
val root = JsonParser.parseString(json).asJsonObject
|
val root = JsonParser.parseString(json).asJsonObject
|
||||||
return when {
|
return when {
|
||||||
@@ -761,6 +783,7 @@ data class ThemeExportData(
|
|||||||
val bgImageBlurring: Int = 0,
|
val bgImageBlurring: Int = 0,
|
||||||
val bgImageNBlurring: Int = 0,
|
val bgImageNBlurring: Int = 0,
|
||||||
val appFontPath: String? = null,
|
val appFontPath: String? = null,
|
||||||
|
val selectedCoverAlbumId: String? = null,
|
||||||
|
|
||||||
// 封面配置 (CoverConfig)
|
// 封面配置 (CoverConfig)
|
||||||
val coverLoadOnlyWifi: Boolean = false,
|
val coverLoadOnlyWifi: Boolean = false,
|
||||||
@@ -792,3 +815,8 @@ data class SavedTheme(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val data: ThemeExportData
|
val data: ThemeExportData
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal data class AppliedThemeAssets(
|
||||||
|
val lightCoverPaths: List<String> = emptyList(),
|
||||||
|
val darkCoverPaths: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package io.legado.app.help.config
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.domain.model.CoverAlbumImageInput
|
import io.legado.app.domain.model.CoverAlbumImageInput
|
||||||
import io.legado.app.domain.usecase.CoverAlbumUseCase
|
import io.legado.app.domain.usecase.CoverAlbumUseCase
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -60,7 +59,10 @@ class ThemePackageManager(
|
|||||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||||
ZipOutputStream(BufferedOutputStream(output)).use { zip ->
|
ZipOutputStream(BufferedOutputStream(output)).use { zip ->
|
||||||
val assetEntries = exportAssets(zip, rawConfig)
|
val assetEntries = exportAssets(zip, rawConfig)
|
||||||
val coverData = exportCoverAlbums(zip)
|
val coverData = exportCoverAlbums(
|
||||||
|
zip = zip,
|
||||||
|
preferredAlbumId = rawConfig.selectedCoverAlbumId,
|
||||||
|
)
|
||||||
val manifest = ThemePackageManifest(
|
val manifest = ThemePackageManifest(
|
||||||
formatVersion = FORMAT_VERSION,
|
formatVersion = FORMAT_VERSION,
|
||||||
name = themeName,
|
name = themeName,
|
||||||
@@ -126,6 +128,10 @@ class ThemePackageManager(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
coverAlbumUseCase.selectAlbum(selectedAlbumId)
|
coverAlbumUseCase.selectAlbum(selectedAlbumId)
|
||||||
|
saveImportedTheme(
|
||||||
|
name = importedThemeName(uri, manifest.name),
|
||||||
|
selectedCoverAlbumId = selectedAlbumId,
|
||||||
|
)
|
||||||
} catch (error: CancellationException) {
|
} catch (error: CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
@@ -145,13 +151,61 @@ class ThemePackageManager(
|
|||||||
val json = context.contentResolver.openInputStream(uri)?.use {
|
val json = context.contentResolver.openInputStream(uri)?.use {
|
||||||
it.bufferedReader().readText()
|
it.bufferedReader().readText()
|
||||||
} ?: error("无法读取旧主题配置")
|
} ?: error("无法读取旧主题配置")
|
||||||
check(ThemeImportExport.importFromJson(json)) {
|
val appliedAssets = ThemeImportExport.importFromJsonWithAssets(json)
|
||||||
"旧主题配置格式无效或字段已被混淆"
|
?: error("旧主题配置格式无效或字段已被混淆")
|
||||||
}
|
val name = importedThemeName(uri)
|
||||||
importLegacyCoverAlbums()
|
val selectedCoverAlbumId = importLegacyCoverAlbums(
|
||||||
|
albumName = name,
|
||||||
|
appliedAssets = appliedAssets,
|
||||||
|
)
|
||||||
|
saveImportedTheme(
|
||||||
|
name = name,
|
||||||
|
selectedCoverAlbumId = selectedCoverAlbumId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveTheme(
|
||||||
|
name: String,
|
||||||
|
data: ThemeExportData = ThemeImportExport.exportFromCurrent(),
|
||||||
|
): SavedTheme {
|
||||||
|
val selectedCoverAlbumId = data.selectedCoverAlbumId
|
||||||
|
?: coverAlbumUseCase.selection.value.albumId
|
||||||
|
return ThemeImportExport.saveCurrentAsTheme(
|
||||||
|
name = name,
|
||||||
|
data = data.copy(selectedCoverAlbumId = selectedCoverAlbumId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun applySavedTheme(theme: SavedTheme): Result<Unit> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runSuspendCatching {
|
||||||
|
val savedAlbumId = theme.data.selectedCoverAlbumId
|
||||||
|
?.takeIf { id -> coverAlbumUseCase.albums.value.any { it.id == id } }
|
||||||
|
val appliedAssets = ThemeImportExport.applyToThemeConfig(
|
||||||
|
data = theme.data,
|
||||||
|
applyEmbeddedCoverAssets = savedAlbumId == null,
|
||||||
|
)
|
||||||
|
val selectedCoverAlbumId = if (savedAlbumId != null) {
|
||||||
|
coverAlbumUseCase.selectAlbum(savedAlbumId)
|
||||||
|
savedAlbumId
|
||||||
|
} else {
|
||||||
|
importLegacyCoverAlbums(
|
||||||
|
albumName = theme.name,
|
||||||
|
appliedAssets = appliedAssets,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (selectedCoverAlbumId != theme.data.selectedCoverAlbumId) {
|
||||||
|
ThemeImportExport.saveCurrentAsTheme(
|
||||||
|
name = theme.name,
|
||||||
|
data = theme.data.copy(
|
||||||
|
selectedCoverAlbumId = selectedCoverAlbumId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun exportAssets(
|
private fun exportAssets(
|
||||||
zip: ZipOutputStream,
|
zip: ZipOutputStream,
|
||||||
config: ThemeExportData,
|
config: ThemeExportData,
|
||||||
@@ -183,10 +237,15 @@ class ThemePackageManager(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportCoverAlbums(zip: ZipOutputStream): ExportedCoverData {
|
private fun exportCoverAlbums(
|
||||||
val selection = coverAlbumUseCase.selection.value
|
zip: ZipOutputStream,
|
||||||
|
preferredAlbumId: String?,
|
||||||
|
): ExportedCoverData {
|
||||||
val albumsById = coverAlbumUseCase.albums.value.associateBy { it.id }
|
val albumsById = coverAlbumUseCase.albums.value.associateBy { it.id }
|
||||||
val selectedIds = listOfNotNull(selection.albumId)
|
val selectedAlbumId = preferredAlbumId
|
||||||
|
?.takeIf(albumsById::containsKey)
|
||||||
|
?: coverAlbumUseCase.selection.value.albumId
|
||||||
|
val selectedIds = listOfNotNull(selectedAlbumId)
|
||||||
val refById = selectedIds.associateWith { "album_0" }
|
val refById = selectedIds.associateWith { "album_0" }
|
||||||
val exportedAlbums = selectedIds.mapNotNull { albumId ->
|
val exportedAlbums = selectedIds.mapNotNull { albumId ->
|
||||||
val album = albumsById[albumId] ?: return@mapNotNull null
|
val album = albumsById[albumId] ?: return@mapNotNull null
|
||||||
@@ -217,7 +276,7 @@ class ThemePackageManager(
|
|||||||
return ExportedCoverData(
|
return ExportedCoverData(
|
||||||
albums = exportedAlbums,
|
albums = exportedAlbums,
|
||||||
selection = ThemePackageCoverSelection(
|
selection = ThemePackageCoverSelection(
|
||||||
albumRef = selection.albumId?.let(refById::get),
|
albumRef = selectedAlbumId?.let(refById::get),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -289,15 +348,18 @@ class ThemePackageManager(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun importLegacyCoverAlbums() {
|
private suspend fun importLegacyCoverAlbums(
|
||||||
|
albumName: String,
|
||||||
|
appliedAssets: AppliedThemeAssets,
|
||||||
|
): String? {
|
||||||
val importedIds = mutableListOf<String>()
|
val importedIds = mutableListOf<String>()
|
||||||
try {
|
try {
|
||||||
val lightFiles = CoverConfig.defaultCover.toExistingFiles()
|
val lightFiles = appliedAssets.lightCoverPaths.toExistingFiles()
|
||||||
val darkFiles = CoverConfig.defaultCoverDark.toExistingFiles()
|
val darkFiles = appliedAssets.darkCoverPaths.toExistingFiles()
|
||||||
val albumId = if (lightFiles.isNotEmpty() || darkFiles.isNotEmpty()) {
|
val albumId = if (lightFiles.isNotEmpty() || darkFiles.isNotEmpty()) {
|
||||||
val existingNames = coverAlbumUseCase.albums.value.mapTo(mutableSetOf()) { it.name }
|
val existingNames = coverAlbumUseCase.albums.value.mapTo(mutableSetOf()) { it.name }
|
||||||
val name = uniqueImportedAlbumName(
|
val name = uniqueImportedAlbumName(
|
||||||
context.getString(R.string.default_cover),
|
albumName,
|
||||||
existingNames,
|
existingNames,
|
||||||
)
|
)
|
||||||
coverAlbumUseCase.importAlbum(
|
coverAlbumUseCase.importAlbum(
|
||||||
@@ -311,6 +373,7 @@ class ThemePackageManager(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
coverAlbumUseCase.selectAlbum(albumId)
|
coverAlbumUseCase.selectAlbum(albumId)
|
||||||
|
return albumId
|
||||||
} catch (error: CancellationException) {
|
} catch (error: CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
@@ -321,6 +384,43 @@ class ThemePackageManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveImportedTheme(
|
||||||
|
name: String,
|
||||||
|
selectedCoverAlbumId: String?,
|
||||||
|
) {
|
||||||
|
saveTheme(
|
||||||
|
name = ThemeImportExport.uniqueSavedThemeName(name),
|
||||||
|
data = ThemeImportExport.exportFromCurrent().copy(
|
||||||
|
selectedCoverAlbumId = selectedCoverAlbumId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importedThemeName(
|
||||||
|
uri: Uri,
|
||||||
|
packageName: String? = null,
|
||||||
|
): String {
|
||||||
|
val displayName = context.contentResolver.query(
|
||||||
|
uri,
|
||||||
|
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||||
|
}
|
||||||
|
val rawName = packageName
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
|
?: displayName?.substringBeforeLast('.')
|
||||||
|
?: uri.lastPathSegment?.substringBeforeLast('.')
|
||||||
|
?: context.getString(R.string.import_theme)
|
||||||
|
return rawName
|
||||||
|
.replace(Regex("""[\\/:*?"<>|]"""), "_")
|
||||||
|
.trim()
|
||||||
|
.trim('.')
|
||||||
|
.ifBlank { context.getString(R.string.import_theme) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractPackage(uri: Uri, root: File) {
|
private fun extractPackage(uri: Uri, root: File) {
|
||||||
root.mkdirs()
|
root.mkdirs()
|
||||||
var entryCount = 0
|
var entryCount = 0
|
||||||
@@ -410,6 +510,7 @@ class ThemePackageManager(
|
|||||||
appFontPath = null,
|
appFontPath = null,
|
||||||
coverDefaultImage = "",
|
coverDefaultImage = "",
|
||||||
coverDefaultImageDark = "",
|
coverDefaultImageDark = "",
|
||||||
|
selectedCoverAlbumId = null,
|
||||||
assets = null,
|
assets = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -456,9 +557,8 @@ class ThemePackageManager(
|
|||||||
return entryBytes
|
return entryBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toExistingFiles(): List<File> =
|
private fun List<String>.toExistingFiles(): List<File> =
|
||||||
split(",")
|
map(String::trim)
|
||||||
.map(String::trim)
|
|
||||||
.filter(String::isNotEmpty)
|
.filter(String::isNotEmpty)
|
||||||
.map(::File)
|
.map(::File)
|
||||||
.filter(File::isFile)
|
.filter(File::isFile)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package io.legado.app.ui.config.customTheme
|
package io.legado.app.ui.config.customTheme
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -18,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -33,13 +32,13 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
||||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
|
||||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
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.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -140,7 +139,7 @@ fun CustomThemeScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item {
|
item {
|
||||||
SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
|
SplicedColumnGroup(title = stringResource(R.string.custom_theme_colors)) {
|
||||||
ClickableSettingItem(
|
ClickableSettingItem(
|
||||||
title = stringResource(R.string.seed_color),
|
title = stringResource(R.string.seed_color),
|
||||||
description = stringResource(R.string.day),
|
description = stringResource(R.string.day),
|
||||||
|
|||||||
@@ -322,6 +322,11 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.theme_pack),
|
||||||
|
description = stringResource(R.string.theme_pack_s),
|
||||||
|
onClick = onNavigateToThemeManage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup {
|
SplicedColumnGroup {
|
||||||
@@ -331,7 +336,7 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) {
|
if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) {
|
||||||
ClickableSettingItem(
|
ClickableSettingItem(
|
||||||
title = stringResource(R.string.custom_theme),
|
title = stringResource(R.string.custom_theme_colors),
|
||||||
onClick = onNavigateToCustomTheme
|
onClick = onNavigateToCustomTheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -803,16 +808,6 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme management
|
|
||||||
item {
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.theme_pack)) {
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = stringResource(R.string.theme_pack),
|
|
||||||
description = stringResource(R.string.theme_pack_s),
|
|
||||||
onClick = onNavigateToThemeManage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,14 @@ fun ThemeManageScreen(
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.effects.collectLatest { effect ->
|
viewModel.effects.collectLatest { effect ->
|
||||||
when (effect) {
|
when (effect) {
|
||||||
|
ThemeManageEffect.SavedThemesChanged -> {
|
||||||
|
savedThemesVersion++
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeManageEffect.RestartRequired -> {
|
||||||
|
showRestartDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
is ThemeManageEffect.ShowResult -> {
|
is ThemeManageEffect.ShowResult -> {
|
||||||
val message = buildString {
|
val message = buildString {
|
||||||
append(context.getString(effect.messageRes))
|
append(context.getString(effect.messageRes))
|
||||||
@@ -124,6 +132,9 @@ fun ThemeManageScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.toastOnUi(message)
|
context.toastOnUi(message)
|
||||||
|
if (effect.savedThemesChanged) {
|
||||||
|
savedThemesVersion++
|
||||||
|
}
|
||||||
if (effect.restartRequired) {
|
if (effect.restartRequired) {
|
||||||
showRestartDialog = true
|
showRestartDialog = true
|
||||||
}
|
}
|
||||||
@@ -249,8 +260,7 @@ fun ThemeManageScreen(
|
|||||||
confirmText = stringResource(R.string.theme_manage_save),
|
confirmText = stringResource(R.string.theme_manage_save),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
if (newThemeName.isNotBlank()) {
|
if (newThemeName.isNotBlank()) {
|
||||||
ThemeImportExport.saveCurrentAsTheme(newThemeName)
|
viewModel.onIntent(ThemeManageIntent.SaveTheme(newThemeName))
|
||||||
savedThemesVersion++
|
|
||||||
showSaveDialog = false
|
showSaveDialog = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -275,8 +285,7 @@ fun ThemeManageScreen(
|
|||||||
confirmText = stringResource(R.string.theme_manage_apply),
|
confirmText = stringResource(R.string.theme_manage_apply),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
applyTarget?.let { theme ->
|
applyTarget?.let { theme ->
|
||||||
ThemeImportExport.applySavedTheme(theme)
|
viewModel.onIntent(ThemeManageIntent.ApplySavedTheme(theme))
|
||||||
showRestartDialog = true
|
|
||||||
}
|
}
|
||||||
applyTarget = null
|
applyTarget = null
|
||||||
},
|
},
|
||||||
@@ -310,11 +319,13 @@ fun ThemeManageScreen(
|
|||||||
themeName = editTarget?.name ?: "",
|
themeName = editTarget?.name ?: "",
|
||||||
onDismissRequest = { editTarget = null },
|
onDismissRequest = { editTarget = null },
|
||||||
onSave = { newName, newData ->
|
onSave = { newName, newData ->
|
||||||
editTarget?.let { old ->
|
viewModel.onIntent(
|
||||||
ThemeImportExport.deleteSavedTheme(old)
|
ThemeManageIntent.SaveTheme(
|
||||||
}
|
name = newName,
|
||||||
ThemeImportExport.saveCurrentAsTheme(newName, newData)
|
data = newData,
|
||||||
savedThemesVersion++
|
replacedTheme = editTarget,
|
||||||
|
)
|
||||||
|
)
|
||||||
editTarget = null
|
editTarget = null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
|
import io.legado.app.help.config.SavedTheme
|
||||||
import io.legado.app.help.config.ThemeExportData
|
import io.legado.app.help.config.ThemeExportData
|
||||||
|
import io.legado.app.help.config.ThemeImportExport
|
||||||
import io.legado.app.help.config.ThemePackageManager
|
import io.legado.app.help.config.ThemePackageManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -23,6 +26,49 @@ class ThemeManageViewModel(
|
|||||||
is ThemeManageIntent.ExportPackage -> exportPackage(intent)
|
is ThemeManageIntent.ExportPackage -> exportPackage(intent)
|
||||||
is ThemeManageIntent.ImportPackage -> importPackage(intent.uri)
|
is ThemeManageIntent.ImportPackage -> importPackage(intent.uri)
|
||||||
is ThemeManageIntent.ImportLegacyJson -> importLegacyJson(intent.uri)
|
is ThemeManageIntent.ImportLegacyJson -> importLegacyJson(intent.uri)
|
||||||
|
is ThemeManageIntent.SaveTheme -> saveTheme(intent)
|
||||||
|
is ThemeManageIntent.ApplySavedTheme -> applySavedTheme(intent.theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveTheme(intent: ThemeManageIntent.SaveTheme) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val result = runCatching {
|
||||||
|
themePackageManager.saveTheme(
|
||||||
|
name = intent.name,
|
||||||
|
data = intent.data ?: ThemeImportExport.exportFromCurrent(),
|
||||||
|
)
|
||||||
|
intent.replacedTheme
|
||||||
|
?.takeIf { it.name != intent.name }
|
||||||
|
?.let(ThemeImportExport::deleteSavedTheme)
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
_effects.emit(ThemeManageEffect.SavedThemesChanged)
|
||||||
|
} else {
|
||||||
|
_effects.emit(
|
||||||
|
ThemeManageEffect.ShowResult(
|
||||||
|
messageRes = R.string.theme_manage_save_failed,
|
||||||
|
detail = result.exceptionOrNull()?.localizedMessage,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySavedTheme(theme: SavedTheme) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = themePackageManager.applySavedTheme(theme)
|
||||||
|
if (result.isSuccess) {
|
||||||
|
_effects.emit(ThemeManageEffect.SavedThemesChanged)
|
||||||
|
_effects.emit(ThemeManageEffect.RestartRequired)
|
||||||
|
} else {
|
||||||
|
_effects.emit(
|
||||||
|
ThemeManageEffect.ShowResult(
|
||||||
|
messageRes = R.string.theme_manage_apply_failed,
|
||||||
|
detail = result.exceptionOrNull()?.localizedMessage,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +110,7 @@ class ThemeManageViewModel(
|
|||||||
ThemeManageEffect.ShowResult(
|
ThemeManageEffect.ShowResult(
|
||||||
messageRes = R.string.theme_manage_import_success,
|
messageRes = R.string.theme_manage_import_success,
|
||||||
restartRequired = true,
|
restartRequired = true,
|
||||||
|
savedThemesChanged = true,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ThemeManageEffect.ShowResult(
|
ThemeManageEffect.ShowResult(
|
||||||
@@ -84,12 +131,23 @@ sealed interface ThemeManageIntent {
|
|||||||
|
|
||||||
data class ImportPackage(val uri: String) : ThemeManageIntent
|
data class ImportPackage(val uri: String) : ThemeManageIntent
|
||||||
data class ImportLegacyJson(val uri: String) : ThemeManageIntent
|
data class ImportLegacyJson(val uri: String) : ThemeManageIntent
|
||||||
|
data class SaveTheme(
|
||||||
|
val name: String,
|
||||||
|
val data: ThemeExportData? = null,
|
||||||
|
val replacedTheme: SavedTheme? = null,
|
||||||
|
) : ThemeManageIntent
|
||||||
|
|
||||||
|
data class ApplySavedTheme(val theme: SavedTheme) : ThemeManageIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ThemeManageEffect {
|
sealed interface ThemeManageEffect {
|
||||||
|
data object SavedThemesChanged : ThemeManageEffect
|
||||||
|
data object RestartRequired : ThemeManageEffect
|
||||||
|
|
||||||
data class ShowResult(
|
data class ShowResult(
|
||||||
@param:StringRes val messageRes: Int,
|
@param:StringRes val messageRes: Int,
|
||||||
val detail: String? = null,
|
val detail: String? = null,
|
||||||
val restartRequired: Boolean = false,
|
val restartRequired: Boolean = false,
|
||||||
|
val savedThemesChanged: Boolean = false,
|
||||||
) : ThemeManageEffect
|
) : ThemeManageEffect
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1436,6 +1436,8 @@
|
|||||||
<string name="theme_manage_export_failed">主题导出失败</string>
|
<string name="theme_manage_export_failed">主题导出失败</string>
|
||||||
<string name="theme_manage_import_success">主题导入成功,部分设置需要重启生效</string>
|
<string name="theme_manage_import_success">主题导入成功,部分设置需要重启生效</string>
|
||||||
<string name="theme_manage_import_failed">主题导入失败</string>
|
<string name="theme_manage_import_failed">主题导入失败</string>
|
||||||
|
<string name="theme_manage_save_failed">主题保存失败</string>
|
||||||
|
<string name="theme_manage_apply_failed">主题应用失败</string>
|
||||||
<string name="theme_manage_save_current">保存当前设置</string>
|
<string name="theme_manage_save_current">保存当前设置</string>
|
||||||
<string name="theme_manage_save_current_summary">保存当前主题配置为新主题</string>
|
<string name="theme_manage_save_current_summary">保存当前主题配置为新主题</string>
|
||||||
<string name="theme_manage_export_current">导出当前主题</string>
|
<string name="theme_manage_export_current">导出当前主题</string>
|
||||||
|
|||||||
@@ -2114,6 +2114,8 @@
|
|||||||
<string name="theme_manage_import_config_summary">從舊版 JSON 檔案匯入主題設定</string>
|
<string name="theme_manage_import_config_summary">從舊版 JSON 檔案匯入主題設定</string>
|
||||||
<string name="theme_manage_import_failed">主題匯入失敗</string>
|
<string name="theme_manage_import_failed">主題匯入失敗</string>
|
||||||
<string name="theme_manage_import_success">主題已匯入。部分更改需要重啟。</string>
|
<string name="theme_manage_import_success">主題已匯入。部分更改需要重啟。</string>
|
||||||
|
<string name="theme_manage_save_failed">主題儲存失敗</string>
|
||||||
|
<string name="theme_manage_apply_failed">主題套用失敗</string>
|
||||||
<string name="theme_manage_label_container_color">標籤容器色</string>
|
<string name="theme_manage_label_container_color">標籤容器色</string>
|
||||||
<string name="theme_manage_label_visibility">標籤可見性</string>
|
<string name="theme_manage_label_visibility">標籤可見性</string>
|
||||||
<string name="theme_manage_name_hint">輸入主題名稱</string>
|
<string name="theme_manage_name_hint">輸入主題名稱</string>
|
||||||
|
|||||||
@@ -1583,6 +1583,8 @@
|
|||||||
<string name="theme_manage_export_failed">無法匯出主題</string>
|
<string name="theme_manage_export_failed">無法匯出主題</string>
|
||||||
<string name="theme_manage_import_success">主題已匯入。部分變更需要重新啟動。</string>
|
<string name="theme_manage_import_success">主題已匯入。部分變更需要重新啟動。</string>
|
||||||
<string name="theme_manage_import_failed">無法匯入主題</string>
|
<string name="theme_manage_import_failed">無法匯入主題</string>
|
||||||
|
<string name="theme_manage_save_failed">無法儲存主題</string>
|
||||||
|
<string name="theme_manage_apply_failed">無法套用主題</string>
|
||||||
<string name="theme_manage_save_current">儲存目前主題</string>
|
<string name="theme_manage_save_current">儲存目前主題</string>
|
||||||
<string name="theme_manage_save_current_summary">將目前外觀設定儲存為主題</string>
|
<string name="theme_manage_save_current_summary">將目前外觀設定儲存為主題</string>
|
||||||
<string name="theme_manage_export_current">匯出目前主題</string>
|
<string name="theme_manage_export_current">匯出目前主題</string>
|
||||||
|
|||||||
@@ -1471,6 +1471,8 @@
|
|||||||
<string name="theme_manage_export_failed">Could not export theme</string>
|
<string name="theme_manage_export_failed">Could not export theme</string>
|
||||||
<string name="theme_manage_import_success">Theme imported. Some changes require a restart.</string>
|
<string name="theme_manage_import_success">Theme imported. Some changes require a restart.</string>
|
||||||
<string name="theme_manage_import_failed">Could not import theme</string>
|
<string name="theme_manage_import_failed">Could not import theme</string>
|
||||||
|
<string name="theme_manage_save_failed">Could not save theme</string>
|
||||||
|
<string name="theme_manage_apply_failed">Could not apply theme</string>
|
||||||
<string name="theme_manage_save_current">Save Current Theme</string>
|
<string name="theme_manage_save_current">Save Current Theme</string>
|
||||||
<string name="theme_manage_save_current_summary">Save the current appearance settings as a theme</string>
|
<string name="theme_manage_save_current_summary">Save the current appearance settings as a theme</string>
|
||||||
<string name="theme_manage_export_current">Export Current Theme</string>
|
<string name="theme_manage_export_current">Export Current Theme</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user