perf: 优化字体选择器性能 (#1497)
This commit is contained in:
@@ -12,7 +12,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.repository.ReadPreferences
|
||||
import io.legado.app.data.repository.ReadSettingsRepository
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.book.read.sheet.BgTextConfigSheet
|
||||
@@ -37,6 +36,7 @@ import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
|
||||
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
|
||||
import io.legado.app.ui.widget.components.FontFolderState
|
||||
import io.legado.app.ui.widget.components.FontSelectSheet
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
|
||||
@@ -172,15 +172,21 @@ fun ReadBookScreen(
|
||||
)
|
||||
val fontSelectReadSettings: ReadSettingsRepository = org.koin.compose.koinInject()
|
||||
val fontSelectPreferences by fontSelectReadSettings.preferences.collectAsStateWithLifecycle(
|
||||
initialValue = ReadPreferences()
|
||||
initialValue = null
|
||||
)
|
||||
val fontSelectFolderUri = fontSelectPreferences.fontFolder
|
||||
.takeIf { it.isNotEmpty() }?.toUri()
|
||||
val fontSelectFolderState = remember(fontSelectPreferences) {
|
||||
val pref = fontSelectPreferences
|
||||
if (pref == null) {
|
||||
FontFolderState.Loading
|
||||
} else {
|
||||
FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
|
||||
}
|
||||
}
|
||||
val fontSelectSystemTypefaces = stringArrayResource(R.array.system_typefaces)
|
||||
FontSelectSheet(
|
||||
show = state.activeSheet is ReadBookSheet.FontSelect,
|
||||
title = stringResource(R.string.select_font),
|
||||
fontFolderUri = fontSelectFolderUri,
|
||||
folderState = fontSelectFolderState,
|
||||
selectedFontPath = ReadBookConfig.textFont,
|
||||
onDismissRequest = dismissSheet,
|
||||
onSelectFont = { onIntent(ReadBookIntent.SelectFont(it.uri.toString())) },
|
||||
@@ -191,7 +197,7 @@ fun ReadBookScreen(
|
||||
FontSelectSheet(
|
||||
show = state.activeSheet is ReadBookSheet.TitleFontSelect,
|
||||
title = stringResource(R.string.read_config_title_settings),
|
||||
fontFolderUri = fontSelectFolderUri,
|
||||
folderState = fontSelectFolderState,
|
||||
selectedFontPath = ReadBookConfig.titleFont,
|
||||
onDismissRequest = dismissSheet,
|
||||
onSelectFont = { onIntent(ReadBookIntent.SelectTitleFont(it.uri.toString())) },
|
||||
|
||||
@@ -49,12 +49,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.repository.ReadPreferences
|
||||
import io.legado.app.data.repository.ReadSettingsRepository
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.book.read.ConfigUpdate
|
||||
import io.legado.app.ui.book.read.ReadBookIntent
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.FontFolderState
|
||||
import io.legado.app.ui.widget.components.FontSelectSheet
|
||||
import io.legado.app.ui.widget.components.SectionTitle
|
||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||
@@ -568,15 +568,22 @@ internal fun HeaderFooterPage(
|
||||
// Font selector for header/footer
|
||||
val readSettingsRepository: ReadSettingsRepository = koinInject()
|
||||
val preferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
|
||||
initialValue = ReadPreferences()
|
||||
initialValue = null
|
||||
)
|
||||
val fontFolderUri = preferences.fontFolder.takeIf { it.isNotEmpty() }?.toUri()
|
||||
val fontFolderState = remember(preferences) {
|
||||
val pref = preferences
|
||||
if (pref == null) {
|
||||
FontFolderState.Loading
|
||||
} else {
|
||||
FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
|
||||
}
|
||||
}
|
||||
val systemTypefaces = stringArrayResource(R.array.system_typefaces)
|
||||
|
||||
FontSelectSheet(
|
||||
show = showFontSelect,
|
||||
title = stringResource(R.string.select_font),
|
||||
fontFolderUri = fontFolderUri,
|
||||
folderState = fontFolderState,
|
||||
selectedFontPath = ReadBookConfig.headerFont,
|
||||
onDismissRequest = { showFontSelect = false },
|
||||
onSelectFont = {
|
||||
|
||||
@@ -43,13 +43,13 @@ import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.repository.ReadPreferences
|
||||
import io.legado.app.data.repository.ReadSettingsRepository
|
||||
import io.legado.app.data.repository.configNames
|
||||
import io.legado.app.data.repository.toJsonArray
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.FontFolderState
|
||||
import io.legado.app.ui.widget.components.FontSelectSheet
|
||||
import io.legado.app.ui.widget.components.SectionTitle
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
@@ -515,9 +515,16 @@ fun HighlightRuleEditSheet(
|
||||
val readSettingsRepository: ReadSettingsRepository = org.koin.compose.koinInject()
|
||||
val fontSelectScope = rememberCoroutineScope()
|
||||
val fontSelectPreferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
|
||||
initialValue = ReadPreferences()
|
||||
initialValue = null
|
||||
)
|
||||
val fontFolderUri = fontSelectPreferences.fontFolder.takeIf { it.isNotEmpty() }?.toUri()
|
||||
val fontFolderState = remember(fontSelectPreferences) {
|
||||
val pref = fontSelectPreferences
|
||||
if (pref == null) {
|
||||
FontFolderState.Loading
|
||||
} else {
|
||||
FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
|
||||
}
|
||||
}
|
||||
val systemTypefaces = stringArrayResource(R.array.system_typefaces)
|
||||
val fontFolderLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree()
|
||||
@@ -534,7 +541,7 @@ fun HighlightRuleEditSheet(
|
||||
FontSelectSheet(
|
||||
show = showFontSelect,
|
||||
title = stringResource(R.string.select_font),
|
||||
fontFolderUri = fontFolderUri,
|
||||
folderState = fontFolderState,
|
||||
selectedFontPath = fontPath,
|
||||
onDismissRequest = { showFontSelect = false },
|
||||
onSelectFont = { fontPath = it.uri.toString(); showFontSelect = false },
|
||||
|
||||
@@ -74,7 +74,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.help.LauncherIconHelp
|
||||
import io.legado.app.help.config.ThemeConfigStore
|
||||
import io.legado.app.ui.config.labConfig.LabConfig
|
||||
@@ -83,6 +82,7 @@ import io.legado.app.ui.theme.ThemeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.FontFolderState
|
||||
import io.legado.app.ui.widget.components.FontSelectSheet
|
||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
@@ -98,9 +98,7 @@ 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.getPrefString
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.putPrefString
|
||||
import io.legado.app.utils.restart
|
||||
import io.legado.app.utils.takePersistablePermissionSafely
|
||||
import io.legado.app.utils.toastOnUi
|
||||
@@ -128,19 +126,22 @@ fun ThemeConfigScreen(
|
||||
var showFontSheet by remember { mutableStateOf(false) }
|
||||
val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
|
||||
|
||||
var fontFolderUri by remember {
|
||||
mutableStateOf(
|
||||
context.getPrefString(PreferKey.fontFolder)?.toUri()
|
||||
)
|
||||
val fontFolder by viewModel.fontFolder.collectAsStateWithLifecycle()
|
||||
val fontFolderState = remember(fontFolder) {
|
||||
val folder = fontFolder
|
||||
if (folder == null) {
|
||||
FontFolderState.Loading
|
||||
} else {
|
||||
FontFolderState.Loaded(folder.takeIf { it.isNotEmpty() }?.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
val fontFolderLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
fontFolderUri = uri
|
||||
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
||||
viewModel.setFontFolder(uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,7 +870,7 @@ fun ThemeConfigScreen(
|
||||
FontSelectSheet(
|
||||
show = showFontSheet,
|
||||
title = stringResource(R.string.font_setting),
|
||||
fontFolderUri = fontFolderUri,
|
||||
folderState = fontFolderState,
|
||||
selectedFontPath = ThemeConfig.appFontPath,
|
||||
onDismissRequest = { showFontSheet = false },
|
||||
onSelectFont = { doc ->
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||
import io.legado.app.data.repository.ReadSettingsRepository
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.MD5Utils
|
||||
@@ -13,6 +14,7 @@ import io.legado.app.utils.externalFiles
|
||||
import io.legado.app.utils.inputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -21,9 +23,22 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class ThemeConfigViewModel(
|
||||
private val localPreferencesRepository: LocalPreferencesRepository
|
||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||
private val readSettingsRepository: ReadSettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val fontFolder = readSettingsRepository.preferences
|
||||
.map { it.fontFolder as String? }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
fun setFontFolder(path: String) {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
readSettingsRepository.setFontFolder(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showThemeRefactorTip = localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, true)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -25,8 +26,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -41,35 +44,49 @@ import io.legado.app.R
|
||||
import io.legado.app.help.loadFontFiles
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.utils.FileDoc
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val fontGridHeight = 360.dp
|
||||
|
||||
|
||||
sealed interface FontFolderState {
|
||||
data object Loading : FontFolderState
|
||||
data class Loaded(val uri: Uri?) : FontFolderState
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared font selection grid with search support.
|
||||
*
|
||||
* @param fontFolderUri URI of the font folder to load from
|
||||
* @param folderState 字体文件夹加载状态,见 [FontFolderState]
|
||||
* @param selectedFontName currently selected font name (for check mark), null to hide
|
||||
* @param onSelectFont called when a font file is selected
|
||||
* @param emptyText text to show when no fonts found
|
||||
*/
|
||||
@Composable
|
||||
fun FontSelectGrid(
|
||||
fontFolderUri: android.net.Uri?,
|
||||
folderState: FontFolderState,
|
||||
selectedFontName: String?,
|
||||
onSelectFont: (FileDoc) -> Unit,
|
||||
emptyText: String? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var filesLoading by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(fontFolderUri) {
|
||||
isLoading = true
|
||||
fontItems = withContext(Dispatchers.IO) {
|
||||
loadFontFiles(context, fontFolderUri)
|
||||
LaunchedEffect(folderState) {
|
||||
if (folderState is FontFolderState.Loaded) {
|
||||
filesLoading = true
|
||||
try {
|
||||
fontItems = withContext(Dispatchers.IO) {
|
||||
loadFontFiles(context, folderState.uri)
|
||||
}
|
||||
} finally {
|
||||
filesLoading = false
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
val filteredItems = remember(fontItems, searchQuery) {
|
||||
@@ -77,6 +94,8 @@ fun FontSelectGrid(
|
||||
else fontItems.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
||||
}
|
||||
|
||||
val showLoading = folderState is FontFolderState.Loading || filesLoading
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -92,11 +111,11 @@ fun FontSelectGrid(
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// Font grid
|
||||
if (isLoading) {
|
||||
if (showLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
.height(fontGridHeight),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
@@ -105,7 +124,7 @@ fun FontSelectGrid(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
.height(fontGridHeight),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
@@ -120,7 +139,7 @@ fun FontSelectGrid(
|
||||
contentPadding = PaddingValues(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.height(360.dp),
|
||||
modifier = Modifier.height(fontGridHeight),
|
||||
) {
|
||||
items(filteredItems, key = { it.name }) { item ->
|
||||
FontItem(
|
||||
@@ -140,21 +159,7 @@ private fun FontItem(
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val fontFamily = remember(item.uri) {
|
||||
runCatching {
|
||||
val uri = item.uri
|
||||
val typeface: Typeface? = if (uri.scheme == "content") {
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||
Typeface.Builder(it.fileDescriptor).build()
|
||||
}
|
||||
} else {
|
||||
uri.path?.let { Typeface.createFromFile(it) }
|
||||
}
|
||||
typeface?.let { FontFamily(it) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
val fontFamily by rememberItemFontFamily(item.uri)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -186,3 +191,23 @@ private fun FontItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberItemFontFamily(uri: Uri): State<FontFamily?> {
|
||||
val context = LocalContext.current
|
||||
return produceState<FontFamily?>(initialValue = null, uri) {
|
||||
val parsed = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val typeface: Typeface? = if (uri.scheme == "content") {
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||
Typeface.Builder(it.fileDescriptor).build()
|
||||
}
|
||||
} else {
|
||||
uri.path?.let { Typeface.createFromFile(it) }
|
||||
}
|
||||
typeface?.let { FontFamily(it) }
|
||||
}.onFailure { if (it is CancellationException) throw it }.getOrNull()
|
||||
}
|
||||
if (parsed != null) value = parsed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.TextFields
|
||||
@@ -24,7 +23,7 @@ import io.legado.app.utils.FileDoc
|
||||
fun FontSelectSheet(
|
||||
show: Boolean = true,
|
||||
title: String,
|
||||
fontFolderUri: Uri?,
|
||||
folderState: FontFolderState,
|
||||
selectedFontPath: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectFont: (FileDoc) -> Unit,
|
||||
@@ -83,7 +82,7 @@ fun FontSelectSheet(
|
||||
},
|
||||
) {
|
||||
FontSelectGrid(
|
||||
fontFolderUri = fontFolderUri,
|
||||
folderState = folderState,
|
||||
selectedFontName = selectedFontName,
|
||||
onSelectFont = { doc ->
|
||||
onSelectFont(doc)
|
||||
|
||||
Reference in New Issue
Block a user