diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt
index 2463d2ee..8e554a17 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt
@@ -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(
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
index 1f43aaca..6a0d8104 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
@@ -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,
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
index fedf819d..281d185f 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
@@ -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)
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
index c5af8f31..776e19dd 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
@@ -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()
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt
new file mode 100644
index 00000000..b7b2833f
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt
@@ -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 .
+ */
+
+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()
+
+ 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
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt
index d9b45ac3..451bd4c3 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt
@@ -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")