[Manatoki] Implemented Recent page
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user