[Manatoki] Implemented Recent page

This commit is contained in:
tom5079
2021-12-26 10:09:41 +09:00
parent 480bbd3628
commit 84c536a597
6 changed files with 208 additions and 30 deletions

View File

@@ -33,6 +33,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -40,12 +41,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastFirstOrNull
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.theme.LightBlue300
@@ -69,10 +72,11 @@ fun OverscrollPager(
var overscroll: Float? by remember { mutableStateOf(null) }
val screenWidth = LocalConfiguration.current.screenWidthDp
var size: Size? by remember { mutableStateOf(null) }
val circleRadius = (size?.width ?: 0f) / 2
val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) screenWidth.toFloat() else 0f)
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) screenWidth.toFloat() else 0f)
val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) circleRadius else 0f)
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) circleRadius else 0f)
val prevPageTurnIndicatorOffsetPx = LocalDensity.current.run { prevPageTurnIndicatorOffset.toPx() }
val nextPageTurnIndicatorOffsetPx = LocalDensity.current.run { nextPageTurnIndicatorOffset.toPx() }
@@ -96,7 +100,11 @@ fun OverscrollPager(
if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
Box {
Box(
Modifier.onGloballyPositioned {
size = it.size.toSize()
}
) {
overscroll?.let { overscroll ->
if (overscroll > 0f)
Row(

View File

@@ -469,6 +469,14 @@ class Manatoki(app: Application) : Source(), DIAware {
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
actions = {
IconButton({ }) {
Image(
@@ -564,6 +572,7 @@ class Manatoki(app: Application) : Source(), DIAware {
) {
Icon(
Icons.Default.List,
contentDescription = null
)
}
@@ -581,29 +590,104 @@ class Manatoki(app: Application) : Source(), DIAware {
@Composable
fun Recent(navController: NavController) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("최신 업데이트")
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
val model: RecentViewModel = viewModel()
val coroutineScope = rememberCoroutineScope()
var mangaListing: MangaListing? by rememberSaveable {mutableStateOf(null) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) {
model.load()
}
BackHandler {
if (state.isVisible) coroutineScope.launch { state.hide() }
else navController.popBackStack()
}
ModalBottomSheetLayout(
sheetState = state,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onReader = {
launch {
state.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${it.itemID}")
}
})
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("최신 업데이트")
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
OverscrollPager(
currentPage = model.page,
prevPageAvailable = model.page > 1,
nextPageAvailable = model.page < 10,
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding(),
onPageTurn = {
model.page = it
model.load()
}
) {
Box(Modifier.fillMaxSize()) {
LazyVerticalGrid(
GridCells.Adaptive(minSize = 200.dp),
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
)
) {
items(model.result) {
Thumbnail(
it,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4)
.padding(8.dp)
) {
coroutineScope.launch {
mangaListing = null
state.show()
}
coroutineScope.launch {
client.getItem(it, onListing = {
mangaListing = it
})
}
}
}
}
if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
}
@@ -666,7 +750,8 @@ class Manatoki(app: Application) : Source(), DIAware {
modifier = Modifier
.onFocusChanged {
searchFocused = it.isFocused
}.fillMaxWidth(),
}
.fillMaxWidth(),
onValueChange = { model.stx = it },
placeholder = { Text("제목") },
textStyle = MaterialTheme.typography.subtitle1,

View File

@@ -41,6 +41,8 @@ import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.util.concurrent.Executors
val manatokiUrl = "https://manatoki118.net"
private val rateLimitCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val rateLimiter = RateLimiter.create(10.0)
@@ -118,7 +120,7 @@ suspend fun HttpClient.getItem(
} else {
runCatching {
waitForRateLimit()
val content: String = get("https://manatoki116.net/comic/$itemID")
val content: String = get("$manatokiUrl/comic/$itemID")
val doc = Jsoup.parse(content)

View File

@@ -36,6 +36,7 @@ import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
import xyz.quaver.pupil.sources.manatoki.manatokiUrl
import xyz.quaver.pupil.sources.manatoki.waitForRateLimit
@Serializable
@@ -68,7 +69,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
loadJob = launch {
runCatching {
waitForRateLimit()
val doc = Jsoup.parse(client.get("https://manatoki116.net/"))
val doc = Jsoup.parse(client.get(manatokiUrl))
yield()

View File

@@ -0,0 +1,81 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.sources.manatoki.viewmodel
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
import xyz.quaver.pupil.sources.manatoki.manatokiUrl
class RecentViewModel(app: Application): AndroidViewModel(app), DIAware {
override val di by closestDI(app)
private val client: HttpClient by instance()
var page by mutableStateOf(1)
var loading by mutableStateOf(false)
private set
var error by mutableStateOf(false)
private set
val result = mutableStateListOf<Thumbnail>()
private var loadJob: Job? = null
fun load() {
viewModelScope.launch {
loadJob?.cancelAndJoin()
result.clear()
loading = true
loadJob = launch {
runCatching {
val doc = Jsoup.parse(client.get("$manatokiUrl/bbs/page.php?hid=update&page=$page"))
doc.getElementsByClass("post-list").forEach {
val (itemID, title) = it.selectFirst(".post-subject > a")!!.let {
it.attr("href").takeLastWhile { it != '/' } to it.ownText()
}
val thumbnail = it.getElementsByTag("img").attr("src")
loading = false
result.add(Thumbnail(itemID, title, thumbnail))
}
}.onFailure {
loading = false
error = true
}
}
}
}
}

View File

@@ -37,6 +37,7 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.pupil.sources.manatoki.manatokiUrl
@Parcelize
@Serializable
@@ -157,7 +158,7 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
searchJob = launch {
runCatching {
val urlBuilder = StringBuilder("https://manatoki116.net/comic")
val urlBuilder = StringBuilder("$manatokiUrl/comic")
if (page != 1) urlBuilder.append("/p$page")