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