mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2026-01-31 07:54:09 +01:00
Rewrite Reader, add side menu and many bugfixes
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.data.models
|
||||
|
||||
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -29,9 +30,22 @@ data class Manga(
|
||||
val inLibraryAt: Long,
|
||||
val unreadCount: Int?,
|
||||
val downloadCount: Int?
|
||||
)
|
||||
) {
|
||||
suspend fun updateRemote(
|
||||
mangaHandler: MangaInteractionHandler,
|
||||
readerMode: String = meta.juiReaderMode
|
||||
) {
|
||||
if (readerMode != meta.juiReaderMode) {
|
||||
mangaHandler.updateMangaMeta(this, "juiReaderMode", readerMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaMeta(
|
||||
val jui: Int? = null
|
||||
)
|
||||
val juiReaderMode: String = DEFAULT_READER_MODE
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_READER_MODE = "default"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,16 @@ import ca.gosyer.util.system.getAsFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
|
||||
class ReaderModeWatch(
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val scope: CoroutineScope,
|
||||
private val mode: StateFlow<String> = readerPreferences.mode().stateIn(scope),
|
||||
initialPreferences: ReaderModePreferences = readerPreferences.getMode(
|
||||
readerPreferences.mode().get()
|
||||
mode.value
|
||||
)
|
||||
) {
|
||||
private val preferenceJobs = mutableListOf<Job>()
|
||||
@@ -29,12 +31,10 @@ class ReaderModeWatch(
|
||||
val maxSize = MutableStateFlow(initialPreferences.maxSize().get())
|
||||
val navigationMode = MutableStateFlow(initialPreferences.navigationMode().get())
|
||||
|
||||
private val mode = readerPreferences.mode().stateIn(scope)
|
||||
|
||||
init {
|
||||
setupJobs(mode.value)
|
||||
mode
|
||||
.onEach { mode ->
|
||||
.mapLatest { mode ->
|
||||
setupJobs(mode)
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
@@ -12,16 +12,13 @@ import ca.gosyer.ui.reader.loader.TachideskPageLoader
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import ca.gosyer.util.system.CKLogger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ChapterLoader(
|
||||
val context: CoroutineContext,
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val chapterHandler: ChapterInteractionHandler
|
||||
) {
|
||||
@@ -32,7 +29,7 @@ class ChapterLoader(
|
||||
chapter.state = ReaderChapter.State.Loading
|
||||
debug { "Loading pages for ${chapter.chapter.name}" }
|
||||
|
||||
val loader = TachideskPageLoader(context + Dispatchers.Default, chapter, readerPreferences, chapterHandler)
|
||||
val loader = TachideskPageLoader(chapter, readerPreferences, chapterHandler)
|
||||
|
||||
val pages = loader.getPages()
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -15,8 +21,13 @@ import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ChevronRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -139,6 +150,8 @@ fun ReaderMenu(
|
||||
val chapter by vm.chapter.collectAsState()
|
||||
val nextChapter by vm.nextChapter.collectAsState()
|
||||
val pages by vm.pages.collectAsState()
|
||||
val readerModes by vm.readerModes.collectAsState()
|
||||
val readerMode by vm.readerMode.collectAsState()
|
||||
val continuous by vm.readerModeSettings.continuous.collectAsState()
|
||||
val direction by vm.readerModeSettings.direction.collectAsState()
|
||||
val padding by vm.readerModeSettings.padding.collectAsState()
|
||||
@@ -177,16 +190,49 @@ fun ReaderMenu(
|
||||
Surface {
|
||||
Crossfade(state to chapter) { (state, chapter) ->
|
||||
if (state is ReaderChapter.State.Loaded && chapter != null) {
|
||||
Box(
|
||||
Modifier.fillMaxSize()
|
||||
.navigationClickable(navigationMode.toNavigation()) {
|
||||
vm.navigate(it)
|
||||
if (pages.isNotEmpty()) {
|
||||
var sideMenuOpen by remember { mutableStateOf(true) }
|
||||
val sideMenuSize by animateDpAsState(
|
||||
targetValue = if (sideMenuOpen) {
|
||||
260.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
) {
|
||||
)
|
||||
|
||||
val loadingModifier = Modifier.fillMaxWidth().aspectRatio(mangaAspectRatio)
|
||||
if (pages.isNotEmpty()) {
|
||||
AnimatedVisibility(
|
||||
sideMenuOpen,
|
||||
enter = fadeIn() + slideInHorizontally(),
|
||||
exit = fadeOut() + slideOutHorizontally()
|
||||
) {
|
||||
ReaderSideMenu(
|
||||
chapter = chapter,
|
||||
currentPage = currentPage,
|
||||
readerModes = readerModes,
|
||||
selectedMode = readerMode,
|
||||
onNewPageClicked = vm::navigate,
|
||||
onCloseSideMenuClicked = {
|
||||
sideMenuOpen = false
|
||||
},
|
||||
onSetReaderMode = vm::setMangaReaderMode,
|
||||
onPrevChapterClicked = vm::prevChapter,
|
||||
onNextChapterClicked = vm::nextChapter
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.padding(start = sideMenuSize).fillMaxSize()
|
||||
) {
|
||||
val readerModifier = Modifier
|
||||
.navigationClickable(
|
||||
navigation = navigationMode.toNavigation(),
|
||||
onClick = vm::navigate
|
||||
)
|
||||
|
||||
if (continuous) {
|
||||
ContinuousReader(
|
||||
readerModifier,
|
||||
pages,
|
||||
direction,
|
||||
maxSize,
|
||||
@@ -213,6 +259,7 @@ fun ReaderMenu(
|
||||
)
|
||||
} else {
|
||||
PagerReader(
|
||||
readerModifier,
|
||||
direction,
|
||||
currentPage,
|
||||
pages,
|
||||
@@ -226,9 +273,10 @@ fun ReaderMenu(
|
||||
vm::progress
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ErrorScreen(stringResource("no_pages_found"))
|
||||
SideMenuButton(sideMenuOpen, onOpenSideMenuClicked = { sideMenuOpen = true })
|
||||
}
|
||||
} else {
|
||||
ErrorScreen(stringResource("no_pages_found"))
|
||||
}
|
||||
} else {
|
||||
LoadingScreen(
|
||||
@@ -241,6 +289,19 @@ fun ReaderMenu(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideMenuButton(sideMenuOpen: Boolean, onOpenSideMenuClicked: () -> Unit) {
|
||||
AnimatedVisibility(
|
||||
!sideMenuOpen,
|
||||
enter = fadeIn() + slideInHorizontally(),
|
||||
exit = fadeOut() + slideOutHorizontally()
|
||||
) {
|
||||
IconButton(onOpenSideMenuClicked) {
|
||||
Icon(Icons.Rounded.ChevronRight, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderImage(
|
||||
imageIndex: Int,
|
||||
|
||||
@@ -6,34 +6,48 @@
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.data.models.MangaMeta
|
||||
import ca.gosyer.data.reader.ReaderModeWatch
|
||||
import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||
import ca.gosyer.ui.base.prefs.asStateIn
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.ui.reader.model.MoveTo
|
||||
import ca.gosyer.ui.reader.model.Navigation
|
||||
import ca.gosyer.ui.reader.model.PageMove
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import ca.gosyer.ui.reader.model.ViewerChapters
|
||||
import ca.gosyer.util.lang.throwIfCancellation
|
||||
import ca.gosyer.util.system.CKLogger
|
||||
import ca.gosyer.util.system.getAsFlow
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReaderMenuViewModel @Inject constructor(
|
||||
private val params: Params,
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val mangaHandler: MangaInteractionHandler,
|
||||
private val chapterHandler: ChapterInteractionHandler
|
||||
) : ViewModel() {
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
private val viewerChapters = ViewerChapters(
|
||||
MutableStateFlow(null),
|
||||
MutableStateFlow(null),
|
||||
@@ -55,12 +69,25 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
private val _currentPageOffset = MutableStateFlow(1)
|
||||
val currentPageOffset = _currentPageOffset.asStateFlow()
|
||||
|
||||
private val _pageEmitter = MutableSharedFlow<Pair<MoveTo, Int>>()
|
||||
private val _pageEmitter = MutableSharedFlow<PageMove>()
|
||||
val pageEmitter = _pageEmitter.asSharedFlow()
|
||||
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope)
|
||||
val readerModes = readerPreferences.modes().asStateIn(scope)
|
||||
val readerMode = combine(merge(readerPreferences.mode().getAsFlow()), _manga) { mode, manga ->
|
||||
if (
|
||||
manga != null &&
|
||||
manga.meta.juiReaderMode != MangaMeta.DEFAULT_READER_MODE &&
|
||||
manga.meta.juiReaderMode in readerModes.value
|
||||
) {
|
||||
manga.meta.juiReaderMode
|
||||
} else {
|
||||
mode
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get())
|
||||
|
||||
private val loader = ChapterLoader(scope.coroutineContext, readerPreferences, chapterHandler)
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode)
|
||||
|
||||
private val loader = ChapterLoader(readerPreferences, chapterHandler)
|
||||
|
||||
init {
|
||||
init()
|
||||
@@ -68,7 +95,8 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
|
||||
fun init() {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
init(params.mangaId, params.chapterIndex)
|
||||
initManga(params.mangaId)
|
||||
initChapters(params.mangaId, params.chapterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,31 +116,81 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
if (moveTo != null) {
|
||||
moveDirection(moveTo)
|
||||
_pageEmitter.emit(PageMove.Direction(moveTo, currentPage.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun moveDirection(direction: MoveTo) {
|
||||
_pageEmitter.emit(direction to currentPage.value)
|
||||
fun navigate(page: Int) {
|
||||
info { "Navigate to $page" }
|
||||
scope.launch {
|
||||
_pageEmitter.emit(PageMove.Page(page))
|
||||
}
|
||||
}
|
||||
|
||||
fun progress(index: Int) {
|
||||
info { "Progressed to $index" }
|
||||
_currentPage.value = index
|
||||
}
|
||||
|
||||
fun retry(page: ReaderPage) {
|
||||
info { "Retrying $page" }
|
||||
chapter.value?.pageLoader?.retryPage(page)
|
||||
}
|
||||
|
||||
private fun resetValues() {
|
||||
viewerChapters.recycle()
|
||||
_pages.value = emptyList()
|
||||
_currentPage.value = 1
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
viewerChapters.recycle()
|
||||
}
|
||||
|
||||
suspend fun init(mangaId: Long, chapterIndex: Int) {
|
||||
fun setMangaReaderMode(mode: String) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
_manga.value?.updateRemote(
|
||||
mangaHandler,
|
||||
mode
|
||||
)
|
||||
initManga(params.mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
fun prevChapter() {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val prevChapter = previousChapter.value ?: return@launch
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
initChapters(params.mangaId, prevChapter.chapter.index)
|
||||
} catch (e: Exception) {
|
||||
info(e) { "Error loading prev chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nextChapter() {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val nextChapter = nextChapter.value ?: return@launch
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
initChapters(params.mangaId, nextChapter.chapter.index)
|
||||
} catch (e: Exception) {
|
||||
info(e) { "Error loading next chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initManga(mangaId: Long) {
|
||||
try {
|
||||
_manga.value = mangaHandler.getManga(mangaId)
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
_state.value = ReaderChapter.State.Error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initChapters(mangaId: Long, chapterIndex: Int) {
|
||||
resetValues()
|
||||
val chapter = ReaderChapter(
|
||||
try {
|
||||
@@ -147,12 +225,12 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
}
|
||||
val lastPageRead = chapter.chapter.lastPageRead
|
||||
if (lastPageRead != 0) {
|
||||
_currentPage.value = lastPageRead
|
||||
_currentPage.value = lastPageRead.coerceAtMost(chapter.chapter.pageCount!!)
|
||||
}
|
||||
|
||||
val lastPageReadOffset = chapter.chapter.meta.juiPageOffset
|
||||
if (lastPageReadOffset != 0) {
|
||||
_currentPage.value = lastPageReadOffset
|
||||
_currentPageOffset.value = lastPageReadOffset
|
||||
}
|
||||
|
||||
chapter.stateObserver
|
||||
@@ -162,17 +240,16 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
.launchIn(chapter.scope)
|
||||
pages
|
||||
.onEach { pageList ->
|
||||
pageList.forEach { it.chapter = chapter }
|
||||
_pages.value = pageList
|
||||
pageList.getOrNull(_currentPage.value - 1)?.let { chapter.pageLoader?.loadPage(it) }
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
|
||||
_currentPage
|
||||
.onEach { index ->
|
||||
pages.value.getOrNull(_currentPage.value - 1)?.let { chapter.pageLoader?.loadPage(it) }
|
||||
if (index == pages.value.size) {
|
||||
markChapterRead(mangaId, chapter)
|
||||
} else {
|
||||
pages.value.getOrNull(index - 1)?.let { chapter.pageLoader?.loadPage(it) }
|
||||
}
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
@@ -183,18 +260,22 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun sendProgress() {
|
||||
val chapter = chapter.value?.chapter ?: return
|
||||
fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) {
|
||||
chapter ?: return
|
||||
if (chapter.read) return
|
||||
GlobalScope.launch {
|
||||
chapterHandler.updateChapter(chapter.mangaId, chapter.index, lastPageRead = currentPage.value)
|
||||
chapterHandler.updateChapter(chapter.mangaId, chapter.index, lastPageRead = lastPageRead)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun updateLastPageReadOffset(offset: Int) {
|
||||
updateLastPageReadOffset(chapter.value?.chapter ?: return, offset)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun updateLastPageReadOffset(chapter: Chapter, offset: Int) {
|
||||
GlobalScope.launch {
|
||||
chapter.value?.chapter?.updateRemote(chapterHandler, offset)
|
||||
chapter.updateRemote(chapterHandler, offset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,4 +284,6 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
data class Params(val chapterIndex: Int, val mangaId: Long)
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
197
src/main/kotlin/ca/gosyer/ui/reader/ReaderSideMenu.kt
Normal file
197
src/main/kotlin/ca/gosyer/ui/reader/ReaderSideMenu.kt
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Slider
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ChevronLeft
|
||||
import androidx.compose.material.icons.rounded.NavigateBefore
|
||||
import androidx.compose.material.icons.rounded.NavigateNext
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.common.util.replace
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.models.ChapterMeta
|
||||
import ca.gosyer.data.models.MangaMeta
|
||||
import ca.gosyer.ui.base.components.Spinner
|
||||
import ca.gosyer.ui.base.resources.stringResource
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.util.lang.milliseconds
|
||||
import ca.gosyer.util.system.kLogger
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val logger = kLogger {}
|
||||
|
||||
@Composable
|
||||
fun ReaderSideMenu(
|
||||
chapter: ReaderChapter,
|
||||
currentPage: Int,
|
||||
readerModes: List<String>,
|
||||
selectedMode: String,
|
||||
onNewPageClicked: (Int) -> Unit,
|
||||
onCloseSideMenuClicked: () -> Unit,
|
||||
onSetReaderMode: (String) -> Unit,
|
||||
onPrevChapterClicked: () -> Unit,
|
||||
onNextChapterClicked: () -> Unit
|
||||
) {
|
||||
Surface(Modifier.fillMaxHeight().width(260.dp)) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
val pageCount = chapter.chapter.pageCount!!
|
||||
ReaderMenuToolbar(onCloseSideMenuClicked)
|
||||
ReaderModeSetting(readerModes, selectedMode, onSetReaderMode)
|
||||
ReaderProgressSlider(currentPage, pageCount, onNewPageClicked)
|
||||
NavigateChapters(onPrevChapterClicked, onNextChapterClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderModeSetting(readerModes: List<String>, selectedMode: String, onSetReaderMode: (String) -> Unit) {
|
||||
val modes = remember(readerModes) { listOf(MangaMeta.DEFAULT_READER_MODE) + readerModes }
|
||||
val defaultModeString = stringResource("default_reader_mode")
|
||||
val displayModes = remember(modes, defaultModeString) { modes.replace(0, defaultModeString) }
|
||||
val selectedModeIndex = remember(modes, selectedMode) { modes.indexOf(selectedMode) }
|
||||
Row(
|
||||
Modifier.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(stringResource("reader_mode"), Modifier.weight(0.25f), maxLines = 2, fontSize = 14.sp)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spinner(
|
||||
modifier = Modifier.weight(0.75f),
|
||||
items = displayModes,
|
||||
selectedItemIndex = selectedModeIndex
|
||||
) {
|
||||
onSetReaderMode(modes[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReaderMenuToolbar(onCloseSideMenuClicked: () -> Unit) {
|
||||
Surface(elevation = 2.dp) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
IconButton(onCloseSideMenuClicked) {
|
||||
Icon(Icons.Rounded.ChevronLeft, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReaderProgressSlider(
|
||||
currentPage: Int,
|
||||
pageCount: Int,
|
||||
onNewPageClicked: (Int) -> Unit,
|
||||
) {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = currentPage.toFloat(),
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
|
||||
)
|
||||
var isValueChanging by remember { mutableStateOf(false) }
|
||||
Row {
|
||||
Slider(
|
||||
animatedProgress,
|
||||
onValueChange = {
|
||||
if (!isValueChanging) {
|
||||
isValueChanging = true
|
||||
onNewPageClicked(it.roundToInt())
|
||||
}
|
||||
},
|
||||
valueRange = 0F..pageCount.toFloat(),
|
||||
steps = pageCount,
|
||||
onValueChangeFinished = { isValueChanging = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigateChapters(loadPrevChapter: () -> Unit, loadNextChapter: () -> Unit) {
|
||||
Divider(Modifier.padding(horizontal = 4.dp, vertical = 8.dp))
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween,) {
|
||||
OutlinedButton(loadPrevChapter, Modifier.weight(0.5F)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val nextChapter = stringResource("nav_prev_chapter")
|
||||
Icon(Icons.Rounded.NavigateBefore, nextChapter)
|
||||
Text(nextChapter, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
OutlinedButton(loadNextChapter, Modifier.weight(0.5F)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val nextChapter = stringResource("nav_next_chapter")
|
||||
Text(nextChapter, fontSize = 10.sp)
|
||||
Icon(Icons.Rounded.NavigateNext, nextChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ReaderSideMenuPreview() {
|
||||
ReaderSideMenu(
|
||||
chapter = remember {
|
||||
ReaderChapter(
|
||||
Chapter(
|
||||
url = "",
|
||||
name = "Test Chapter",
|
||||
uploadDate = System.currentTimeMillis(),
|
||||
chapterNumber = 15.5F,
|
||||
scanlator = "No Group",
|
||||
mangaId = 100L,
|
||||
read = false,
|
||||
bookmarked = false,
|
||||
lastPageRead = 11,
|
||||
index = 10,
|
||||
fetchedAt = System.currentTimeMillis(),
|
||||
chapterCount = null,
|
||||
pageCount = 20,
|
||||
lastReadAt = System.currentTimeMillis().milliseconds.inWholeSeconds.toInt(),
|
||||
downloaded = false,
|
||||
ChapterMeta()
|
||||
)
|
||||
)
|
||||
},
|
||||
currentPage = 11,
|
||||
readerModes = listOf("Vertical"),
|
||||
selectedMode = "Vertical",
|
||||
onNewPageClicked = {},
|
||||
onCloseSideMenuClicked = {},
|
||||
onSetReaderMode = {},
|
||||
onPrevChapterClicked = {},
|
||||
onNextChapterClicked = {}
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import ca.gosyer.util.system.CKLogger
|
||||
import io.github.kerubistan.kroki.coroutines.priorityChannel
|
||||
import io.ktor.client.features.onDownload
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -22,15 +23,13 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TachideskPageLoader(
|
||||
context: CoroutineContext,
|
||||
val chapter: ReaderChapter,
|
||||
readerPreferences: ReaderPreferences,
|
||||
chapterHandler: ChapterInteractionHandler
|
||||
) : PageLoader() {
|
||||
val scope = CoroutineScope(SupervisorJob() + context)
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
/**
|
||||
* A channel used to manage requests one by one while allowing priorities.
|
||||
@@ -107,14 +106,15 @@ class TachideskPageLoader(
|
||||
override fun getPages(): StateFlow<List<ReaderPage>> {
|
||||
scope.launch {
|
||||
if (pagesFlow.value.isNotEmpty()) return@launch
|
||||
val pageRange = chapter.chapter.pageCount?.let { 0..it } ?: IntRange.EMPTY
|
||||
val pageRange = chapter.chapter.pageCount?.let { 0..it.minus(1) } ?: IntRange.EMPTY
|
||||
pagesFlow.value = pageRange.map {
|
||||
ReaderPage(
|
||||
index = it,
|
||||
bitmap = MutableStateFlow(null),
|
||||
progress = MutableStateFlow(0.0F),
|
||||
status = MutableStateFlow(ReaderPage.Status.QUEUE),
|
||||
error = MutableStateFlow(null)
|
||||
error = MutableStateFlow(null),
|
||||
chapter = chapter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
12
src/main/kotlin/ca/gosyer/ui/reader/model/PageMove.kt
Normal file
12
src/main/kotlin/ca/gosyer/ui/reader/model/PageMove.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.model
|
||||
|
||||
sealed class PageMove {
|
||||
data class Direction(val moveTo: MoveTo, val currentPage: Int) : PageMove()
|
||||
data class Page(val pageNumber: Int) : PageMove()
|
||||
}
|
||||
@@ -14,13 +14,11 @@ data class ReaderPage(
|
||||
val bitmap: MutableStateFlow<ImageBitmap?>,
|
||||
val progress: MutableStateFlow<Float>,
|
||||
val status: MutableStateFlow<Status>,
|
||||
val error: MutableStateFlow<String?>
|
||||
val error: MutableStateFlow<String?>,
|
||||
val chapter: ReaderChapter
|
||||
) {
|
||||
lateinit var chapter: ReaderChapter
|
||||
enum class Status {
|
||||
QUEUE,
|
||||
LOAD_PAGE,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package ca.gosyer.ui.reader.navigation
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -15,27 +16,15 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.changedToDown
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import ca.gosyer.ui.base.components.LocalComposeWindow
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import ca.gosyer.ui.reader.model.Navigation
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
|
||||
var event: PointerEvent
|
||||
do {
|
||||
event = awaitPointerEvent()
|
||||
} while (
|
||||
!event.changes.all { it.changedToDown() }
|
||||
)
|
||||
return event
|
||||
}
|
||||
import ca.gosyer.util.compose.contains
|
||||
|
||||
fun Modifier.navigationClickable(
|
||||
navigation: ViewerNavigation,
|
||||
@@ -72,19 +61,23 @@ fun Modifier.navigationClickable(
|
||||
properties["interactionSource"] = interactionSource
|
||||
}
|
||||
) {
|
||||
var lastEvent by remember { mutableStateOf<MouseEvent?>(null) }
|
||||
val window = LocalComposeWindow.current
|
||||
var offsetEvent by remember { mutableStateOf<Offset?>(null) }
|
||||
var layoutSize by remember { mutableStateOf(Size.Zero) }
|
||||
Modifier
|
||||
.clickable(interactionSource, null, enabled, onClickLabel, role) {
|
||||
val savedLastEvent = lastEvent ?: return@clickable
|
||||
val offset = savedLastEvent.let { IntOffset(it.x, it.y) }
|
||||
onClick(navigation.getAction(offset, IntSize(window.width, window.height)))
|
||||
val offset = offsetEvent ?: return@clickable
|
||||
if (offset in layoutSize) {
|
||||
onClick(navigation.getAction(offset, layoutSize))
|
||||
}
|
||||
}
|
||||
.pointerInput(interactionSource) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
lastEvent = awaitEventFirstDown().awtEvent
|
||||
offsetEvent = awaitFirstDown().position
|
||||
}
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned {
|
||||
layoutSize = it.size.toSize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
package ca.gosyer.ui.reader.navigation
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import ca.gosyer.data.reader.model.TappingInvertMode
|
||||
import ca.gosyer.ui.reader.model.Navigation
|
||||
|
||||
@@ -54,9 +55,9 @@ abstract class ViewerNavigation {
|
||||
|
||||
var invertMode: TappingInvertMode = TappingInvertMode.NONE
|
||||
|
||||
fun getAction(pos: IntOffset, windowSize: IntSize): Navigation {
|
||||
val realX = pos.x / (windowSize.width * 0.01F)
|
||||
val realY = pos.y / (windowSize.height * 0.01F)
|
||||
fun getAction(pos: Offset, layoutSize: Size): Navigation {
|
||||
val realX = pos.x / (layoutSize.width * 0.01F)
|
||||
val realY = pos.y / (layoutSize.height * 0.01F)
|
||||
val realPos = IntOffset(realX.toInt(), realY.toInt())
|
||||
|
||||
val region = regions.map { it.invert(invertMode) }
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -32,6 +33,7 @@ import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.ui.reader.ChapterSeparator
|
||||
import ca.gosyer.ui.reader.ReaderImage
|
||||
import ca.gosyer.ui.reader.model.MoveTo
|
||||
import ca.gosyer.ui.reader.model.PageMove
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.mapLatest
|
||||
|
||||
@Composable
|
||||
fun ContinuousReader(
|
||||
modifier: Modifier,
|
||||
pages: List<ReaderPage>,
|
||||
direction: Direction,
|
||||
maxSize: Int,
|
||||
@@ -51,21 +54,33 @@ fun ContinuousReader(
|
||||
nextChapter: ReaderChapter?,
|
||||
loadingModifier: Modifier,
|
||||
pageContentScale: ContentScale,
|
||||
pageEmitter: SharedFlow<Pair<MoveTo, Int>>,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit,
|
||||
updateLastPageReadOffset: (Int) -> Unit
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
BoxWithConstraints(modifier then Modifier.fillMaxSize()) {
|
||||
val state = rememberLazyListState(currentPage, currentPageOffset)
|
||||
LaunchedEffect(Unit) {
|
||||
pageEmitter
|
||||
.mapLatest { (moveTo) ->
|
||||
val by = when (moveTo) {
|
||||
MoveTo.Previous -> -maxHeight
|
||||
MoveTo.Next -> maxHeight
|
||||
.mapLatest { pageMove ->
|
||||
when (pageMove) {
|
||||
is PageMove.Direction -> {
|
||||
val (moveTo) = pageMove
|
||||
val by = when (moveTo) {
|
||||
MoveTo.Previous -> -maxHeight
|
||||
MoveTo.Next -> maxHeight
|
||||
}
|
||||
state.animateScrollBy(by.value)
|
||||
Unit
|
||||
}
|
||||
is PageMove.Page -> {
|
||||
val (pageNumber) = pageMove
|
||||
if (pageNumber in 0..pages.size) {
|
||||
state.animateScrollToItem(pageNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.animateScrollBy(by.value)
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.ui.reader.viewer
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -15,6 +16,7 @@ import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.ui.reader.ChapterSeparator
|
||||
import ca.gosyer.ui.reader.ReaderImage
|
||||
import ca.gosyer.ui.reader.model.MoveTo
|
||||
import ca.gosyer.ui.reader.model.PageMove
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
@@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.mapLatest
|
||||
|
||||
@Composable
|
||||
fun PagerReader(
|
||||
parentModifier: Modifier,
|
||||
direction: Direction,
|
||||
currentPage: Int,
|
||||
pages: List<ReaderPage>,
|
||||
@@ -34,21 +37,33 @@ fun PagerReader(
|
||||
nextChapter: ReaderChapter?,
|
||||
loadingModifier: Modifier,
|
||||
pageContentScale: ContentScale,
|
||||
pageEmitter: SharedFlow<Pair<MoveTo, Int>>,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit
|
||||
) {
|
||||
val state = rememberPagerState(pages.size + 1, initialPage = currentPage)
|
||||
val state = rememberPagerState(pages.size + 2, initialPage = currentPage)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val pageRange = 0..(pages.size + 1)
|
||||
pageEmitter
|
||||
.mapLatest { (moveTo, currentPage) ->
|
||||
val page = when (moveTo) {
|
||||
MoveTo.Previous -> currentPage - 1
|
||||
MoveTo.Next -> currentPage + 1
|
||||
}
|
||||
if (page in 0..pages.size) {
|
||||
state.animateScrollToPage(page)
|
||||
.mapLatest { pageMove ->
|
||||
when (pageMove) {
|
||||
is PageMove.Direction -> {
|
||||
val (moveTo, currentPage) = pageMove
|
||||
val page = when (moveTo) {
|
||||
MoveTo.Previous -> currentPage - 1
|
||||
MoveTo.Next -> currentPage + 1
|
||||
}
|
||||
if (page in pageRange) {
|
||||
state.animateScrollToPage(page)
|
||||
}
|
||||
}
|
||||
is PageMove.Page -> {
|
||||
val (pageNumber) = pageMove
|
||||
if (pageNumber in pageRange) {
|
||||
state.animateScrollToPage(pageNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
@@ -59,9 +74,10 @@ fun PagerReader(
|
||||
progress(state.currentPage)
|
||||
}
|
||||
}
|
||||
val modifier = parentModifier then Modifier.fillMaxSize()
|
||||
|
||||
if (direction == Direction.Down || direction == Direction.Up) {
|
||||
VerticalPager(state, reverseLayout = direction == Direction.Up) {
|
||||
VerticalPager(state, reverseLayout = direction == Direction.Up, modifier = modifier) {
|
||||
HandlePager(
|
||||
pages,
|
||||
it,
|
||||
@@ -74,7 +90,7 @@ fun PagerReader(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
HorizontalPager(state, reverseLayout = direction == Direction.Left) {
|
||||
HorizontalPager(state, reverseLayout = direction == Direction.Left, modifier = modifier) {
|
||||
HandlePager(
|
||||
pages,
|
||||
it,
|
||||
@@ -102,7 +118,7 @@ fun HandlePager(
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ChapterSeparator(previousChapter, currentChapter)
|
||||
pages.size -> ChapterSeparator(currentChapter, nextChapter)
|
||||
pages.size + 1 -> ChapterSeparator(currentChapter, nextChapter)
|
||||
else -> {
|
||||
val image = pages[page - 1]
|
||||
ReaderImage(
|
||||
|
||||
20
src/main/kotlin/ca/gosyer/util/compose/Offset.kt
Normal file
20
src/main/kotlin/ca/gosyer/util/compose/Offset.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.util.compose
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
operator fun IntSize.contains(offset: IntOffset): Boolean {
|
||||
return offset.x <= width && offset.y <= height
|
||||
}
|
||||
|
||||
operator fun Size.contains(offset: Offset): Boolean {
|
||||
return offset.x <= width && offset.y <= height
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
fun <T> Preference<T>.getAsFlow(action: suspend (T) -> Unit): Flow<T> {
|
||||
return merge(flowOf(get()), changes()).onEach(action = action)
|
||||
fun <T> Preference<T>.getAsFlow(action: (suspend (T) -> Unit)? = null): Flow<T> {
|
||||
val flow = merge(flowOf(get()), changes())
|
||||
return if (action != null) {
|
||||
flow.onEach(action = action)
|
||||
} else flow
|
||||
}
|
||||
|
||||
@@ -77,11 +77,15 @@
|
||||
<string name="filter_source">Filter</string>
|
||||
|
||||
<!-- Reader Menu -->
|
||||
<string name="default_reader_mode">Default</string>
|
||||
<string name="reader_mode">Reader mode</string>
|
||||
<string name="no_pages_found">No pages found</string>
|
||||
<string name="no_previous_chapter">There is no previous chapter</string>
|
||||
<string name="previous_chapter">Previous:\n %1$s</string>
|
||||
<string name="next_chapter">Next:\n %1$s</string>
|
||||
<string name="no_next_chapter">There is no next chapter</string>
|
||||
<string name="nav_next_chapter">Next chapter</string>
|
||||
<string name="nav_prev_chapter">Previous chapter</string>
|
||||
|
||||
<!-- Settings-->
|
||||
<string name="settings_advanced">Advanced</string>
|
||||
|
||||
Reference in New Issue
Block a user