Page previews for Exh/E-H and NH
- Still needs click image to open chapter
This commit is contained in:
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user