Reader improvements, hotkey support, mark as read when finished, a lot more

This commit is contained in:
Syer10
2021-05-27 19:59:52 -04:00
parent 768746c033
commit 71ce61cc33
5 changed files with 329 additions and 153 deletions

View File

@@ -15,20 +15,16 @@ 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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeysSet
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -42,12 +38,12 @@ import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.mangaAspectRatio
import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.reader.model.MoveTo
import ca.gosyer.ui.reader.model.ReaderChapter
import ca.gosyer.ui.reader.model.ReaderPage
import ca.gosyer.ui.reader.viewer.ContinuousReader
import ca.gosyer.ui.reader.viewer.PagerReader
import ca.gosyer.util.lang.launchUI
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.VerticalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
@@ -113,14 +109,52 @@ fun ReaderMenu(chapterIndex: Int, mangaId: Long, setHotkeys: (List<KeyboardShort
val direction by vm.readerModeSettings.direction.collectAsState()
val padding by vm.readerModeSettings.padding.collectAsState()
val currentPage by vm.currentPage.collectAsState()
remember {
LaunchedEffect(Unit) {
setHotkeys(
listOf(
KeyboardShortcut(KeysSet(setOf(Key.W, Key.DirectionUp))) {
vm.progress(currentPage + 1)
KeyboardShortcut(Key.W) {
vm.moveDirection(MoveTo.Previous)
},
KeyboardShortcut(KeysSet(setOf(Key.S, Key.DirectionDown))) {
vm.progress(currentPage - 1)
KeyboardShortcut(Key.DirectionUp) {
vm.moveDirection(MoveTo.Previous)
},
KeyboardShortcut(Key.S) {
vm.moveDirection(MoveTo.Next)
},
KeyboardShortcut(Key.DirectionDown) {
vm.moveDirection(MoveTo.Next)
},
KeyboardShortcut(Key.A) {
vm.moveDirection(
when (direction) {
Direction.Left -> MoveTo.Next
else -> MoveTo.Previous
}
)
},
KeyboardShortcut(Key.DirectionLeft) {
vm.moveDirection(
when (direction) {
Direction.Left -> MoveTo.Next
else -> MoveTo.Previous
}
)
},
KeyboardShortcut(Key.D) {
vm.moveDirection(
when (direction) {
Direction.Left -> MoveTo.Previous
else -> MoveTo.Next
}
)
},
KeyboardShortcut(Key.DirectionRight) {
vm.moveDirection(
when (direction) {
Direction.Left -> MoveTo.Previous
else -> MoveTo.Next
}
)
}
)
)
@@ -132,9 +166,13 @@ fun ReaderMenu(chapterIndex: Int, mangaId: Long, setHotkeys: (List<KeyboardShort
val pageModifier = Modifier.fillMaxWidth().aspectRatio(mangaAspectRatio)
if (pages.isNotEmpty()) {
if (continuous) {
ContinuesReader(
ContinuousReader(
pages,
previousChapter,
chapter,
nextChapter,
pageModifier,
vm.pageEmitter,
vm::retry,
vm::progress
)
@@ -147,6 +185,7 @@ fun ReaderMenu(chapterIndex: Int, mangaId: Long, setHotkeys: (List<KeyboardShort
chapter,
nextChapter,
pageModifier,
vm.pageEmitter,
vm::retry,
vm::progress
)
@@ -187,88 +226,12 @@ fun ReaderImage(
}
}
@Composable
fun PagerReader(
direction: Direction,
currentPage: Int,
pages: List<ReaderPage>,
previousChapter: ReaderChapter?,
currentChapter: ReaderChapter,
nextChapter: ReaderChapter?,
pageModifier: Modifier,
retry: (ReaderPage) -> Unit,
progress: (Int) -> Unit
) {
val state = rememberPagerState(pages.size + 1, initialPage = currentPage)
LaunchedEffect(state.currentPage) {
if (state.currentPage != currentPage) {
progress(state.currentPage)
}
}
if (direction == Direction.Down || direction == Direction.Up) {
VerticalPager(state, reverseLayout = direction == Direction.Up) {
HandlePager(
pages,
it,
previousChapter,
currentChapter,
nextChapter,
pageModifier,
retry
)
}
} else {
HorizontalPager(state, reverseLayout = direction == Direction.Left) {
HandlePager(
pages,
it,
previousChapter,
currentChapter,
nextChapter,
pageModifier,
retry
)
}
}
}
@Composable
fun HandlePager(
pages: List<ReaderPage>,
page: Int,
previousChapter: ReaderChapter?,
currentChapter: ReaderChapter,
nextChapter: ReaderChapter?,
pageModifier: Modifier,
retry: (ReaderPage) -> Unit,
) {
when (page) {
0 -> ChapterSeperator(previousChapter, currentChapter)
pages.size -> ChapterSeperator(currentChapter, nextChapter)
else -> {
val image = pages[page - 1]
ReaderImage(
image.index,
image.bitmap.collectAsState().value,
image.status.collectAsState().value,
image.error.collectAsState().value,
loadingModifier = pageModifier,
retry = { pageIndex ->
pages.find { it.index == pageIndex }?.let { retry(it) }
}
)
}
}
}
@Composable
fun ChapterSeperator(
previousChapter: ReaderChapter?,
nextChapter: ReaderChapter?
) {
Box(contentAlignment = Alignment.Center) {
Box(Modifier.fillMaxWidth().height(350.dp), contentAlignment = Alignment.Center) {
Column {
when {
previousChapter == null && nextChapter != null -> {
@@ -286,29 +249,3 @@ fun ChapterSeperator(
}
}
}
@Composable
fun ContinuesReader(
pages: List<ReaderPage>,
pageModifier: Modifier,
retry: (ReaderPage) -> Unit,
progress: (Int) -> Unit
) {
LazyColumn {
items(pages) { image ->
LaunchedEffect(image.index) {
progress(image.index)
}
ReaderImage(
image.index,
image.bitmap.collectAsState().value,
image.status.collectAsState().value,
image.error.collectAsState().value,
loadingModifier = pageModifier,
retry = { pageIndex ->
pages.find { it.index == pageIndex }?.let { retry(it) }
}
)
}
}
}

View File

@@ -10,14 +10,15 @@ import ca.gosyer.data.reader.ReaderModeWatch
import ca.gosyer.data.reader.ReaderPreferences
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.ui.reader.model.MoveTo
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -47,6 +48,9 @@ class ReaderMenuViewModel @Inject constructor(
private val _currentPage = MutableStateFlow(1)
val currentPage = _currentPage.asStateFlow()
private val _pageEmitter = MutableSharedFlow<Pair<MoveTo, Int>>()
val pageEmitter = _pageEmitter.asSharedFlow()
val readerModeSettings = ReaderModeWatch(readerPreferences, scope)
private val loader = ChapterLoader(scope.coroutineContext, readerPreferences, chapterHandler)
@@ -57,6 +61,12 @@ class ReaderMenuViewModel @Inject constructor(
}
}
fun moveDirection(direction: MoveTo) {
scope.launch {
_pageEmitter.emit(direction to currentPage.value)
}
}
fun progress(index: Int) {
_currentPage.value = index
}
@@ -76,46 +86,68 @@ class ReaderMenuViewModel @Inject constructor(
resetValues()
val chapter = ReaderChapter(
scope.coroutineContext + Dispatchers.Default,
chapterHandler.getChapter(mangaId, chapterIndex)
try {
chapterHandler.getChapter(mangaId, chapterIndex)
} catch (e: Exception) {
e.throwIfCancellation()
_state.value = ReaderChapter.State.Error(e)
throw e
}
)
val pages = loader.loadChapter(chapter)
viewerChapters.currChapter.value = chapter
scope.launch(Dispatchers.Default) {
listOf(
async {
try {
viewerChapters.nextChapter.value = ReaderChapter(
scope.coroutineContext + Dispatchers.Default,
chapterHandler.getChapter(mangaId, chapterIndex + 1)
)
} catch (e: Exception) {
e.throwIfCancellation()
}
},
async {
if (chapterIndex != 0) {
try {
viewerChapters.prevChapter.value = ReaderChapter(
scope.coroutineContext + Dispatchers.Default,
chapterHandler.getChapter(mangaId, chapterIndex - 1)
)
} catch (e: Exception) {
e.throwIfCancellation()
}
}
}
).awaitAll()
val chapters = try {
chapterHandler.getChapters(mangaId)
} catch (e: Exception) {
e.throwIfCancellation()
emptyList()
}
val nextChapter = chapters.find { it.index == chapterIndex + 1 }
if (nextChapter != null) {
viewerChapters.nextChapter.value = ReaderChapter(
scope.coroutineContext + Dispatchers.Default,
nextChapter
)
}
val prevChapter = chapters.find { it.index == chapterIndex - 1 }
if (prevChapter != null) {
viewerChapters.prevChapter.value = ReaderChapter(
scope.coroutineContext + Dispatchers.Default,
prevChapter
)
}
}
chapter.stateObserver.onEach {
_state.value = it
}.launchIn(chapter.scope)
pages.onEach { pageList ->
pageList.forEach { it.chapter = chapter }
_pages.value = pageList
}.launchIn(chapter.scope)
_currentPage.onEach { index ->
pages.value.getOrNull(index - 1)?.let { chapter.pageLoader?.loadPage(it) }
}.launchIn(chapter.scope)
val lastPageRead = chapter.chapter.lastPageRead
if (lastPageRead != 0) {
_currentPage.value = chapter.chapter.lastPageRead
}
chapter.stateObserver
.onEach {
_state.value = it
}
.launchIn(chapter.scope)
pages
.onEach { pageList ->
pageList.forEach { it.chapter = chapter }
_pages.value = pageList
}
.launchIn(chapter.scope)
_currentPage
.onEach { index ->
if (index == pages.value.size) {
markChapterRead(mangaId, chapter)
} else {
pages.value.getOrNull(index - 1)?.let { chapter.pageLoader?.loadPage(it) }
}
}
.launchIn(chapter.scope)
}
private suspend fun markChapterRead(mangaId: Long, chapter: ReaderChapter) {
chapterHandler.updateChapter(mangaId, chapter.chapter.index, true)
}
data class Params(val chapterIndex: Int, val mangaId: Long)

View 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
enum class MoveTo {
Previous,
Next
}

View File

@@ -0,0 +1,82 @@
/*
* 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.viewer
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import ca.gosyer.ui.reader.ChapterSeperator
import ca.gosyer.ui.reader.ReaderImage
import ca.gosyer.ui.reader.model.MoveTo
import ca.gosyer.ui.reader.model.ReaderChapter
import ca.gosyer.ui.reader.model.ReaderPage
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
@Composable
fun ContinuousReader(
pages: List<ReaderPage>,
previousChapter: ReaderChapter?,
currentChapter: ReaderChapter,
nextChapter: ReaderChapter?,
pageModifier: Modifier,
pageEmitter: SharedFlow<Pair<MoveTo, Int>>,
retry: (ReaderPage) -> Unit,
progress: (Int) -> Unit
) {
BoxWithConstraints {
val state = rememberLazyListState(1)
LaunchedEffect(Unit) {
pageEmitter
.mapLatest { (moveTo) ->
val by = when (moveTo) {
MoveTo.Previous -> -maxHeight
MoveTo.Next -> maxHeight
}
state.animateScrollBy(by.value)
}
.launchIn(this)
}
LazyColumn(state = state) {
item {
LaunchedEffect(Unit) {
progress(0)
}
ChapterSeperator(previousChapter, currentChapter)
}
items(pages) { image ->
LaunchedEffect(image.index) {
progress(image.index)
}
ReaderImage(
image.index,
image.bitmap.collectAsState().value,
image.status.collectAsState().value,
image.error.collectAsState().value,
loadingModifier = pageModifier,
retry = { pageIndex ->
pages.find { it.index == pageIndex }?.let { retry(it) }
}
)
}
item {
LaunchedEffect(Unit) {
progress(pages.size)
}
ChapterSeperator(currentChapter, nextChapter)
}
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.viewer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import ca.gosyer.data.reader.model.Direction
import ca.gosyer.ui.reader.ChapterSeperator
import ca.gosyer.ui.reader.ReaderImage
import ca.gosyer.ui.reader.model.MoveTo
import ca.gosyer.ui.reader.model.ReaderChapter
import ca.gosyer.ui.reader.model.ReaderPage
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.VerticalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
@Composable
fun PagerReader(
direction: Direction,
currentPage: Int,
pages: List<ReaderPage>,
previousChapter: ReaderChapter?,
currentChapter: ReaderChapter,
nextChapter: ReaderChapter?,
pageModifier: Modifier,
pageEmitter: SharedFlow<Pair<MoveTo, Int>>,
retry: (ReaderPage) -> Unit,
progress: (Int) -> Unit
) {
val state = rememberPagerState(pages.size + 1, initialPage = currentPage)
LaunchedEffect(Unit) {
pageEmitter
.mapLatest { (moveTo, currentPage) ->
val page = when (moveTo) {
MoveTo.Previous -> currentPage - 1
MoveTo.Next -> currentPage + 1
}
state.animateScrollToPage(page)
}
.launchIn(this)
}
LaunchedEffect(state.currentPage) {
if (state.currentPage != currentPage) {
progress(state.currentPage)
}
}
if (direction == Direction.Down || direction == Direction.Up) {
VerticalPager(state, reverseLayout = direction == Direction.Up) {
HandlePager(
pages,
it,
previousChapter,
currentChapter,
nextChapter,
pageModifier,
retry
)
}
} else {
HorizontalPager(state, reverseLayout = direction == Direction.Left) {
HandlePager(
pages,
it,
previousChapter,
currentChapter,
nextChapter,
pageModifier,
retry
)
}
}
}
@Composable
fun HandlePager(
pages: List<ReaderPage>,
page: Int,
previousChapter: ReaderChapter?,
currentChapter: ReaderChapter,
nextChapter: ReaderChapter?,
pageModifier: Modifier,
retry: (ReaderPage) -> Unit,
) {
when (page) {
0 -> ChapterSeperator(previousChapter, currentChapter)
pages.size -> ChapterSeperator(currentChapter, nextChapter)
else -> {
val image = pages[page - 1]
ReaderImage(
image.index,
image.bitmap.collectAsState().value,
image.status.collectAsState().value,
image.error.collectAsState().value,
loadingModifier = pageModifier,
retry = { pageIndex ->
pages.find { it.index == pageIndex }?.let { retry(it) }
}
)
}
}
}