[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.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(

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()

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.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")