فهرست منبع

fix: 修复阅读界面功能问题

HapeLee 4 ماه پیش
والد
کامیت
eb0a0ff2ff

+ 43 - 2
app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt

@@ -21,6 +21,28 @@ class SearchContentRepository {
 
     private var lastSearchResults: List<SearchResult>? = 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<SearchResult>,
+    )
+
+    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<SearchResult>,
+    ) {
+        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,

+ 41 - 21
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<TextChapter, List<String>>? {
+        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)

+ 7 - 12
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<Float>,
     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) {

+ 1 - 3
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)))

+ 7 - 2
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
         }

+ 4 - 3
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
         }
     }
 

+ 1 - 6
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(

+ 10 - 1
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,

+ 3 - 0
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(

+ 10 - 3
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)

+ 39 - 0
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<Any>(), 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"))
+    }
+}