diff --git a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt b/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt index b2b302268..527434761 100644 --- a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt @@ -21,6 +21,28 @@ class SearchContentRepository { private var lastSearchResults: List? = null private var lastQueryKey: String? = null + private var lastSearchSession: SearchSession? = null + + data class SearchSession( + val bookUrl: String, + val query: String, + val replaceEnabled: Boolean, + val regexReplace: Boolean, + val results: List, + ) + + fun getLastSession(bookUrl: String): SearchSession? { + return lastSearchSession?.takeIf { it.bookUrl == bookUrl } + } + + fun beginSearch( + bookUrl: String, + query: String, + replaceEnabled: Boolean, + regexReplace: Boolean, + ) { + cacheResults(bookUrl, query, replaceEnabled, regexReplace, emptyList()) + } fun getCache( bookUrl: String, @@ -62,15 +84,34 @@ class SearchContentRepository { if (chapterResults.isNotEmpty()) { allResults.addAll(chapterResults) + cacheResults(book.bookUrl, query, replaceEnabled, regexReplace, allResults) emit(ArrayList(allResults)) } } } - lastSearchResults = allResults - lastQueryKey = searchKey(book.bookUrl, query, replaceEnabled, regexReplace) + cacheResults(book.bookUrl, query, replaceEnabled, regexReplace, allResults) emit(ArrayList(allResults)) }.flowOn(Dispatchers.Default) + private fun cacheResults( + bookUrl: String, + query: String, + replaceEnabled: Boolean, + regexReplace: Boolean, + results: List, + ) { + val cachedResults = ArrayList(results) + lastSearchResults = cachedResults + lastQueryKey = searchKey(bookUrl, query, replaceEnabled, regexReplace) + lastSearchSession = SearchSession( + bookUrl = bookUrl, + query = query, + replaceEnabled = replaceEnabled, + regexReplace = regexReplace, + results = cachedResults, + ) + } + private fun searchKey( bookUrl: String, query: String, diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 3b95c8455..ba58f38c1 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -34,6 +34,7 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.HttpTTS import io.legado.app.exception.NoStackTraceException import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.exoplayer.InputStreamDataSource import io.legado.app.help.http.okHttpClient @@ -41,6 +42,7 @@ import io.legado.app.model.ReadAloud import io.legado.app.model.ReadBook import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.config.readConfig.ReadConfig import io.legado.app.utils.FileUtils import io.legado.app.utils.MD5Utils @@ -48,6 +50,7 @@ import io.legado.app.utils.printOnDebug import io.legado.app.utils.servicePendingIntent import io.legado.app.utils.toastOnUi import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -201,9 +204,38 @@ class HttpReadAloudService : BaseReadAloudService(), } } - // 辅助方法:确保能读到文件 - private fun getChapterContent(book: Book, chapter: BookChapter): String? { - return BookHelp.getContent(book, chapter) + private suspend fun getPreDownloadChapter( + book: Book, + chapter: BookChapter, + ): Pair>? { + val content = BookHelp.getContent(book, chapter) ?: return null + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val displayTitle = chapter.getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + book.getUseReplaceRule(), + ) + val processedContent = contentProcessor.getContent( + book, + chapter, + content, + includeTitle = false, + ) + val textChapter = ChapterProvider.getTextChapterAsync( + CoroutineScope(currentCoroutineContext()), + book, + chapter, + displayTitle, + processedContent, + ReadBook.simulatedChapterSize, + ) + for (ignored in textChapter.layoutChannel) { + currentCoroutineContext().ensureActive() + } + val contentList = textChapter + .getNeedReadAloud(0, ReadConfig.readAloudByPage, 0) + .split("\n") + .filter { it.isNotEmpty() } + return textChapter to contentList } private suspend fun preDownloadAudios(httpTts: HttpTTS) { @@ -218,25 +250,18 @@ class HttpReadAloudService : BaseReadAloudService(), val targetIndex = currentIdx + i val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break - // 1. 获取内容 - val contentString = getChapterContent(book, chapter) - if (contentString.isNullOrEmpty()) continue // 内容没下载,跳过 - - val contentList = contentString.split("\n").filter { it.isNotEmpty() } + val (textChapter, contentList) = + getPreDownloadChapter(book, chapter) ?: continue contentList.forEach { content -> currentCoroutineContext().ensureActive() - // 2. 生成文件名:必须用 chapter.title (数据库原始标题) - val titleMd5 = MD5Utils.md5Encode16(chapter.title) - val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content") - val fileName = "${titleMd5}_${contentMd5}" + val fileName = md5SpeakFileName(content, textChapter) val speakText = content.replace(AppPattern.notReadAloudRegex, "") if (speakText.isEmpty()) { createSilentSound(fileName) } else if (!hasSpeakFile(fileName)) { - // 3. 文件不存在才下载 runCatching { val inputStream = getSpeakStream(httpTts, speakText) if (inputStream != null) { @@ -307,17 +332,12 @@ class HttpReadAloudService : BaseReadAloudService(), val targetIndex = currentIdx + i val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break - val contentString = getChapterContent(book, chapter) - if (contentString.isNullOrEmpty()) continue - - val contentList = contentString.split("\n").filter { it.isNotEmpty() } + val (textChapter, contentList) = + getPreDownloadChapter(book, chapter) ?: continue contentList.forEach { content -> currentCoroutineContext().ensureActive() - // 同样使用数据库标题,保持一致 - val titleMd5 = MD5Utils.md5Encode16(chapter.title) - val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content") - val fileName = "${titleMd5}_${contentMd5}" + val fileName = md5SpeakFileName(content, textChapter) val speakText = content.replace(AppPattern.notReadAloudRegex, "") val dataSourceFactory = createDataSourceFactory(httpTts, speakText) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt index 065cf636a..1158026a8 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt @@ -95,7 +95,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -175,7 +174,6 @@ import io.legado.app.ui.widget.components.menuItem.MenuItemIcon import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.text.AppText -import kotlinx.coroutines.flow.collectLatest import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.ceil @@ -2244,7 +2242,7 @@ private fun ReadMenuSlider( ) { if (glassThumbEnabled && backdrop != null) { ReadMenuLiquidSlider( - value = { value }, + value = value, onValueChange = onValueChange, valueRange = valueRange, visibilityThreshold = 0.001f, @@ -2272,7 +2270,7 @@ private fun ReadMenuSlider( @Composable private fun ReadMenuLiquidSlider( - value: () -> Float, + value: Float, onValueChange: (Float) -> Unit, valueRange: ClosedFloatingPointRange, visibilityThreshold: Float, @@ -2303,7 +2301,7 @@ private fun ReadMenuLiquidSlider( val dampedDragAnimation = remember(animationScope, trackWidth, rangeStart, rangeEnd, isLtr) { DampedDragAnimation( animationScope = animationScope, - initialValue = value(), + initialValue = value, valueRange = valueRange, visibilityThreshold = visibilityThreshold, initialScale = 1f, @@ -2333,13 +2331,10 @@ private fun ReadMenuLiquidSlider( ) } - LaunchedEffect(dampedDragAnimation) { - snapshotFlow { value() } - .collectLatest { currentValue -> - if (dampedDragAnimation.targetValue != currentValue) { - dampedDragAnimation.updateValue(currentValue) - } - } + LaunchedEffect(dampedDragAnimation, value) { + if (dampedDragAnimation.targetValue != value) { + dampedDragAnimation.updateValue(value) + } } val progress = if (range == 0f) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 606e88bd5..499b32028 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -119,7 +119,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.FileNotFoundException -import java.net.URLEncoder import kotlin.coroutines.coroutineContext /** @@ -849,12 +848,11 @@ class ReadBookViewModel( is ReadBookIntent.ExportAllHttpTtsAsUrl -> { execute { val json = exportHttpTtsJson() - val url = uploadRepository.upload( + uploadRepository.upload( fileName = "httpTTS.json", file = json, contentType = "application/json" ) - "legado://import/httpTTS?src=" + URLEncoder.encode(url, "UTF-8") }.onSuccess { url -> context.sendToClip(url) _effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.copy_url))) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index 3faf550f6..e4fd473f0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -244,13 +244,18 @@ data class TextLine( null val lineY = height + (ReadBookConfig.durConfig.underlinePadding - 10).dpToPx() + val pageOffset = if (textPage.doublePage && !isLeftLine) { + ChapterProvider.viewWidth / 2f + } else { + 0f + } val startX = if (ReadBookConfig.underlineExtend) { - ChapterProvider.paddingLeft.toFloat() + pageOffset + ChapterProvider.paddingLeft } else { lineStart + indentWidth } val endX = if (ReadBookConfig.underlineExtend) { - (ChapterProvider.paddingLeft + ChapterProvider.visibleWidth).toFloat() + pageOffset + ChapterProvider.paddingLeft + ChapterProvider.visibleWidth } else { lineEnd } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index ef709ffaf..cae801a55 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -1057,6 +1057,10 @@ object ChapterProvider { * 更新View尺寸 */ fun upViewSize(width: Int, height: Int) { + upViewSizeRunnable?.let { + handler.removeCallbacks(it) + upViewSizeRunnable = null + } if (width <= 0 || height <= 0) { return } @@ -1069,9 +1073,6 @@ object ChapterProvider { } else { notifyViewSizeChange(width, height) } - } else if (upViewSizeRunnable != null) { - handler.removeCallbacks(upViewSizeRunnable!!) - upViewSizeRunnable = null } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt index 8e0d27f71..619aadb97 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt @@ -96,12 +96,7 @@ fun ContentEditSheet( AppModalBottomSheet( show = show, - onDismissRequest = { - if (state.contentEditTitle.isNotEmpty()) { - onIntent(ReadBookIntent.SaveContentEdit(state.contentEditText, state.contentEditSaveToSource)) - } - onDismissRequest() - }, + onDismissRequest = onDismissRequest, title = state.contentEditTitle, ) { Column( diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ShadowSetSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ShadowSetSheet.kt index f58940681..889f7a136 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ShadowSetSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ShadowSetSheet.kt @@ -1,7 +1,10 @@ package io.legado.app.ui.book.read.sheet +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -11,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.help.config.ReadBookConfig import io.legado.app.ui.book.read.ConfigUpdate @@ -39,7 +43,12 @@ fun ShadowSetSheet( onDismissRequest = onDismissRequest, title = stringResource(R.string.text_shadow_set), ) { - Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp), + ) { TinySwitchSettingItem( title = stringResource(R.string.text_shadow_set), checked = textShadow, diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/UnderlineConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/UnderlineConfigSheet.kt index 9b1bc17dc..c285356d3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/UnderlineConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/UnderlineConfigSheet.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -52,6 +54,7 @@ fun UnderlineConfigSheet( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(rememberScrollState()) .padding(bottom = 16.dp), ) { TinySwitchSettingItem( diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt index 324f9eab8..f602c8bd1 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -45,13 +45,19 @@ class SearchContentViewModel( private val searchContentRepository: SearchContentRepository ) : ViewModel() { - private val _searchQuery = MutableStateFlow(initialSearchWord ?: "") + private val restoredSession = if (initialSearchWord == null) { + searchContentRepository.getLastSession(bookUrl) + } else { + null + } + + private val _searchQuery = MutableStateFlow(initialSearchWord ?: restoredSession?.query.orEmpty()) val searchQuery = _searchQuery.asStateFlow() - private val _replaceEnabled = MutableStateFlow(false) + private val _replaceEnabled = MutableStateFlow(restoredSession?.replaceEnabled ?: false) val replaceEnabled = _replaceEnabled.asStateFlow() - private val _regexReplace = MutableStateFlow(false) + private val _regexReplace = MutableStateFlow(restoredSession?.regexReplace ?: false) val regexReplace = _regexReplace.asStateFlow() private val _uiState = MutableStateFlow(SearchUiState()) @@ -138,6 +144,7 @@ class SearchContentViewModel( return } + searchContentRepository.beginSearch(bookUrl, query, replace, regex) searchJob = viewModelScope.launch { _uiState.value.book?.let { book -> saveSearchHistory(book, query) diff --git a/app/src/test/java/io/legado/app/data/repository/SearchContentRepositoryTest.kt b/app/src/test/java/io/legado/app/data/repository/SearchContentRepositoryTest.kt new file mode 100644 index 000000000..4033893c3 --- /dev/null +++ b/app/src/test/java/io/legado/app/data/repository/SearchContentRepositoryTest.kt @@ -0,0 +1,39 @@ +package io.legado.app.data.repository + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class SearchContentRepositoryTest { + + @Test + fun `beginSearch remembers latest search parameters for the book`() { + val repository = SearchContentRepository() + + repository.beginSearch( + bookUrl = "book-1", + query = "keyword", + replaceEnabled = true, + regexReplace = false, + ) + + val session = repository.getLastSession("book-1") + assertEquals("keyword", session?.query) + assertEquals(true, session?.replaceEnabled) + assertEquals(false, session?.regexReplace) + assertEquals(emptyList(), session?.results) + } + + @Test + fun `getLastSession does not return another book search`() { + val repository = SearchContentRepository() + repository.beginSearch( + bookUrl = "book-1", + query = "keyword", + replaceEnabled = false, + regexReplace = false, + ) + + assertNull(repository.getLastSession("book-2")) + } +}