[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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
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.changedToUpIgnoreConsumed
|
||||||
import androidx.compose.ui.input.pointer.consumePositionChange
|
import androidx.compose.ui.input.pointer.consumePositionChange
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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 androidx.compose.ui.util.fastFirstOrNull
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.theme.LightBlue300
|
import xyz.quaver.pupil.ui.theme.LightBlue300
|
||||||
@@ -69,10 +72,11 @@ fun OverscrollPager(
|
|||||||
|
|
||||||
var overscroll: Float? by remember { mutableStateOf(null) }
|
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 topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) circleRadius else 0f)
|
||||||
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) screenWidth.toFloat() else 0f)
|
val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) circleRadius else 0f)
|
||||||
|
|
||||||
val prevPageTurnIndicatorOffsetPx = LocalDensity.current.run { prevPageTurnIndicatorOffset.toPx() }
|
val prevPageTurnIndicatorOffsetPx = LocalDensity.current.run { prevPageTurnIndicatorOffset.toPx() }
|
||||||
val nextPageTurnIndicatorOffsetPx = LocalDensity.current.run { nextPageTurnIndicatorOffset.toPx() }
|
val nextPageTurnIndicatorOffsetPx = LocalDensity.current.run { nextPageTurnIndicatorOffset.toPx() }
|
||||||
@@ -96,7 +100,11 @@ fun OverscrollPager(
|
|||||||
if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
Box(
|
||||||
|
Modifier.onGloballyPositioned {
|
||||||
|
size = it.size.toSize()
|
||||||
|
}
|
||||||
|
) {
|
||||||
overscroll?.let { overscroll ->
|
overscroll?.let { overscroll ->
|
||||||
if (overscroll > 0f)
|
if (overscroll > 0f)
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -469,6 +469,14 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.NavigateBefore,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton({ }) {
|
IconButton({ }) {
|
||||||
Image(
|
Image(
|
||||||
@@ -564,6 +572,7 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.List,
|
Icons.Default.List,
|
||||||
|
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -581,29 +590,104 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Recent(navController: NavController) {
|
fun Recent(navController: NavController) {
|
||||||
Scaffold(
|
val model: RecentViewModel = viewModel()
|
||||||
topBar = {
|
val coroutineScope = rememberCoroutineScope()
|
||||||
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)) {
|
|
||||||
|
|
||||||
|
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
|
modifier = Modifier
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
searchFocused = it.isFocused
|
searchFocused = it.isFocused
|
||||||
}.fillMaxWidth(),
|
}
|
||||||
|
.fillMaxWidth(),
|
||||||
onValueChange = { model.stx = it },
|
onValueChange = { model.stx = it },
|
||||||
placeholder = { Text("제목") },
|
placeholder = { Text("제목") },
|
||||||
textStyle = MaterialTheme.typography.subtitle1,
|
textStyle = MaterialTheme.typography.subtitle1,
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import kotlinx.serialization.Serializable
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
val manatokiUrl = "https://manatoki118.net"
|
||||||
|
|
||||||
private val rateLimitCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val rateLimitCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
private val rateLimiter = RateLimiter.create(10.0)
|
private val rateLimiter = RateLimiter.create(10.0)
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ suspend fun HttpClient.getItem(
|
|||||||
} else {
|
} else {
|
||||||
runCatching {
|
runCatching {
|
||||||
waitForRateLimit()
|
waitForRateLimit()
|
||||||
val content: String = get("https://manatoki116.net/comic/$itemID")
|
val content: String = get("$manatokiUrl/comic/$itemID")
|
||||||
|
|
||||||
val doc = Jsoup.parse(content)
|
val doc = Jsoup.parse(content)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.kodein.di.instance
|
|||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
||||||
|
import xyz.quaver.pupil.sources.manatoki.manatokiUrl
|
||||||
import xyz.quaver.pupil.sources.manatoki.waitForRateLimit
|
import xyz.quaver.pupil.sources.manatoki.waitForRateLimit
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -68,7 +69,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
loadJob = launch {
|
loadJob = launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
waitForRateLimit()
|
waitForRateLimit()
|
||||||
val doc = Jsoup.parse(client.get("https://manatoki116.net/"))
|
val doc = Jsoup.parse(client.get(manatokiUrl))
|
||||||
|
|
||||||
yield()
|
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.di.instance
|
||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
|
import xyz.quaver.pupil.sources.manatoki.manatokiUrl
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -157,7 +158,7 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
|
|
||||||
searchJob = launch {
|
searchJob = launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
val urlBuilder = StringBuilder("https://manatoki116.net/comic")
|
val urlBuilder = StringBuilder("$manatokiUrl/comic")
|
||||||
|
|
||||||
if (page != 1) urlBuilder.append("/p$page")
|
if (page != 1) urlBuilder.append("/p$page")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user