Page previews for Exh/E-H and NH

- Still needs click image to open chapter
This commit is contained in:
Jobobby04
2022-07-16 16:44:55 -04:00
parent 36461b52c0
commit 67e190bffd
22 changed files with 1446 additions and 9 deletions
@@ -0,0 +1,36 @@
package exh.pagepreview
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.core.os.bundleOf
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import exh.pagepreview.components.PagePreviewScreen
class PagePreviewController : FullComposeController<PagePreviewPresenter> {
@Suppress("unused")
constructor(bundle: Bundle? = null) : super(bundle)
constructor(mangaId: Long) : super(
bundleOf(MANGA_ID to mangaId),
)
override fun createPresenter() = PagePreviewPresenter(args.getLong(MANGA_ID, -1))
@Composable
override fun ComposeContent() {
PagePreviewScreen(
state = presenter.state.collectAsState().value,
pageDialogOpen = presenter.pageDialogOpen,
onPageSelected = presenter::moveToPage,
onOpenPageDialog = { presenter.pageDialogOpen = true },
onDismissPageDialog = { presenter.pageDialogOpen = false },
navigateUp = router::popCurrentController,
)
}
companion object {
const val MANGA_ID = "manga_id"
}
}
@@ -0,0 +1,103 @@
package exh.pagepreview
import android.os.Bundle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.GetPagePreviews
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PagePreviewPresenter(
private val mangaId: Long,
private val getPagePreviews: GetPagePreviews = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) : BasePresenter<PagePreviewController>() {
private val _state = MutableStateFlow<PagePreviewState>(PagePreviewState.Loading)
val state = _state.asStateFlow()
private val page = MutableStateFlow(1)
var pageDialogOpen by mutableStateOf(false)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
val manga = getManga.await(mangaId)!!
val source = sourceManager.getOrStub(manga.source)
page
.onEach { page ->
when (
val previews = getPagePreviews.await(manga, source, page)
) {
is GetPagePreviews.Result.Error -> _state.update {
PagePreviewState.Error(previews.error)
}
is GetPagePreviews.Result.Success -> _state.update {
when (it) {
PagePreviewState.Loading, is PagePreviewState.Error -> {
PagePreviewState.Success(
page,
previews.pagePreviews,
previews.hasNextPage,
previews.pageCount,
manga,
source,
)
}
is PagePreviewState.Success -> it.copy(
page = page,
pagePreviews = previews.pagePreviews,
hasNextPage = previews.hasNextPage,
pageCount = previews.pageCount,
).also { logcat { page.toString() } }
}
}
GetPagePreviews.Result.Unused -> Unit
}
}
.catch { e ->
_state.update {
PagePreviewState.Error(e)
}
}
.collect()
}
}
fun moveToPage(page: Int) {
this.page.value = page
}
}
sealed class PagePreviewState {
object Loading : PagePreviewState()
data class Success(
val page: Int,
val pagePreviews: List<PagePreview>,
val hasNextPage: Boolean,
val pageCount: Int?,
val manga: Manga,
val source: Source,
) : PagePreviewState()
data class Error(val error: Throwable) : PagePreviewState()
}
@@ -0,0 +1,209 @@
package exh.pagepreview.components
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateTo
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.UTurnRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Slider
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AroundLayout
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.manga.components.PagePreview
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.logcat
import exh.pagepreview.PagePreviewState
import exh.util.floor
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun PagePreviewScreen(
state: PagePreviewState,
pageDialogOpen: Boolean,
onPageSelected: (Int) -> Unit,
onOpenPageDialog: () -> Unit,
onDismissPageDialog: () -> Unit,
navigateUp: () -> Unit,
) {
val topAppBarScrollState = rememberTopAppBarScrollState()
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState)
Scaffold(
modifier = Modifier
.statusBarsPadding()
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
topBar = {
PagePreviewTopAppBar(
topAppBarScrollBehavior = topAppBarScrollBehavior,
navigateUp = navigateUp,
title = stringResource(id = R.string.page_previews),
onOpenPageDialog = onOpenPageDialog,
showOpenPageDialog = state is PagePreviewState.Success &&
(state.pageCount != null && state.pageCount > 1 /* TODO support unknown pageCount || state.hasNextPage*/),
)
},
) { paddingValues ->
when (state) {
is PagePreviewState.Error -> EmptyScreen(state.error.message.orEmpty())
PagePreviewState.Loading -> LoadingScreen()
is PagePreviewState.Success -> {
BoxWithConstraints(Modifier.fillMaxSize()) {
val itemPerRowCount by derivedStateOf { (maxWidth / 120.dp).floor() }
val items by derivedStateOf { state.pagePreviews.chunked(itemPerRowCount) }
SideEffect {
logcat { (items.hashCode() to state.page).toString() }
}
ScrollbarLazyColumn(
modifier = Modifier,
contentPadding = paddingValues + topPaddingValues,
) {
items.forEach {
item {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
it.forEach { page ->
PagePreview(
modifier = Modifier.weight(1F),
page = page,
)
}
}
}
}
}
}
}
}
}
if (pageDialogOpen && state is PagePreviewState.Success) {
PagePreviewPageDialog(
currentPage = state.page,
pageCount = state.pageCount!!,
onDismissPageDialog = onDismissPageDialog,
onPageSelected = onPageSelected,
)
}
}
@Composable
fun PagePreviewPageDialog(
currentPage: Int,
pageCount: Int,
onDismissPageDialog: () -> Unit,
onPageSelected: (Int) -> Unit,
) {
var page by remember(currentPage) {
mutableStateOf(currentPage.toFloat())
}
val scope = rememberCoroutineScope()
AlertDialog(
onDismissRequest = onDismissPageDialog,
confirmButton = {
TextButton(onClick = {
onPageSelected(page.roundToInt())
onDismissPageDialog()
},) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissPageDialog) {
Text(stringResource(android.R.string.cancel))
}
},
title = {
Text(stringResource(R.string.page_preview_page_go_to))
},
text = {
AroundLayout(
startLayout = { Text(text = page.roundToInt().toString()) },
endLayout = { Text(text = pageCount.toString()) },
) {
Slider(
modifier = Modifier.fillMaxWidth(),
value = page,
onValueChange = { page = it },
onValueChangeFinished = {
scope.launch {
val newPage = page
AnimationState(
newPage,
).animateTo(newPage.roundToInt().toFloat()) {
page = value
}
}
},
valueRange = 1F..pageCount.toFloat(),
)
}
},
)
}
@Composable
fun PagePreviewTopAppBar(
topAppBarScrollBehavior: TopAppBarScrollBehavior,
navigateUp: () -> Unit,
title: String,
onOpenPageDialog: () -> Unit,
showOpenPageDialog: Boolean,
) {
SmallTopAppBar(
navigationIcon = {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
},
title = {
Text(text = title)
},
scrollBehavior = topAppBarScrollBehavior,
actions = {
if (showOpenPageDialog) {
IconButton(onClick = onOpenPageDialog) {
Icon(
imageVector = Icons.Default.UTurnRight,
contentDescription = stringResource(R.string.page_preview_page_go_to),
)
}
}
},
)
}