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