refactor: reader viewmodel
This commit is contained in:
parent
3817cc1980
commit
2ab62e629f
17 changed files with 566 additions and 500 deletions
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.use_case.book
|
||||
|
||||
import ua.acclorite.book_story.core.helpers.coerceAndPreventNaN
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText.Chapter
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetChapterProgressUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(index: Int, text: List<ReaderText>): Pair<Chapter?, Float> {
|
||||
fun getCurrentChapter(index: Int): Chapter? {
|
||||
for (textIndex in index downTo 0) {
|
||||
val readerText = text.getOrNull(textIndex) ?: break
|
||||
if (readerText is Chapter) {
|
||||
return readerText
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentChapterProgress(currentChapter: Chapter?): Float {
|
||||
return currentChapter?.let { currentChapter ->
|
||||
val startIndex = text.indexOf(currentChapter).coerceIn(0, text.lastIndex)
|
||||
val endIndex = (text.indexOfFirst {
|
||||
it is Chapter && text.indexOf(it) > startIndex
|
||||
}.takeIf { it != -1 }) ?: (text.lastIndex + 1)
|
||||
|
||||
val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
|
||||
val chapterLength = endIndex - (startIndex + 1)
|
||||
(currentIndexInChapter / chapterLength.toFloat())
|
||||
}.coerceAndPreventNaN()
|
||||
}
|
||||
|
||||
val currentChapter = getCurrentChapter(index)
|
||||
val currentChapterProgress = getCurrentChapterProgress(currentChapter)
|
||||
|
||||
return currentChapter to currentChapterProgress
|
||||
}
|
||||
}
|
||||
|
|
@ -400,6 +400,7 @@ class BookInfoModel @Inject constructor(
|
|||
job.cancel()
|
||||
job.join()
|
||||
}
|
||||
eventStack.clear()
|
||||
_state.update { BookInfoState() }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.presentation.reader
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class ReaderEffect {
|
||||
data class OnSystemBarsVisibility(
|
||||
val show: Boolean?
|
||||
) : ReaderEffect()
|
||||
|
||||
data object OnResetBrightness : ReaderEffect()
|
||||
|
||||
data class OnScroll(
|
||||
val scrollIndex: Int,
|
||||
val scrollOffset: Int
|
||||
) : ReaderEffect()
|
||||
|
||||
data class OnOpenTranslator(
|
||||
val textToTranslate: String,
|
||||
val translateWholeParagraph: Boolean
|
||||
) : ReaderEffect()
|
||||
|
||||
data class OnOpenShareApp(
|
||||
val textToShare: String
|
||||
) : ReaderEffect()
|
||||
|
||||
data class OnOpenWebBrowser(
|
||||
val textToSearch: String
|
||||
) : ReaderEffect()
|
||||
|
||||
data class OnOpenDictionary(
|
||||
val textToDefine: String
|
||||
) : ReaderEffect()
|
||||
|
||||
data object OnNavigateBack : ReaderEffect()
|
||||
|
||||
data class OnNavigateToBookInfo(
|
||||
val changePath: Boolean
|
||||
) : ReaderEffect()
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
package ua.acclorite.book_story.presentation.reader
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText.Chapter
|
||||
import ua.acclorite.book_story.presentation.reader.model.Checkpoint
|
||||
|
|
@ -14,16 +13,13 @@ import ua.acclorite.book_story.presentation.reader.model.Checkpoint
|
|||
@Immutable
|
||||
sealed class ReaderEvent {
|
||||
|
||||
data class OnLoadText(
|
||||
val activity: ComponentActivity,
|
||||
val fullscreenMode: Boolean
|
||||
) : ReaderEvent()
|
||||
data object OnLoadText : ReaderEvent()
|
||||
|
||||
data object OnRestoreScroll : ReaderEvent()
|
||||
|
||||
data class OnMenuVisibility(
|
||||
val show: Boolean,
|
||||
val fullscreenMode: Boolean,
|
||||
val saveCheckpoint: Boolean,
|
||||
val activity: ComponentActivity
|
||||
val saveCheckpoint: Boolean
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnChangeProgress(
|
||||
|
|
@ -32,6 +28,10 @@ sealed class ReaderEvent {
|
|||
val firstVisibleItemOffset: Int
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnUpdateChapter(
|
||||
val index: Int
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnScrollToChapter(
|
||||
val chapter: Chapter
|
||||
) : ReaderEvent()
|
||||
|
|
@ -45,29 +45,24 @@ sealed class ReaderEvent {
|
|||
) : ReaderEvent()
|
||||
|
||||
data class OnLeave(
|
||||
val activity: ComponentActivity,
|
||||
val navigate: () -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnOpenTranslator(
|
||||
val textToTranslate: String,
|
||||
val translateWholeParagraph: Boolean,
|
||||
val activity: ComponentActivity
|
||||
val translateWholeParagraph: Boolean
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnOpenShareApp(
|
||||
val textToShare: String,
|
||||
val activity: ComponentActivity
|
||||
val textToShare: String
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnOpenWebBrowser(
|
||||
val textToSearch: String,
|
||||
val activity: ComponentActivity
|
||||
val textToSearch: String
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnOpenDictionary(
|
||||
val textToDefine: String,
|
||||
val activity: ComponentActivity
|
||||
val textToDefine: String
|
||||
) : ReaderEvent()
|
||||
|
||||
data object OnShowSettingsBottomSheet : ReaderEvent()
|
||||
|
|
@ -77,4 +72,10 @@ sealed class ReaderEvent {
|
|||
data object OnShowChaptersDrawer : ReaderEvent()
|
||||
|
||||
data object OnDismissDrawer : ReaderEvent()
|
||||
|
||||
data object OnNavigateBack : ReaderEvent()
|
||||
|
||||
data class OnNavigateToBookInfo(
|
||||
val changePath: Boolean
|
||||
) : ReaderEvent()
|
||||
}
|
||||
|
|
@ -6,59 +6,51 @@
|
|||
|
||||
package ua.acclorite.book_story.presentation.reader
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.core.helpers.coerceAndPreventNaN
|
||||
import ua.acclorite.book_story.core.ui.UIText
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText.Chapter
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetChapterProgressUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetTextUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.UpdateBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.history.GetHistoryForBookUseCase
|
||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
||||
import ua.acclorite.book_story.presentation.reader.model.Checkpoint
|
||||
import ua.acclorite.book_story.ui.common.helpers.launchActivity
|
||||
import ua.acclorite.book_story.ui.common.helpers.setBrightness
|
||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val READER = "READER, MODEL"
|
||||
|
||||
@HiltViewModel
|
||||
class ReaderModel @Inject constructor(
|
||||
private val updateBookUseCase: UpdateBookUseCase,
|
||||
private val getTextUseCase: GetTextUseCase,
|
||||
private val getBookUseCase: GetBookUseCase,
|
||||
private val getHistoryForBookUseCase: GetHistoryForBookUseCase
|
||||
private val getHistoryForBookUseCase: GetHistoryForBookUseCase,
|
||||
private val getChapterProgressUseCase: GetChapterProgressUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
|
@ -66,38 +58,36 @@ class ReaderModel @Inject constructor(
|
|||
private val _state = MutableStateFlow(ReaderState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var eventJob = SupervisorJob()
|
||||
private var resetJob: Job? = null
|
||||
private val _effects = MutableSharedFlow<ReaderEffect>()
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
private var scrollJob: Job? = null
|
||||
private val eventStack = mutableListOf<Job>()
|
||||
|
||||
fun onEvent(event: ReaderEvent) {
|
||||
viewModelScope.launch(eventJob + Dispatchers.Main) {
|
||||
viewModelScope.launch {
|
||||
when (event) {
|
||||
is ReaderEvent.OnLoadText -> {
|
||||
launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val text = getTextUseCase(_state.value.book.id)
|
||||
yield()
|
||||
ensureActive()
|
||||
|
||||
if (text.isEmpty()) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = UIText.StringResource(R.string.error_could_not_get_text)
|
||||
errorMessage = UIText.StringResource(
|
||||
resId = R.string.error_could_not_get_text
|
||||
)
|
||||
)
|
||||
}
|
||||
systemBarsVisibility(show = true, activity = event.activity)
|
||||
return@launch
|
||||
_effects.emit(ReaderEffect.OnSystemBarsVisibility(show = true))
|
||||
return@withContext
|
||||
}
|
||||
|
||||
systemBarsVisibility(
|
||||
show = !event.fullscreenMode,
|
||||
activity = event.activity
|
||||
)
|
||||
_effects.emit(ReaderEffect.OnSystemBarsVisibility(show = null))
|
||||
|
||||
val lastOpened = getHistoryForBookUseCase(_state.value.book.id)?.time
|
||||
yield()
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
showMenu = false,
|
||||
|
|
@ -107,54 +97,54 @@ class ReaderModel @Inject constructor(
|
|||
text = text
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
ensureActive()
|
||||
|
||||
updateBookUseCase(_state.value.book)
|
||||
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
launch {
|
||||
snapshotFlow {
|
||||
_state.value.listState.layoutInfo.totalItemsCount
|
||||
}.collectLatest { itemsCount ->
|
||||
if (itemsCount < _state.value.text.size) return@collectLatest
|
||||
onEvent(ReaderEvent.OnRestoreScroll)
|
||||
}
|
||||
}
|
||||
|
||||
_state.value.book.apply {
|
||||
_state.value.listState.requestScrollToItem(
|
||||
scrollIndex,
|
||||
scrollOffset
|
||||
)
|
||||
updateChapter(index = scrollIndex)
|
||||
}
|
||||
is ReaderEvent.OnRestoreScroll -> {
|
||||
snapshotFlow { _state.value.listState.layoutInfo.totalItemsCount }.first { it > 0 }
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = null
|
||||
)
|
||||
}
|
||||
_effects.emit(
|
||||
ReaderEffect.OnScroll(
|
||||
scrollIndex = _state.value.book.scrollIndex,
|
||||
scrollOffset = _state.value.book.scrollOffset
|
||||
)
|
||||
)
|
||||
|
||||
return@collectLatest
|
||||
}
|
||||
}
|
||||
_state.update {
|
||||
val (currentChapter, currentChapterProgress) = getChapterProgressUseCase(
|
||||
it.book.scrollIndex,
|
||||
it.text
|
||||
)
|
||||
it.copy(
|
||||
currentChapter = currentChapter,
|
||||
currentChapterProgress = currentChapterProgress,
|
||||
isLoading = false,
|
||||
errorMessage = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnMenuVisibility -> {
|
||||
launch(Dispatchers.Default) {
|
||||
if (_state.value.lockMenu) return@launch
|
||||
withContext(Dispatchers.Default) {
|
||||
if (_state.value.lockMenu) return@withContext
|
||||
|
||||
yield()
|
||||
|
||||
systemBarsVisibility(
|
||||
show = event.show || !event.fullscreenMode,
|
||||
activity = event.activity
|
||||
_effects.emit(
|
||||
ReaderEffect.OnSystemBarsVisibility(
|
||||
show = if (event.show) true
|
||||
else null
|
||||
)
|
||||
)
|
||||
|
||||
val checkpoints = _state.value.checkpoints.toMutableList()
|
||||
if (event.saveCheckpoint && event.show) {
|
||||
val checkpoints = _state.value.checkpoints.toMutableList()
|
||||
checkpoints.removeIf {
|
||||
it.index == _state.value.listState.firstVisibleItemIndex
|
||||
}
|
||||
|
|
@ -164,19 +154,20 @@ class ReaderModel @Inject constructor(
|
|||
_state.value.listState.firstVisibleItemScrollOffset
|
||||
)
|
||||
)
|
||||
|
||||
_state.update {
|
||||
it.copy(checkpoints = checkpoints)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
showMenu = event.show,
|
||||
checkpoints = checkpoints
|
||||
)
|
||||
it.copy(showMenu = event.show)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnChangeProgress -> {
|
||||
launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -194,44 +185,64 @@ class ReaderModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnScrollToChapter -> {
|
||||
launch(Dispatchers.Default) {
|
||||
_state.value.apply {
|
||||
val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
|
||||
if (chapterIndex == null) {
|
||||
return@launch
|
||||
}
|
||||
is ReaderEvent.OnUpdateChapter -> {
|
||||
_state.update {
|
||||
val (currentChapter, currentChapterProgress) = getChapterProgressUseCase(
|
||||
event.index,
|
||||
_state.value.text
|
||||
)
|
||||
it.copy(
|
||||
currentChapter = currentChapter,
|
||||
currentChapterProgress = currentChapterProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
listState.requestScrollToItem(chapterIndex)
|
||||
updateChapter(index = chapterIndex)
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = calculateProgress(chapterIndex),
|
||||
firstVisibleItemIndex = chapterIndex,
|
||||
firstVisibleItemOffset = 0
|
||||
)
|
||||
is ReaderEvent.OnScrollToChapter -> {
|
||||
withContext(Dispatchers.Default) {
|
||||
val chapterIndex = _state.value.text
|
||||
.indexOf(event.chapter)
|
||||
.takeIf { it != -1 }
|
||||
if (chapterIndex == null) return@withContext
|
||||
|
||||
_effects.emit(
|
||||
ReaderEffect.OnScroll(
|
||||
scrollIndex = chapterIndex,
|
||||
scrollOffset = 0
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
onEvent(ReaderEvent.OnUpdateChapter(chapterIndex))
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = calculateProgress(chapterIndex),
|
||||
firstVisibleItemIndex = chapterIndex,
|
||||
firstVisibleItemOffset = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnScroll -> {
|
||||
scrollJob?.cancel()
|
||||
scrollJob = launch(Dispatchers.Main) {
|
||||
scrollJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(300)
|
||||
|
||||
val scrollTo = (_state.value.text.lastIndex * event.progress).roundToInt()
|
||||
updateChapter(scrollTo)
|
||||
try {
|
||||
_state.value.listState.requestScrollToItem(scrollTo)
|
||||
} catch (_: Exception) {
|
||||
|
||||
}
|
||||
_effects.emit(
|
||||
ReaderEffect.OnScroll(
|
||||
scrollIndex = scrollTo,
|
||||
scrollOffset = 0
|
||||
)
|
||||
)
|
||||
|
||||
onEvent(ReaderEvent.OnUpdateChapter(scrollTo))
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnRestoreCheckpoint -> {
|
||||
launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
val checkpoints = it.checkpoints.toMutableList()
|
||||
if (checkpoints.size > 1) checkpoints.remove(event.checkpoint)
|
||||
|
|
@ -241,16 +252,14 @@ class ReaderModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
try {
|
||||
_state.value.listState.requestScrollToItem(
|
||||
event.checkpoint.index,
|
||||
event.checkpoint.offset
|
||||
_effects.emit(
|
||||
ReaderEffect.OnScroll(
|
||||
scrollIndex = event.checkpoint.index,
|
||||
scrollOffset = event.checkpoint.offset
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
updateChapter(event.checkpoint.index)
|
||||
onEvent(ReaderEvent.OnUpdateChapter(event.checkpoint.index))
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = calculateProgress(event.checkpoint.index),
|
||||
|
|
@ -262,186 +271,74 @@ class ReaderModel @Inject constructor(
|
|||
}
|
||||
|
||||
is ReaderEvent.OnLeave -> {
|
||||
launch(Dispatchers.Main) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
lockMenu = true
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!_state.value.isLoading &&
|
||||
_state.value.listState.layoutInfo.totalItemsCount > 0 &&
|
||||
_state.value.text.isNotEmpty() &&
|
||||
_state.value.errorMessage != null
|
||||
) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
lockMenu = true
|
||||
book = it.book.copy(
|
||||
progress = calculateProgress(),
|
||||
scrollIndex = _state.value.listState.firstVisibleItemIndex,
|
||||
scrollOffset = _state.value.listState.firstVisibleItemScrollOffset
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
_state.value.listState.apply {
|
||||
if (
|
||||
_state.value.isLoading ||
|
||||
layoutInfo.totalItemsCount < 1 ||
|
||||
_state.value.text.isEmpty() ||
|
||||
_state.value.errorMessage != null
|
||||
) return@apply
|
||||
updateBookUseCase(_state.value.book)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
progress = calculateProgress(),
|
||||
scrollIndex = firstVisibleItemIndex,
|
||||
scrollOffset = firstVisibleItemScrollOffset
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
updateBookUseCase(_state.value.book)
|
||||
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
}
|
||||
|
||||
WindowCompat.getInsetsController(
|
||||
event.activity.window,
|
||||
event.activity.window.decorView
|
||||
).show(WindowInsetsCompat.Type.systemBars())
|
||||
event.activity.setBrightness(brightness = null)
|
||||
|
||||
event.navigate()
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
}
|
||||
|
||||
_effects.emit(
|
||||
ReaderEffect.OnSystemBarsVisibility(
|
||||
show = true
|
||||
)
|
||||
)
|
||||
_effects.emit(ReaderEffect.OnResetBrightness)
|
||||
event.navigate()
|
||||
}
|
||||
|
||||
is ReaderEvent.OnOpenTranslator -> {
|
||||
launch(Dispatchers.Default) {
|
||||
val translatorIntent = Intent()
|
||||
val browserIntent = Intent()
|
||||
|
||||
translatorIntent.type = "text/plain"
|
||||
translatorIntent.action = Intent.ACTION_PROCESS_TEXT
|
||||
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
||||
|
||||
translatorIntent.putExtra(
|
||||
Intent.EXTRA_PROCESS_TEXT,
|
||||
event.textToTranslate
|
||||
_effects.emit(
|
||||
ReaderEffect.OnOpenTranslator(
|
||||
textToTranslate = event.textToTranslate,
|
||||
translateWholeParagraph = event.translateWholeParagraph
|
||||
)
|
||||
translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
browserIntent.putExtra(
|
||||
SearchManager.QUERY,
|
||||
"translate: ${event.textToTranslate.trim()}"
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
translatorIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = !event.translateWholeParagraph,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
browserIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.activity.getString(R.string.error_no_translator)
|
||||
.showToast(context = event.activity, longToast = false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderEvent.OnOpenShareApp -> {
|
||||
launch(Dispatchers.Default) {
|
||||
val shareIntent = Intent()
|
||||
|
||||
shareIntent.action = Intent.ACTION_SEND
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_SUBJECT,
|
||||
event.activity.getString(R.string.app_name)
|
||||
_effects.emit(
|
||||
ReaderEffect.OnOpenShareApp(
|
||||
textToShare = event.textToShare
|
||||
)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
event.textToShare.trim()
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
shareIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = true,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.activity.getString(R.string.error_no_share_app)
|
||||
.showToast(context = event.activity, longToast = false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderEvent.OnOpenWebBrowser -> {
|
||||
launch(Dispatchers.Default) {
|
||||
val browserIntent = Intent()
|
||||
|
||||
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
||||
browserIntent.putExtra(
|
||||
SearchManager.QUERY,
|
||||
event.textToSearch
|
||||
_effects.emit(
|
||||
ReaderEffect.OnOpenWebBrowser(
|
||||
textToSearch = event.textToSearch
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
browserIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.activity.getString(R.string.error_no_browser)
|
||||
.showToast(context = event.activity, longToast = false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderEvent.OnOpenDictionary -> {
|
||||
launch(Dispatchers.Default) {
|
||||
val dictionaryIntent = Intent()
|
||||
val browserIntent = Intent()
|
||||
|
||||
dictionaryIntent.type = "text/plain"
|
||||
dictionaryIntent.action = Intent.ACTION_PROCESS_TEXT
|
||||
dictionaryIntent.putExtra(
|
||||
Intent.EXTRA_PROCESS_TEXT,
|
||||
event.textToDefine.trim()
|
||||
_effects.emit(
|
||||
ReaderEffect.OnOpenDictionary(
|
||||
textToDefine = event.textToDefine
|
||||
)
|
||||
dictionaryIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
|
||||
browserIntent.action = Intent.ACTION_VIEW
|
||||
val text = event.textToDefine.trim().replace(" ", "+")
|
||||
browserIntent.data = "https://www.onelook.com/?w=$text".toUri()
|
||||
|
||||
yield()
|
||||
|
||||
dictionaryIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = true,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
|
||||
browserIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
success = {
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.activity.getString(R.string.error_no_dictionary)
|
||||
.showToast(context = event.activity, longToast = false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderEvent.OnShowSettingsBottomSheet -> {
|
||||
|
|
@ -477,43 +374,57 @@ class ReaderModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnNavigateBack -> {
|
||||
_effects.emit(ReaderEffect.OnNavigateBack)
|
||||
}
|
||||
|
||||
is ReaderEvent.OnNavigateToBookInfo -> {
|
||||
_effects.emit(ReaderEffect.OnNavigateToBookInfo(event.changePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also { eventStack.add(it) }
|
||||
}
|
||||
|
||||
fun init(
|
||||
bookId: Int,
|
||||
fullscreenMode: Boolean,
|
||||
activity: ComponentActivity,
|
||||
navigateBack: () -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun init(bookId: Int) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val book = getBookUseCase(bookId)
|
||||
|
||||
if (book == null) {
|
||||
navigateBack()
|
||||
_effects.emit(ReaderEffect.OnNavigateBack)
|
||||
return@launch
|
||||
}
|
||||
|
||||
eventJob.cancel()
|
||||
resetJob?.cancel()
|
||||
eventJob.join()
|
||||
resetJob?.join()
|
||||
eventJob = SupervisorJob()
|
||||
clear()
|
||||
|
||||
_state.update {
|
||||
ReaderState(book = book)
|
||||
ReaderState(
|
||||
book = book
|
||||
)
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ReaderEvent.OnLoadText(
|
||||
activity = activity,
|
||||
fullscreenMode = fullscreenMode
|
||||
)
|
||||
)
|
||||
onEvent(ReaderEvent.OnLoadText)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAsync() {
|
||||
viewModelScope.launch {
|
||||
eventStack.forEach { job ->
|
||||
job.cancel()
|
||||
}
|
||||
_state.update { ReaderState() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
eventStack.forEach { job ->
|
||||
job.cancel()
|
||||
job.join()
|
||||
}
|
||||
eventStack.clear()
|
||||
_state.update { ReaderState() }
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
suspend fun updateProgress(listState: LazyListState) {
|
||||
snapshotFlow {
|
||||
|
|
@ -527,12 +438,11 @@ class ReaderModel @Inject constructor(
|
|||
) return@collectLatest
|
||||
|
||||
val progress = calculateProgress(index)
|
||||
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
||||
|
||||
Log.i(
|
||||
READER,
|
||||
"Changed progress|currentChapter: $progress; ${currentChapter?.title}"
|
||||
val (currentChapter, currentChapterProgress) = getChapterProgressUseCase(
|
||||
index = index,
|
||||
text = _state.value.text
|
||||
)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -553,126 +463,42 @@ class ReaderModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun findChapterIndexAndLength(index: Int): Pair<Int, Int> {
|
||||
return findCurrentChapter(index)?.let { chapter ->
|
||||
_state.value.text.run {
|
||||
val startIndex = indexOf(chapter).coerceIn(0, lastIndex)
|
||||
val endIndex = (indexOfFirst {
|
||||
it is Chapter && indexOf(it) > startIndex
|
||||
}.takeIf { it != -1 }) ?: (lastIndex + 1)
|
||||
val (chapter, _) = getChapterProgressUseCase(index = index, text = _state.value.text)
|
||||
return chapter?.let { chapter ->
|
||||
val startIndex = _state.value.text
|
||||
.indexOf(chapter)
|
||||
.coerceIn(0, _state.value.text.lastIndex)
|
||||
val endIndex = (_state.value.text.indexOfFirst {
|
||||
it is Chapter && _state.value.text.indexOf(it) > startIndex
|
||||
}.takeIf { it != -1 }) ?: (_state.value.text.lastIndex + 1)
|
||||
|
||||
val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
|
||||
val chapterLength = endIndex - (startIndex + 1)
|
||||
currentIndexInChapter to chapterLength
|
||||
}
|
||||
val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
|
||||
val chapterLength = endIndex - (startIndex + 1)
|
||||
currentIndexInChapter to chapterLength
|
||||
} ?: (-1 to -1)
|
||||
}
|
||||
|
||||
private fun updateChapter(index: Int) {
|
||||
viewModelScope.launch {
|
||||
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
||||
_state.update {
|
||||
Log.i(
|
||||
READER,
|
||||
"Changed currentChapter|currentChapterProgress:" +
|
||||
" ${currentChapter?.title}($currentChapterProgress)"
|
||||
)
|
||||
it.copy(
|
||||
currentChapter = currentChapter,
|
||||
currentChapterProgress = currentChapterProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> {
|
||||
val currentChapter = findCurrentChapter(index)
|
||||
val currentChapterProgress = currentChapter?.let { chapter ->
|
||||
_state.value.text.run {
|
||||
val startIndex = indexOf(chapter).coerceIn(0, lastIndex)
|
||||
val endIndex = (indexOfFirst {
|
||||
it is Chapter && indexOf(it) > startIndex
|
||||
}.takeIf { it != -1 }) ?: (lastIndex + 1)
|
||||
|
||||
val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
|
||||
val chapterLength = endIndex - (startIndex + 1)
|
||||
(currentIndexInChapter / chapterLength.toFloat())
|
||||
}
|
||||
}.coerceAndPreventNaN()
|
||||
|
||||
return currentChapter to currentChapterProgress
|
||||
}
|
||||
|
||||
private fun findCurrentChapter(index: Int): Chapter? {
|
||||
return try {
|
||||
for (textIndex in index downTo 0) {
|
||||
val readerText = _state.value.text.getOrNull(textIndex) ?: break
|
||||
if (readerText is Chapter) {
|
||||
return readerText
|
||||
}
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateProgress(firstVisibleItemIndex: Int? = null): Float {
|
||||
return _state.value.run {
|
||||
if (
|
||||
isLoading ||
|
||||
listState.layoutInfo.totalItemsCount == 0 ||
|
||||
text.isEmpty() ||
|
||||
errorMessage != null
|
||||
) {
|
||||
return book.progress
|
||||
}
|
||||
if (
|
||||
_state.value.isLoading ||
|
||||
_state.value.listState.layoutInfo.totalItemsCount == 0 ||
|
||||
_state.value.text.isEmpty() ||
|
||||
_state.value.errorMessage != null
|
||||
) return _state.value.book.progress
|
||||
|
||||
if ((firstVisibleItemIndex ?: listState.firstVisibleItemIndex) == 0) {
|
||||
return 0f
|
||||
}
|
||||
if ((firstVisibleItemIndex ?: _state.value.listState.firstVisibleItemIndex) == 0) return 0f
|
||||
|
||||
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
|
||||
if (lastVisibleItemIndex >= text.lastIndex) {
|
||||
return 1f
|
||||
}
|
||||
val lastVisibleItemIndex = _state.value.listState.layoutInfo.visibleItemsInfo.last().index
|
||||
if (lastVisibleItemIndex >= _state.value.text.lastIndex) return 1f
|
||||
|
||||
return@run (firstVisibleItemIndex ?: listState.firstVisibleItemIndex)
|
||||
.div(text.lastIndex.toFloat())
|
||||
.coerceAndPreventNaN()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun systemBarsVisibility(
|
||||
show: Boolean,
|
||||
activity: ComponentActivity
|
||||
) {
|
||||
withContext(Dispatchers.Main) {
|
||||
WindowCompat.getInsetsController(
|
||||
activity.window,
|
||||
activity.window.decorView
|
||||
).apply {
|
||||
systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
if (show) show(WindowInsetsCompat.Type.systemBars())
|
||||
else hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
resetJob = viewModelScope.launch(Dispatchers.Main) {
|
||||
eventJob.cancel()
|
||||
eventJob = SupervisorJob()
|
||||
|
||||
yield()
|
||||
_state.update { ReaderState() }
|
||||
}
|
||||
return (firstVisibleItemIndex ?: _state.value.listState.firstVisibleItemIndex)
|
||||
.div(_state.value.text.lastIndex.toFloat())
|
||||
.coerceAndPreventNaN()
|
||||
}
|
||||
|
||||
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||
mutex.withLock {
|
||||
yield()
|
||||
coroutineContext.ensureActive()
|
||||
this.value = function(this.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.core.helpers.calculateProgress
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoScreen
|
||||
import ua.acclorite.book_story.presentation.navigator.Screen
|
||||
import ua.acclorite.book_story.presentation.reader.model.ReaderColorEffects
|
||||
import ua.acclorite.book_story.presentation.reader.model.ReaderProgressCount
|
||||
|
|
@ -54,8 +53,8 @@ import ua.acclorite.book_story.presentation.settings.SettingsModel
|
|||
import ua.acclorite.book_story.ui.common.helpers.LocalActivity
|
||||
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
||||
import ua.acclorite.book_story.ui.common.helpers.setBrightness
|
||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||
import ua.acclorite.book_story.ui.reader.ReaderContent
|
||||
import ua.acclorite.book_story.ui.reader.ReaderEffects
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -69,7 +68,6 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.current
|
||||
val screenModel = hiltViewModel<ReaderModel>()
|
||||
val settingsModel = hiltViewModel<SettingsModel>()
|
||||
val settings = LocalSettings.current
|
||||
|
|
@ -102,9 +100,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
screenModel.onEvent(
|
||||
ReaderEvent.OnMenuVisibility(
|
||||
show = false,
|
||||
fullscreenMode = settings.fullscreen.lastValue,
|
||||
saveCheckpoint = false,
|
||||
activity = activity
|
||||
saveCheckpoint = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -328,22 +324,13 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.init(
|
||||
bookId = bookId,
|
||||
fullscreenMode = settings.fullscreen.lastValue,
|
||||
activity = activity,
|
||||
navigateBack = {
|
||||
navigator.pop()
|
||||
}
|
||||
)
|
||||
screenModel.init(bookId = bookId)
|
||||
}
|
||||
LaunchedEffect(settings.fullscreen.value) {
|
||||
screenModel.onEvent(
|
||||
ReaderEvent.OnMenuVisibility(
|
||||
show = state.value.showMenu,
|
||||
fullscreenMode = settings.fullscreen.lastValue,
|
||||
saveCheckpoint = false,
|
||||
activity = activity
|
||||
saveCheckpoint = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -382,7 +369,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
screenModel.resetScreen()
|
||||
screenModel.clearAsync()
|
||||
WindowCompat.getInsetsController(
|
||||
activity.window,
|
||||
activity.window.decorView
|
||||
|
|
@ -390,6 +377,13 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
ReaderEffects(
|
||||
effects = screenModel.effects,
|
||||
book = state.value.book,
|
||||
listState = listState,
|
||||
fullscreen = settings.fullscreen.value
|
||||
)
|
||||
|
||||
ReaderContent(
|
||||
book = state.value.book,
|
||||
text = state.value.text,
|
||||
|
|
@ -444,7 +438,6 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
letterSpacing = letterSpacing,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
doubleClickTranslation = settings.doubleClickTranslation.value,
|
||||
fullscreenMode = settings.fullscreen.value,
|
||||
selectPreviousPreset = settingsModel::onEvent,
|
||||
selectNextPreset = settingsModel::onEvent,
|
||||
leave = screenModel::onEvent,
|
||||
|
|
@ -461,19 +454,8 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
dismissBottomSheet = screenModel::onEvent,
|
||||
showChaptersDrawer = screenModel::onEvent,
|
||||
dismissDrawer = screenModel::onEvent,
|
||||
navigateBack = {
|
||||
navigator.pop()
|
||||
},
|
||||
navigateToBookInfo = { changePath ->
|
||||
if (changePath) BookInfoScreen.changePathChannel.trySend(true)
|
||||
navigator.push(
|
||||
BookInfoScreen(
|
||||
bookId = bookId,
|
||||
),
|
||||
popping = true,
|
||||
saveInBackStack = false
|
||||
)
|
||||
}
|
||||
navigateBack = screenModel::onEvent,
|
||||
navigateToBookInfo = screenModel::onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,21 +9,17 @@ package ua.acclorite.book_story.ui.reader
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import ua.acclorite.book_story.presentation.reader.ReaderEvent
|
||||
import ua.acclorite.book_story.ui.common.helpers.LocalActivity
|
||||
|
||||
@Composable
|
||||
fun ReaderBackHandler(
|
||||
leave: (ReaderEvent.OnLeave) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateBack: (ReaderEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
BackHandler {
|
||||
leave(
|
||||
ReaderEvent.OnLeave(
|
||||
activity = activity,
|
||||
navigate = {
|
||||
navigateBack()
|
||||
navigateBack(ReaderEvent.OnNavigateBack)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ import ua.acclorite.book_story.presentation.reader.ReaderScreen
|
|||
@Composable
|
||||
fun ReaderBottomSheet(
|
||||
bottomSheet: BottomSheet?,
|
||||
fullscreenMode: Boolean,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
|
||||
dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit
|
||||
) {
|
||||
when (bottomSheet) {
|
||||
ReaderScreen.SETTINGS_BOTTOM_SHEET -> {
|
||||
ReaderSettingsBottomSheet(
|
||||
fullscreenMode = fullscreenMode,
|
||||
menuVisibility = menuVisibility,
|
||||
dismissBottomSheet = dismissBottomSheet
|
||||
)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ fun ReaderContent(
|
|||
letterSpacing: TextUnit,
|
||||
paragraphIndentation: TextUnit,
|
||||
doubleClickTranslation: Boolean,
|
||||
fullscreenMode: Boolean,
|
||||
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
|
||||
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
|
||||
|
|
@ -105,12 +104,11 @@ fun ReaderContent(
|
|||
dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit,
|
||||
showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit,
|
||||
dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit,
|
||||
navigateToBookInfo: (changePath: Boolean) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToBookInfo: (ReaderEvent.OnNavigateToBookInfo) -> Unit,
|
||||
navigateBack: (ReaderEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
ReaderBottomSheet(
|
||||
bottomSheet = bottomSheet,
|
||||
fullscreenMode = fullscreenMode,
|
||||
menuVisibility = menuVisibility,
|
||||
dismissBottomSheet = dismissBottomSheet
|
||||
)
|
||||
|
|
@ -167,7 +165,6 @@ fun ReaderContent(
|
|||
letterSpacing = letterSpacing,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
doubleClickTranslation = doubleClickTranslation,
|
||||
fullscreenMode = fullscreenMode,
|
||||
selectPreviousPreset = selectPreviousPreset,
|
||||
selectNextPreset = selectNextPreset,
|
||||
menuVisibility = menuVisibility,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.ui.reader
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoScreen
|
||||
import ua.acclorite.book_story.presentation.reader.ReaderEffect
|
||||
import ua.acclorite.book_story.ui.common.helpers.LocalActivity
|
||||
import ua.acclorite.book_story.ui.common.helpers.launchActivity
|
||||
import ua.acclorite.book_story.ui.common.helpers.setBrightness
|
||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||
|
||||
@Composable
|
||||
fun ReaderEffects(
|
||||
effects: SharedFlow<ReaderEffect>,
|
||||
book: Book,
|
||||
listState: LazyListState,
|
||||
fullscreen: Boolean
|
||||
) {
|
||||
val navigator = LocalNavigator.current
|
||||
val activity = LocalActivity.current
|
||||
|
||||
LaunchedEffect(effects, book, listState, fullscreen) {
|
||||
effects.collect { effect ->
|
||||
when (effect) {
|
||||
is ReaderEffect.OnSystemBarsVisibility -> {
|
||||
WindowCompat.getInsetsController(
|
||||
activity.window,
|
||||
activity.window.decorView
|
||||
).apply {
|
||||
systemBarsBehavior = WindowInsetsControllerCompat
|
||||
.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
if (effect.show ?: !fullscreen) show(WindowInsetsCompat.Type.systemBars())
|
||||
else hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEffect.OnResetBrightness -> {
|
||||
activity.setBrightness(brightness = null)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnScroll -> {
|
||||
listState.requestScrollToItem(
|
||||
effect.scrollIndex,
|
||||
effect.scrollOffset
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnOpenTranslator -> {
|
||||
val translatorIntent = Intent()
|
||||
val browserIntent = Intent()
|
||||
|
||||
translatorIntent.type = "text/plain"
|
||||
translatorIntent.action = Intent.ACTION_PROCESS_TEXT
|
||||
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
||||
|
||||
translatorIntent.putExtra(
|
||||
Intent.EXTRA_PROCESS_TEXT,
|
||||
effect.textToTranslate
|
||||
)
|
||||
translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
browserIntent.putExtra(
|
||||
SearchManager.QUERY,
|
||||
"translate: ${effect.textToTranslate.trim()}"
|
||||
)
|
||||
|
||||
translatorIntent.launchActivity(
|
||||
activity = activity,
|
||||
createChooser = !effect.translateWholeParagraph,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
browserIntent.launchActivity(
|
||||
activity = activity,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
|
||||
activity.getString(R.string.error_no_translator)
|
||||
.showToast(context = activity, longToast = false)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnOpenShareApp -> {
|
||||
val shareIntent = Intent()
|
||||
|
||||
shareIntent.action = Intent.ACTION_SEND
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_SUBJECT,
|
||||
activity.getString(R.string.app_name)
|
||||
)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
effect.textToShare.trim()
|
||||
)
|
||||
|
||||
shareIntent.launchActivity(
|
||||
activity = activity,
|
||||
createChooser = true,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
|
||||
activity.getString(R.string.error_no_share_app)
|
||||
.showToast(context = activity, longToast = false)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnOpenWebBrowser -> {
|
||||
val browserIntent = Intent()
|
||||
|
||||
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
||||
browserIntent.putExtra(
|
||||
SearchManager.QUERY,
|
||||
effect.textToSearch
|
||||
)
|
||||
|
||||
browserIntent.launchActivity(
|
||||
activity = activity,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
|
||||
activity.getString(R.string.error_no_browser)
|
||||
.showToast(context = activity, longToast = false)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnOpenDictionary -> {
|
||||
val dictionaryIntent = Intent()
|
||||
val browserIntent = Intent()
|
||||
|
||||
dictionaryIntent.type = "text/plain"
|
||||
dictionaryIntent.action = Intent.ACTION_PROCESS_TEXT
|
||||
dictionaryIntent.putExtra(
|
||||
Intent.EXTRA_PROCESS_TEXT,
|
||||
effect.textToDefine.trim()
|
||||
)
|
||||
dictionaryIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
|
||||
browserIntent.action = Intent.ACTION_VIEW
|
||||
val text = effect.textToDefine.trim().replace(" ", "+")
|
||||
browserIntent.data = "https://www.onelook.com/?w=$text".toUri()
|
||||
|
||||
dictionaryIntent.launchActivity(
|
||||
activity = activity,
|
||||
createChooser = true,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
browserIntent.launchActivity(
|
||||
activity = activity,
|
||||
success = {
|
||||
return@collect
|
||||
}
|
||||
)
|
||||
|
||||
activity.getString(R.string.error_no_dictionary)
|
||||
.showToast(context = activity, longToast = false)
|
||||
}
|
||||
|
||||
is ReaderEffect.OnNavigateBack -> {
|
||||
navigator.pop()
|
||||
}
|
||||
|
||||
is ReaderEffect.OnNavigateToBookInfo -> {
|
||||
if (effect.changePath) BookInfoScreen.changePathChannel.trySend(true)
|
||||
navigator.push(
|
||||
BookInfoScreen(
|
||||
bookId = book.id
|
||||
),
|
||||
popping = true,
|
||||
saveInBackStack = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ import ua.acclorite.book_story.presentation.reader.ReaderEvent
|
|||
import ua.acclorite.book_story.ui.common.components.placeholder.ErrorPlaceholder
|
||||
import ua.acclorite.book_story.ui.common.components.top_bar.TopAppBar
|
||||
import ua.acclorite.book_story.ui.common.components.top_bar.TopAppBarData
|
||||
import ua.acclorite.book_story.ui.common.helpers.LocalActivity
|
||||
import ua.acclorite.book_story.ui.navigator.NavigatorBackIconButton
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -33,10 +32,9 @@ import ua.acclorite.book_story.ui.navigator.NavigatorBackIconButton
|
|||
fun ReaderErrorPlaceholder(
|
||||
errorMessage: UIText,
|
||||
leave: (ReaderEvent.OnLeave) -> Unit,
|
||||
navigateToBookInfo: (changePath: Boolean) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToBookInfo: (ReaderEvent.OnNavigateToBookInfo) -> Unit,
|
||||
navigateBack: (ReaderEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
|
|
@ -52,7 +50,7 @@ fun ReaderErrorPlaceholder(
|
|||
contentID = 0,
|
||||
contentNavigationIcon = {
|
||||
NavigatorBackIconButton {
|
||||
navigateBack()
|
||||
navigateBack(ReaderEvent.OnNavigateBack)
|
||||
}
|
||||
},
|
||||
contentTitle = {},
|
||||
|
|
@ -73,9 +71,12 @@ fun ReaderErrorPlaceholder(
|
|||
action = {
|
||||
leave(
|
||||
ReaderEvent.OnLeave(
|
||||
activity = activity,
|
||||
navigate = {
|
||||
navigateToBookInfo(true)
|
||||
navigateToBookInfo(
|
||||
ReaderEvent.OnNavigateToBookInfo(
|
||||
changePath = true
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ fun ReaderLayout(
|
|||
letterSpacing: TextUnit,
|
||||
paragraphIndentation: TextUnit,
|
||||
doubleClickTranslation: Boolean,
|
||||
fullscreenMode: Boolean,
|
||||
isLoading: Boolean,
|
||||
showMenu: Boolean,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
|
||||
|
|
@ -107,16 +106,14 @@ fun ReaderLayout(
|
|||
onShareRequested = { textToShare ->
|
||||
openShareApp(
|
||||
ReaderEvent.OnOpenShareApp(
|
||||
textToShare = textToShare,
|
||||
activity = activity
|
||||
textToShare = textToShare
|
||||
)
|
||||
)
|
||||
},
|
||||
onWebSearchRequested = { textToSearch ->
|
||||
openWebBrowser(
|
||||
ReaderEvent.OnOpenWebBrowser(
|
||||
textToSearch = textToSearch,
|
||||
activity = activity
|
||||
textToSearch = textToSearch
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
@ -124,16 +121,14 @@ fun ReaderLayout(
|
|||
openTranslator(
|
||||
ReaderEvent.OnOpenTranslator(
|
||||
textToTranslate = textToTranslate,
|
||||
translateWholeParagraph = false,
|
||||
activity = activity
|
||||
translateWholeParagraph = false
|
||||
)
|
||||
)
|
||||
},
|
||||
onDictionaryRequested = { textToDefine ->
|
||||
openDictionary(
|
||||
ReaderEvent.OnOpenDictionary(
|
||||
textToDefine,
|
||||
activity = activity
|
||||
textToDefine = textToDefine
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -149,9 +144,7 @@ fun ReaderLayout(
|
|||
menuVisibility(
|
||||
ReaderEvent.OnMenuVisibility(
|
||||
show = !showMenu,
|
||||
fullscreenMode = fullscreenMode,
|
||||
saveCheckpoint = true,
|
||||
activity = activity
|
||||
saveCheckpoint = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -199,7 +192,6 @@ fun ReaderLayout(
|
|||
spacing = paragraphHeight
|
||||
) {
|
||||
ReaderLayoutText(
|
||||
activity = activity,
|
||||
showMenu = showMenu,
|
||||
entry = entry,
|
||||
imagesCornersRoundness = imagesCornersRoundness,
|
||||
|
|
@ -218,7 +210,6 @@ fun ReaderLayout(
|
|||
letterSpacing = letterSpacing,
|
||||
sidePadding = sidePadding,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
fullscreenMode = fullscreenMode,
|
||||
doubleClickTranslation = doubleClickTranslation,
|
||||
highlightedReading = highlightedReading,
|
||||
highlightedReadingThickness = highlightedReadingThickness,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.reader
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -25,7 +24,6 @@ import ua.acclorite.book_story.ui.theme.model.HorizontalAlignment
|
|||
|
||||
@Composable
|
||||
fun LazyItemScope.ReaderLayoutText(
|
||||
activity: ComponentActivity,
|
||||
showMenu: Boolean,
|
||||
entry: ReaderText,
|
||||
imagesCornersRoundness: Dp,
|
||||
|
|
@ -44,7 +42,6 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
letterSpacing: TextUnit,
|
||||
sidePadding: Dp,
|
||||
paragraphIndentation: TextUnit,
|
||||
fullscreenMode: Boolean,
|
||||
doubleClickTranslation: Boolean,
|
||||
highlightedReading: Boolean,
|
||||
highlightedReadingThickness: FontWeight,
|
||||
|
|
@ -85,7 +82,6 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
is ReaderText.Text -> {
|
||||
ReaderLayoutTextParagraph(
|
||||
paragraph = entry,
|
||||
activity = activity,
|
||||
showMenu = showMenu,
|
||||
fontFamily = fontFamily,
|
||||
fontColor = fontColor,
|
||||
|
|
@ -98,7 +94,6 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
letterSpacing = letterSpacing,
|
||||
sidePadding = sidePadding,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
fullscreenMode = fullscreenMode,
|
||||
doubleClickTranslation = doubleClickTranslation,
|
||||
highlightedReading = highlightedReading,
|
||||
highlightedReadingThickness = highlightedReadingThickness,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.reader
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -34,7 +33,6 @@ import ua.acclorite.book_story.ui.reader.model.FontWithName
|
|||
@Composable
|
||||
fun LazyItemScope.ReaderLayoutTextParagraph(
|
||||
paragraph: Text,
|
||||
activity: ComponentActivity,
|
||||
showMenu: Boolean,
|
||||
fontFamily: FontWithName,
|
||||
fontColor: Color,
|
||||
|
|
@ -47,7 +45,6 @@ fun LazyItemScope.ReaderLayoutTextParagraph(
|
|||
letterSpacing: TextUnit,
|
||||
sidePadding: Dp,
|
||||
paragraphIndentation: TextUnit,
|
||||
fullscreenMode: Boolean,
|
||||
doubleClickTranslation: Boolean,
|
||||
highlightedReading: Boolean,
|
||||
highlightedReadingThickness: FontWeight,
|
||||
|
|
@ -72,8 +69,7 @@ fun LazyItemScope.ReaderLayoutTextParagraph(
|
|||
openTranslator(
|
||||
ReaderEvent.OnOpenTranslator(
|
||||
textToTranslate = paragraph.line.text,
|
||||
translateWholeParagraph = true,
|
||||
activity = activity
|
||||
translateWholeParagraph = true
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
@ -81,9 +77,7 @@ fun LazyItemScope.ReaderLayoutTextParagraph(
|
|||
menuVisibility(
|
||||
ReaderEvent.OnMenuVisibility(
|
||||
show = !showMenu,
|
||||
fullscreenMode = fullscreenMode,
|
||||
saveCheckpoint = true,
|
||||
activity = activity
|
||||
saveCheckpoint = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ fun ReaderScaffold(
|
|||
letterSpacing: TextUnit,
|
||||
paragraphIndentation: TextUnit,
|
||||
doubleClickTranslation: Boolean,
|
||||
fullscreenMode: Boolean,
|
||||
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
|
||||
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
|
||||
|
|
@ -106,8 +105,8 @@ fun ReaderScaffold(
|
|||
openDictionary: (ReaderEvent.OnOpenDictionary) -> Unit,
|
||||
showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit,
|
||||
showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit,
|
||||
navigateToBookInfo: (changePath: Boolean) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToBookInfo: (ReaderEvent.OnNavigateToBookInfo) -> Unit,
|
||||
navigateBack: (ReaderEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -197,7 +196,6 @@ fun ReaderScaffold(
|
|||
letterSpacing = letterSpacing,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
doubleClickTranslation = doubleClickTranslation,
|
||||
fullscreenMode = fullscreenMode,
|
||||
isLoading = isLoading,
|
||||
showMenu = showMenu,
|
||||
menuVisibility = menuVisibility,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import kotlinx.coroutines.launch
|
|||
import ua.acclorite.book_story.presentation.reader.ReaderEvent
|
||||
import ua.acclorite.book_story.ui.common.components.common.LazyColumnWithScrollbar
|
||||
import ua.acclorite.book_story.ui.common.components.modal_bottom_sheet.ModalBottomSheet
|
||||
import ua.acclorite.book_story.ui.common.helpers.LocalActivity
|
||||
import ua.acclorite.book_story.ui.settings.appearance.colors.ColorsSubcategory
|
||||
import ua.acclorite.book_story.ui.settings.reader.chapters.ChaptersSubcategory
|
||||
import ua.acclorite.book_story.ui.settings.reader.font.FontSubcategory
|
||||
|
|
@ -47,11 +46,9 @@ private var initialPage = 0
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReaderSettingsBottomSheet(
|
||||
fullscreenMode: Boolean,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
|
||||
dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(initialPage) { 3 }
|
||||
DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } }
|
||||
|
|
@ -70,9 +67,7 @@ fun ReaderSettingsBottomSheet(
|
|||
menuVisibility(
|
||||
ReaderEvent.OnMenuVisibility(
|
||||
show = pagerState.currentPage != 2,
|
||||
fullscreenMode = fullscreenMode,
|
||||
saveCheckpoint = false,
|
||||
activity = activity
|
||||
saveCheckpoint = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ fun ReaderTopBar(
|
|||
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
|
||||
showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit,
|
||||
showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit,
|
||||
navigateToBookInfo: (changePath: Boolean) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToBookInfo: (ReaderEvent.OnNavigateToBookInfo) -> Unit,
|
||||
navigateBack: (ReaderEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val animatedChapterProgress = animateFloatAsState(
|
||||
|
|
@ -81,9 +81,8 @@ fun ReaderTopBar(
|
|||
) {
|
||||
leave(
|
||||
ReaderEvent.OnLeave(
|
||||
activity = activity,
|
||||
navigate = {
|
||||
navigateBack()
|
||||
navigateBack(ReaderEvent.OnNavigateBack)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
@ -99,9 +98,12 @@ fun ReaderTopBar(
|
|||
onClick = {
|
||||
leave(
|
||||
ReaderEvent.OnLeave(
|
||||
activity = activity,
|
||||
navigate = {
|
||||
navigateToBookInfo(false)
|
||||
navigateToBookInfo(
|
||||
ReaderEvent.OnNavigateToBookInfo(
|
||||
changePath = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue