fix: 修复内容编辑定位崩溃
This commit is contained in:
@@ -340,6 +340,7 @@ sealed interface ReadBookIntent {
|
|||||||
data object ToggleReadUrlInBrowser : ReadBookIntent
|
data object ToggleReadUrlInBrowser : ReadBookIntent
|
||||||
|
|
||||||
// Content edit
|
// Content edit
|
||||||
|
data object OpenContentEdit : ReadBookIntent
|
||||||
data object LoadContentEdit : ReadBookIntent
|
data object LoadContentEdit : ReadBookIntent
|
||||||
data class SaveContentEdit(val content: String, val saveToSource: Boolean) : ReadBookIntent
|
data class SaveContentEdit(val content: String, val saveToSource: Boolean) : ReadBookIntent
|
||||||
data object ResetContentEdit : ReadBookIntent
|
data object ResetContentEdit : ReadBookIntent
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class ReadBookController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun openContentEdit() {
|
override fun openContentEdit() {
|
||||||
viewModel.onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentEdit))
|
viewModel.onIntent(ReadBookIntent.OpenContentEdit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addBookmark() {
|
override fun addBookmark() {
|
||||||
@@ -552,7 +552,7 @@ class ReadBookController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
viewModel.onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentEdit))
|
viewModel.onIntent(ReadBookIntent.OpenContentEdit)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1668,7 +1668,7 @@ private fun OverflowDropdownMenu(
|
|||||||
text = stringResource(R.string.edit_content),
|
text = stringResource(R.string.edit_content),
|
||||||
onClick = {
|
onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentEdit))
|
onIntent(ReadBookIntent.OpenContentEdit)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (!state.isLocalBook) {
|
if (!state.isLocalBook) {
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ class ReadBookViewModel(
|
|||||||
|
|
||||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||||
private var pendingBooksDirReloadChapterList: Boolean = false
|
private var pendingBooksDirReloadChapterList: Boolean = false
|
||||||
|
private var pendingContentEditCursorOffset: Int? = null
|
||||||
|
private var pendingContentEditAnchor: String? = null
|
||||||
|
|
||||||
val isInitFinish: Boolean get() = _uiState.value.isInitFinish
|
val isInitFinish: Boolean get() = _uiState.value.isInitFinish
|
||||||
|
|
||||||
@@ -445,6 +447,7 @@ class ReadBookViewModel(
|
|||||||
}
|
}
|
||||||
is ReadBookIntent.OpenChapterUrl -> openChapterUrl()
|
is ReadBookIntent.OpenChapterUrl -> openChapterUrl()
|
||||||
is ReadBookIntent.ToggleReadUrlInBrowser -> toggleReadUrlInBrowser()
|
is ReadBookIntent.ToggleReadUrlInBrowser -> toggleReadUrlInBrowser()
|
||||||
|
is ReadBookIntent.OpenContentEdit -> openContentEdit()
|
||||||
is ReadBookIntent.LoadContentEdit -> loadContentEdit()
|
is ReadBookIntent.LoadContentEdit -> loadContentEdit()
|
||||||
is ReadBookIntent.SaveContentEdit -> saveContentEdit(intent.content, intent.saveToSource)
|
is ReadBookIntent.SaveContentEdit -> saveContentEdit(intent.content, intent.saveToSource)
|
||||||
is ReadBookIntent.ResetContentEdit -> resetContentEdit()
|
is ReadBookIntent.ResetContentEdit -> resetContentEdit()
|
||||||
@@ -2439,6 +2442,62 @@ class ReadBookViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openContentEdit() {
|
||||||
|
pendingContentEditCursorOffset = currentContentEditOffset()
|
||||||
|
pendingContentEditAnchor = currentContentEditAnchor()
|
||||||
|
_uiState.update { it.copy(activeSheet = ReadBookSheet.ContentEdit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentContentEditPage(): TextPage? {
|
||||||
|
return ReadBook.curTextChapter?.getPage(ReadBook.durPageIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentContentEditOffset(): Int {
|
||||||
|
val page = currentContentEditPage()
|
||||||
|
return page?.lines
|
||||||
|
?.firstOrNull { !it.isTitle && it.text.isNotBlank() }
|
||||||
|
?.chapterPosition
|
||||||
|
?: page?.lines
|
||||||
|
?.firstOrNull { !it.isTitle }
|
||||||
|
?.chapterPosition
|
||||||
|
?: ReadBook.durChapterPos
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentContentEditAnchor(): String? {
|
||||||
|
return currentContentEditPage()
|
||||||
|
?.lines
|
||||||
|
?.firstOrNull { !it.isTitle && it.text.isNotBlank() }
|
||||||
|
?.text
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveContentEditCursorOffset(text: String): Int {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
clearPendingContentEditLocation()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val preferred = (pendingContentEditCursorOffset ?: currentContentEditOffset())
|
||||||
|
.coerceIn(0, text.length)
|
||||||
|
val anchor = pendingContentEditAnchor ?: currentContentEditAnchor()
|
||||||
|
clearPendingContentEditLocation()
|
||||||
|
if (anchor.isNullOrBlank()) {
|
||||||
|
return preferred
|
||||||
|
}
|
||||||
|
val startIndex = (preferred - 200).coerceAtLeast(0)
|
||||||
|
val nearIndex = text.indexOf(anchor, startIndex = startIndex)
|
||||||
|
if (nearIndex >= 0) {
|
||||||
|
return nearIndex
|
||||||
|
}
|
||||||
|
val anyIndex = text.indexOf(anchor)
|
||||||
|
return if (anyIndex >= 0) anyIndex else preferred
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearPendingContentEditLocation() {
|
||||||
|
pendingContentEditCursorOffset = null
|
||||||
|
pendingContentEditAnchor = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadContentEdit() {
|
private fun loadContentEdit() {
|
||||||
_uiState.update { it.copy(contentEditLoading = true, contentEditText = "") }
|
_uiState.update { it.copy(contentEditLoading = true, contentEditText = "") }
|
||||||
execute {
|
execute {
|
||||||
@@ -2451,11 +2510,12 @@ class ReadBookViewModel(
|
|||||||
val rawContent = BookHelp.getContent(book, chapter) ?: return@execute
|
val rawContent = BookHelp.getContent(book, chapter) ?: return@execute
|
||||||
val text = contentProcessor.getContent(book, chapter, rawContent, includeTitle = false)
|
val text = contentProcessor.getContent(book, chapter, rawContent, includeTitle = false)
|
||||||
.toString()
|
.toString()
|
||||||
|
val cursorOffset = resolveContentEditCursorOffset(text)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
contentEditText = text,
|
contentEditText = text,
|
||||||
contentEditTitle = title,
|
contentEditTitle = title,
|
||||||
contentEditCursorOffset = ReadBook.durChapterPos.coerceIn(0, text.length),
|
contentEditCursorOffset = cursorOffset,
|
||||||
contentEditIsLocalTxt = book.isLocalTxt,
|
contentEditIsLocalTxt = book.isLocalTxt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2496,10 +2556,11 @@ class ReadBookViewModel(
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
val cursorOffset = resolveContentEditCursorOffset(text)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
contentEditText = text,
|
contentEditText = text,
|
||||||
contentEditCursorOffset = ReadBook.durChapterPos.coerceIn(0, text.length),
|
contentEditCursorOffset = cursorOffset,
|
||||||
contentEditLoading = false,
|
contentEditLoading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ fun ContentEditSheet(
|
|||||||
val editorState = remember { TextFieldState() }
|
val editorState = remember { TextFieldState() }
|
||||||
val editorScrollState = rememberScrollState()
|
val editorScrollState = rememberScrollState()
|
||||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
var pendingLocateOffset by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(show) {
|
LaunchedEffect(show) {
|
||||||
if (!show) return@LaunchedEffect
|
if (!show) return@LaunchedEffect
|
||||||
@@ -57,19 +58,31 @@ fun ContentEditSheet(
|
|||||||
if (editorState.text.toString() != state.contentEditText) {
|
if (editorState.text.toString() != state.contentEditText) {
|
||||||
editorState.setTextAndSelectAll(state.contentEditText)
|
editorState.setTextAndSelectAll(state.contentEditText)
|
||||||
editorState.edit {
|
editorState.edit {
|
||||||
selection = TextRange(state.contentEditCursorOffset.coerceIn(0, length))
|
val offset = state.contentEditCursorOffset.coerceIn(0, length)
|
||||||
|
selection = TextRange(offset)
|
||||||
|
pendingLocateOffset = offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(show, state.contentEditLoading, state.contentEditCursorOffset, textLayoutResult) {
|
LaunchedEffect(show, state.contentEditLoading, pendingLocateOffset, textLayoutResult) {
|
||||||
val layoutResult = textLayoutResult
|
val layoutResult = textLayoutResult
|
||||||
if (!show || state.contentEditLoading || layoutResult == null) {
|
val locateOffset = pendingLocateOffset
|
||||||
|
if (!show || state.contentEditLoading || layoutResult == null || locateOffset == null) {
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
val offset = state.contentEditCursorOffset.coerceIn(0, editorState.text.length)
|
val layoutTextLength = layoutResult.layoutInput.text.length
|
||||||
|
if (layoutTextLength != editorState.text.length) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
if (layoutTextLength == 0) {
|
||||||
|
pendingLocateOffset = null
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val offset = locateOffset.coerceIn(0, layoutTextLength)
|
||||||
val cursorTop = layoutResult.getCursorRect(offset).top.toInt()
|
val cursorTop = layoutResult.getCursorRect(offset).top.toInt()
|
||||||
editorScrollState.scrollTo((cursorTop - 120).coerceIn(0, editorScrollState.maxValue))
|
editorScrollState.scrollTo((cursorTop - 120).coerceIn(0, editorScrollState.maxValue))
|
||||||
|
pendingLocateOffset = null
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(editorState) {
|
LaunchedEffect(editorState) {
|
||||||
|
|||||||
Reference in New Issue
Block a user