From 71ce61cc33fa900343da9ca568359df697ee3c88 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 27 May 2021 19:59:52 -0400 Subject: [PATCH] Reader improvements, hotkey support, mark as read when finished, a lot more --- .../kotlin/ca/gosyer/ui/reader/ReaderMenu.kt | 169 ++++++------------ .../gosyer/ui/reader/ReaderMenuViewModel.kt | 106 +++++++---- .../ca/gosyer/ui/reader/model/MoveTo.kt | 12 ++ .../ca/gosyer/ui/reader/viewer/Continuous.kt | 82 +++++++++ .../ca/gosyer/ui/reader/viewer/Pager.kt | 113 ++++++++++++ 5 files changed, 329 insertions(+), 153 deletions(-) create mode 100644 src/main/kotlin/ca/gosyer/ui/reader/model/MoveTo.kt create mode 100644 src/main/kotlin/ca/gosyer/ui/reader/viewer/Continuous.kt create mode 100644 src/main/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt diff --git a/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt b/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt index 0b76e079..1a2abd48 100644 --- a/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt @@ -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 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, - 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, - 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, - 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) } - } - ) - } - } -} diff --git a/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt index 823a031c..10ef41c6 100644 --- a/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt @@ -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>() + 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) diff --git a/src/main/kotlin/ca/gosyer/ui/reader/model/MoveTo.kt b/src/main/kotlin/ca/gosyer/ui/reader/model/MoveTo.kt new file mode 100644 index 00000000..41e8f505 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/reader/model/MoveTo.kt @@ -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 +} diff --git a/src/main/kotlin/ca/gosyer/ui/reader/viewer/Continuous.kt b/src/main/kotlin/ca/gosyer/ui/reader/viewer/Continuous.kt new file mode 100644 index 00000000..7b027f83 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/reader/viewer/Continuous.kt @@ -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, + previousChapter: ReaderChapter?, + currentChapter: ReaderChapter, + nextChapter: ReaderChapter?, + pageModifier: Modifier, + pageEmitter: SharedFlow>, + 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) + } + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt b/src/main/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt new file mode 100644 index 00000000..200875b8 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/reader/viewer/Pager.kt @@ -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, + previousChapter: ReaderChapter?, + currentChapter: ReaderChapter, + nextChapter: ReaderChapter?, + pageModifier: Modifier, + pageEmitter: SharedFlow>, + 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, + 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) } + } + ) + } + } +}