diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt index 35702677b..795c93f4f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt @@ -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())) }, diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt index dfcf956e1..abd26d051 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt @@ -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 = { diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt index b28b6f6c4..c5edd7d0e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt @@ -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 }, diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index 33efa8825..ce87a4e48 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt @@ -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 -> diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt index af5d4c0d9..09ddd571c 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt b/app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt index dabc609f2..1b5cb3181 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt @@ -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>(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 { + val context = LocalContext.current + return produceState(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 + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt index 9a4fb80f7..f5b64b1e3 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt @@ -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)