HapeLee 3 месяцев назад
Родитель
Сommit
1ad57a3015

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

@@ -22,100 +22,11 @@ import java.io.FileOutputStream
  */
 object ThemeImportExport {
 
-    private const val DIR_NAME = "saved_themes"
-    private val baseDir get() = File(appCtx.filesDir, DIR_NAME)
-
     private val EXPORT_GSON = GsonBuilder()
         .setPrettyPrinting()
         .disableHtmlEscaping()
         .create()
 
-    private val _savedThemes = mutableListOf<SavedTheme>()
-    val savedThemes: List<SavedTheme> get() = _savedThemes
-
-    init {
-        loadAll()
-    }
-
-    private fun loadAll() {
-        _savedThemes.clear()
-        baseDir.mkdirs()
-        baseDir.listFiles()?.forEach { file ->
-            if (file.isFile && file.extension == "json") {
-                kotlin.runCatching {
-                    val json = file.readText()
-                    val data = parseThemeData(json)
-                        ?: error("不支持的主题配置格式")
-                    val name = file.nameWithoutExtension
-                    _savedThemes.add(SavedTheme(name = name, data = data))
-                }
-            }
-        }
-    }
-
-    fun reload() {
-        loadAll()
-    }
-
-    /**
-     * 保存当前设置为新主题
-     */
-    fun saveCurrentAsTheme(name: String): SavedTheme {
-        val data = exportFromCurrent()
-        return saveThemeData(name, data)
-    }
-
-    /**
-     * 保存指定数据为新主题
-     */
-    fun saveCurrentAsTheme(name: String, data: ThemeExportData): SavedTheme {
-        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 {
-        val file = File(baseDir, "$name.json")
-        baseDir.mkdirs()
-        file.writeText(EXPORT_GSON.toJson(data))
-        val theme = SavedTheme(name = name, data = data)
-        _savedThemes.removeAll { it.name == name }
-        _savedThemes.add(theme)
-        return theme
-    }
-
-    /**
-     * 删除已保存的主题
-     */
-    fun deleteSavedTheme(theme: SavedTheme) {
-        val file = File(baseDir, "${theme.name}.json")
-        if (file.exists() && !file.delete()) {
-            error("Failed to delete saved theme: ${theme.name}")
-        }
-        _savedThemes.removeAll { it.name == theme.name }
-    }
-
-    /**
-     * 导出已保存的主题到文件
-     */
-    fun exportSavedThemeToFile(context: Context, theme: SavedTheme, uri: Uri): Boolean {
-        return try {
-            val json = EXPORT_GSON.toJson(theme.data)
-            context.contentResolver.openOutputStream(uri)?.use {
-                it.write(json.toByteArray())
-            }
-            true
-        } catch (e: Exception) {
-            e.printStackTrace()
-            false
-        }
-    }
-
     /**
      * 从当前配置创建导出数据
      */

+ 17 - 33
app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt

@@ -177,14 +177,12 @@ class ThemePackageManager(
 
     suspend fun loadSavedThemes(): List<SavedTheme> = withContext(Dispatchers.IO) {
         savedThemesRoot.mkdirs()
-        val folderThemes = savedThemesRoot
+        savedThemesRoot
             .listFiles()
             .orEmpty()
             .filter(File::isDirectory)
+            .filterNot { it.name.endsWith(".tmp") }
             .mapNotNull(::readSavedThemeFolder)
-        ThemeImportExport.reload()
-        val folderThemeNames = folderThemes.mapTo(mutableSetOf()) { it.name }
-        folderThemes + ThemeImportExport.savedThemes.filter { it.name !in folderThemeNames }
     }
 
     suspend fun saveTheme(
@@ -210,43 +208,22 @@ class ThemePackageManager(
                 error("Failed to delete saved theme: ${theme.name}")
             }
 
-            ThemeImportExport.deleteSavedTheme(theme)
         }
     }
 
     suspend fun applySavedTheme(theme: SavedTheme): Result<Unit> =
         withContext(Dispatchers.IO) {
             runSuspendCatching {
-                val packageRoot = theme.packageRootPath?.let(::File)
-                val packageManifest = theme.packageManifest
-                if (packageRoot?.isDirectory == true && packageManifest != null) {
-                    applySavedThemeFolder(theme, packageRoot, packageManifest)
-                    return@runSuspendCatching
+                val packageRoot = requireNotNull(theme.packageRootPath?.let(::File)) {
+                    "Saved theme folder is missing: ${theme.name}"
                 }
-
-                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,
-                    )
+                val packageManifest = requireNotNull(theme.packageManifest) {
+                    "Saved theme manifest is missing: ${theme.name}"
                 }
-                if (selectedCoverAlbumId != theme.data.selectedCoverAlbumId) {
-                    ThemeImportExport.saveCurrentAsTheme(
-                        name = theme.name,
-                        data = theme.data.copy(
-                            selectedCoverAlbumId = selectedCoverAlbumId,
-                        ),
-                    )
+                require(packageRoot.isDirectory) {
+                    "Saved theme folder does not exist: ${theme.name}"
                 }
+                applySavedThemeFolder(theme, packageRoot, packageManifest)
             }
         }
 
@@ -698,13 +675,20 @@ class ThemePackageManager(
         selectedCoverAlbumId: String?,
     ) {
         saveTheme(
-            name = ThemeImportExport.uniqueSavedThemeName(name),
+            name = uniqueSavedThemeName(name),
             data = ThemeImportExport.exportFromCurrent().copy(
                 selectedCoverAlbumId = selectedCoverAlbumId,
             ),
         )
     }
 
+    private fun uniqueSavedThemeName(name: String): String {
+        if (!savedThemeDir(name).exists()) return name
+        var index = 2
+        while (savedThemeDir("$name $index").exists()) index++
+        return "$name $index"
+    }
+
     private fun importedThemeName(
         uri: Uri,
         packageName: String? = null,

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -882,7 +882,7 @@ fun ThemeConfigScreen(
         selectedFontPath = ThemeConfig.appFontPath,
         onDismissRequest = { showFontSheet = false },
         onSelectFont = { doc ->
-            ThemeConfig.appFontPath = doc.uri.toString()
+            viewModel.setAppFont(doc)
         },
         onOpenFolderPicker = { fontFolderLauncher.launch(null) },
         startAction = {

+ 26 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -12,6 +12,7 @@ import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.externalFiles
 import io.legado.app.utils.inputStream
+import io.legado.app.utils.openInputStream
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map
@@ -95,6 +96,31 @@ class ThemeConfigViewModel(
         updateBackgroundPath(isDarkTheme, null)
     }
 
+    fun setAppFont(fileDoc: FileDoc) {
+        viewModelScope.launch(Dispatchers.IO) {
+            runCatching {
+                val extension = fileDoc.name.substringAfterLast('.', "ttf")
+                    .lowercase()
+                    .takeIf { it.matches(Regex("[a-z0-9]{1,8}")) }
+                    ?: "ttf"
+                val fontDir = File(appCtx.filesDir, "fonts").apply { mkdirs() }
+                val target = File(fontDir, "app_font.$extension")
+                val temp = File(fontDir, "app_font.$extension.tmp")
+                fileDoc.openInputStream().getOrThrow().use { input ->
+                    FileOutputStream(temp).use(input::copyTo)
+                }
+                if (target.exists() && !target.delete()) {
+                    error("Unable to replace app font")
+                }
+                if (!temp.renameTo(target)) {
+                    temp.copyTo(target, overwrite = true)
+                    temp.delete()
+                }
+                ThemeConfig.appFontPath = target.absolutePath
+            }.onFailure(Throwable::printStackTrace)
+        }
+    }
+
     /**
      * 更新背景路径并清理旧文件
      */

+ 22 - 7
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt

@@ -18,11 +18,14 @@ import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
 
 class ThemeManageViewModel(
     private val themePackageManager: ThemePackageManager,
 ) : ViewModel() {
 
+    private val operationMutex = Mutex()
+
     private val _uiState = MutableStateFlow(ThemeManageUiState())
     val uiState = _uiState.asStateFlow()
 
@@ -34,6 +37,7 @@ class ThemeManageViewModel(
     }
 
     fun onIntent(intent: ThemeManageIntent) {
+        if (operationMutex.isLocked) return
         when (intent) {
             ThemeManageIntent.LoadSavedThemes -> loadSavedThemes()
             is ThemeManageIntent.ExportPackage -> exportPackage(intent)
@@ -46,7 +50,7 @@ class ThemeManageViewModel(
     }
 
     private fun loadSavedThemes() {
-        viewModelScope.launch {
+        launchExclusive {
             _uiState.update { it.copy(loading = true) }
             val themes = themePackageManager.loadSavedThemes()
             _uiState.update {
@@ -69,7 +73,7 @@ class ThemeManageViewModel(
     }
 
     private fun saveTheme(intent: ThemeManageIntent.SaveTheme) {
-        viewModelScope.launch {
+        launchExclusive {
             _uiState.update { it.copy(loading = true) }
             val result = runCatching {
                 themePackageManager.saveTheme(
@@ -95,7 +99,7 @@ class ThemeManageViewModel(
     }
 
     private fun applySavedTheme(theme: SavedTheme) {
-        viewModelScope.launch {
+        launchExclusive {
             _uiState.update { it.copy(loading = true) }
             val result = themePackageManager.applySavedTheme(theme)
             if (result.isSuccess) {
@@ -114,7 +118,7 @@ class ThemeManageViewModel(
     }
 
     private fun deleteSavedTheme(theme: SavedTheme) {
-        viewModelScope.launch {
+        launchExclusive {
             _uiState.update { it.copy(loading = true) }
             val result = themePackageManager.deleteSavedTheme(theme)
             if (result.isSuccess) {
@@ -132,7 +136,7 @@ class ThemeManageViewModel(
     }
 
     private fun exportPackage(intent: ThemeManageIntent.ExportPackage) {
-        viewModelScope.launch {
+        launchExclusive {
             val result = themePackageManager.exportPackage(
                 uri = Uri.parse(intent.uri),
                 themeName = intent.themeName,
@@ -153,13 +157,13 @@ class ThemeManageViewModel(
     }
 
     private fun importPackage(uri: String) {
-        viewModelScope.launch {
+        launchExclusive {
             emitImportResult(themePackageManager.importPackage(Uri.parse(uri)))
         }
     }
 
     private fun importLegacyJson(uri: String) {
-        viewModelScope.launch {
+        launchExclusive {
             emitImportResult(themePackageManager.importLegacyJson(Uri.parse(uri)))
         }
     }
@@ -182,6 +186,17 @@ class ThemeManageViewModel(
             }
         )
     }
+
+    private fun launchExclusive(block: suspend () -> Unit) {
+        if (!operationMutex.tryLock()) return
+        viewModelScope.launch {
+            try {
+                block()
+            } finally {
+                operationMutex.unlock()
+            }
+        }
+    }
 }
 
 @Stable