From 2e11a4907a914a542f60108b49eca16b37b12569 Mon Sep 17 00:00:00 2001 From: tom5079 Date: Thu, 30 Dec 2021 13:00:22 +0900 Subject: [PATCH] Prepare to export sources --- app/build.gradle.kts | 2 +- app/release/output-metadata.json | 2 +- .../sources/{Common.kt => SourceLoader.kt} | 0 .../pupil/sources/composable/ReaderBase.kt | 5 +- .../quaver/pupil/sources/hitomi/Database.kt | 2 +- .../quaver/pupil/sources/manatoki/Database.kt | 18 +- .../pupil/sources/manatoki/composable/Main.kt | 167 ++++++++++++++++-- .../composable/MangaListingBottomSheet.kt | 35 ++-- .../sources/manatoki/composable/Reader.kt | 53 +++--- .../sources/manatoki/composable/Recent.kt | 145 +++++++-------- .../sources/manatoki/composable/Search.kt | 105 ++++++++--- .../xyz/quaver/pupil/sources/manatoki/util.kt | 4 +- .../manatoki/viewmodel/MainViewModel.kt | 2 + buildSrc/src/main/kotlin/Config.kt | 2 +- 14 files changed, 371 insertions(+), 171 deletions(-) rename app/src/main/java/xyz/quaver/pupil/sources/{Common.kt => SourceLoader.kt} (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 231cc6f7..eaeaa25b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { minSdk = 21 targetSdk = 31 versionCode = 600 - versionName = "6.0.0-alpha1" + versionName = VERSION testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 94234ee4..f3f6a6ea 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 600, - "versionName": "6.0.0-alpha1", + "versionName": "6.0.0-alpha02", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt similarity index 100% rename from app/src/main/java/xyz/quaver/pupil/sources/Common.kt rename to app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt index a866f593..2d144ad4 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt @@ -243,7 +243,10 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar totalProgress++ } } - else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'") + else -> { + logger.warning(IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")) + progressList[index] = Float.NEGATIVE_INFINITY + } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt index 42f006d9..43990fd0 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt @@ -44,7 +44,7 @@ interface FavoritesDao { suspend fun delete(item: String) = delete(Favorite(item)) } -@Database(entities = [Favorite::class], version = 1) +@Database(entities = [Favorite::class], version = 1, exportSchema = false) abstract class HitomiDatabase : RoomDatabase() { abstract fun favoritesDao(): FavoritesDao } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt index e150823d..2280970d 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt @@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources.manatoki import androidx.room.* import kotlinx.coroutines.flow.Flow +import java.sql.Timestamp @Entity data class Favorite( @@ -35,7 +36,9 @@ data class Bookmark( @Entity data class History( @PrimaryKey val itemID: String, - val page: Int + val parent: String, + val page: Int, + val timestamp: Long = System.currentTimeMillis() ) @Dao @@ -59,10 +62,21 @@ interface BookmarkDao { @Dao interface HistoryDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(history: History) + suspend fun insert(itemID: String, parent: String, page: Int) = insert(History(itemID, parent, page)) + @Query("DELETE FROM history WHERE itemID = :itemID") + suspend fun delete(itemID: String) + + @Query("SELECT parent FROM (SELECT parent, max(timestamp) as t FROM history GROUP BY parent) ORDER BY t DESC") + fun getRecentManga(): Flow> + + @Query("SELECT itemID FROM history WHERE parent = :parent ORDER BY timestamp DESC") + suspend fun getAll(parent: String): List } -@Database(entities = [Favorite::class, Bookmark::class, History::class], version = 1) +@Database(entities = [Favorite::class, Bookmark::class, History::class], version = 1, exportSchema = false) abstract class ManatokiDatabase: RoomDatabase() { abstract fun favoriteDao(): FavoriteDao abstract fun bookmarkDao(): BookmarkDao diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt index 60e5a645..bc1d188d 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt @@ -20,9 +20,13 @@ package xyz.quaver.pupil.sources.manatoki.composable import androidx.activity.compose.BackHandler import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -33,8 +37,11 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable 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.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -46,12 +53,15 @@ import com.google.accompanist.insets.rememberInsetsPaddingValues import com.google.accompanist.insets.ui.Scaffold import com.google.accompanist.insets.ui.TopAppBar import io.ktor.client.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberViewModel import xyz.quaver.pupil.R import xyz.quaver.pupil.proto.settingsDataStore import xyz.quaver.pupil.sources.composable.SourceSelectDialog +import xyz.quaver.pupil.sources.manatoki.ManatokiDatabase import xyz.quaver.pupil.sources.manatoki.MangaListing import xyz.quaver.pupil.sources.manatoki.ReaderInfo import xyz.quaver.pupil.sources.manatoki.getItem @@ -64,15 +74,28 @@ fun Main(navController: NavController) { val client: HttpClient by rememberInstance() + val database: ManatokiDatabase by rememberInstance() + val historyDao = remember { database.historyDao() } + val recent by remember { historyDao.getRecentManga() }.collectAsState(emptyList()) + val recentManga = remember { mutableStateListOf() } + + LaunchedEffect(recent) { + recentManga.clear() + + recent.forEach { + if (isActive) + client.getItem(it, onListing = { + recentManga.add( + Thumbnail(it.itemID, it.title, it.thumbnail) + ) + }) + } + } + val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) - var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } val coroutineScope = rememberCoroutineScope() - val onListing: (MangaListing) -> Unit = { - mangaListing = it - } - val context = LocalContext.current LaunchedEffect(Unit) { context.settingsDataStore.updateData { @@ -82,13 +105,6 @@ fun Main(navController: NavController) { } } - val onReader: (ReaderInfo) -> Unit = { readerInfo -> - coroutineScope.launch { - sheetState.snapTo(ModalBottomSheetValue.Hidden) - navController.navigate("manatoki.net/reader/${readerInfo.itemID}") - } - } - var sourceSelectDialog by remember { mutableStateOf(false) } if (sourceSelectDialog) @@ -107,11 +123,86 @@ fun Main(navController: NavController) { } } + var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } + var recentItem: String? by rememberSaveable { mutableStateOf(null) } + val mangaListingListState = rememberLazyListState() + var mangaListingListSize: Size? by remember { mutableStateOf(null) } + val mangaListingInteractionSource = remember { mutableStateMapOf() } + val navigationBarsPadding = LocalDensity.current.run { + rememberInsetsPaddingValues( + LocalWindowInsets.current.navigationBars + ).calculateBottomPadding().toPx() + } + + val onListing: (MangaListing) -> Unit = { + mangaListing = it + + coroutineScope.launch { + val recentItemID = historyDao.getAll(it.itemID).firstOrNull() ?: return@launch + recentItem = recentItemID + + while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) { + delay(100) + } + + val interactionSource = mangaListingInteractionSource.getOrPut(recentItemID) { + MutableInteractionSource() + } + + val targetIndex = + it.entries.indexOfFirst { entry -> entry.itemID == recentItemID } + + mangaListingListState.scrollToItem(targetIndex) + + mangaListingListSize?.let { sheetSize -> + val targetItem = + mangaListingListState.layoutInfo.visibleItemsInfo.first { + it.key == recentItemID + } + + if (targetItem.offset == 0) { + mangaListingListState.animateScrollBy( + -(sheetSize.height - navigationBarsPadding - targetItem.size) + ) + } + + delay(200) + + with(interactionSource) { + val interaction = + PressInteraction.Press( + Offset( + sheetSize.width / 2, + targetItem.size / 2f + ) + ) + + + emit(interaction) + emit(PressInteraction.Release(interaction)) + } + } + } + } + + val onReader: (ReaderInfo) -> Unit = { readerInfo -> + coroutineScope.launch { + sheetState.snapTo(ModalBottomSheetValue.Hidden) + navController.navigate("manatoki.net/reader/${readerInfo.itemID}") + } + } + ModalBottomSheetLayout( sheetState = sheetState, sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp), sheetContent = { - MangaListingBottomSheet(mangaListing) { + MangaListingBottomSheet( + mangaListing, + onListSize = { mangaListingListSize = it }, + rippleInteractionSource = mangaListingInteractionSource, + listState = mangaListingListState, + recentItem = recentItem + ) { coroutineScope.launch { client.getItem(it, onListing, onReader) } @@ -164,6 +255,50 @@ fun Main(navController: NavController) { .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + if (recentManga.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "이어 보기", + style = MaterialTheme.typography.h5 + ) + + IconButton(onClick = { navController.navigate("manatoki.net/recent") }) { + Icon( + Icons.Default.Add, + contentDescription = null + ) + } + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .height(210.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(recentManga) { item -> + Thumbnail( + item, + Modifier + .width(180.dp) + .aspectRatio(6 / 7f) + ) { + coroutineScope.launch { + mangaListing = null + sheetState.animateTo(ModalBottomSheetValue.Expanded) + } + coroutineScope.launch { + client.getItem(it, onListing, onReader) + } + } + } + } + } + Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -195,7 +330,7 @@ fun Main(navController: NavController) { .aspectRatio(6 / 7f)) { coroutineScope.launch { mangaListing = null - sheetState.show() + sheetState.animateTo(ModalBottomSheetValue.Expanded) } coroutineScope.launch { client.getItem(it, onListing, onReader) @@ -254,7 +389,7 @@ fun Main(navController: NavController) { .aspectRatio(6f / 7)) { coroutineScope.launch { mangaListing = null - sheetState.show() + sheetState.animateTo(ModalBottomSheetValue.Expanded) } coroutineScope.launch { client.getItem(it, onListing, onReader) @@ -273,7 +408,7 @@ fun Main(navController: NavController) { modifier = Modifier.clickable { coroutineScope.launch { mangaListing = null - sheetState.show() + sheetState.animateTo(ModalBottomSheetValue.Expanded) } coroutineScope.launch { diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt index b25f26c2..494695d1 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt @@ -18,14 +18,16 @@ package xyz.quaver.pupil.sources.manatoki.composable -import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowRight @@ -39,7 +41,6 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import coil.compose.rememberImagePainter @@ -50,7 +51,7 @@ import com.google.accompanist.insets.rememberInsetsPaddingValues import xyz.quaver.pupil.sources.manatoki.MangaListing private val FabSpacing = 8.dp -private val HeightPercentage = 75 // take 60% of the available space +private val HeightPercentage = 75 // take 75% of the available space private enum class MangaListingBottomSheetLayoutContent { Top, Bottom, Fab } @Composable @@ -107,7 +108,9 @@ fun MangaListingBottomSheet( currentItemID: String? = null, onListSize: (Size) -> Unit = { }, listState: LazyListState = rememberLazyListState(), - rippleInteractionSource: List = emptyList(), + rippleInteractionSource: Map = emptyMap(), + recentItem: String? = null, + nextItem: String? = null, onOpenItem: (String) -> Unit = { }, ) { val coroutineScope = rememberCoroutineScope() @@ -125,9 +128,19 @@ fun MangaListingBottomSheet( MangaListingBottomSheetLayout( floatingActionButton = { ExtendedFloatingActionButton( - text = { Text("첫화보기") }, + text = { Text( + when { + mangaListing.entries.any { it.itemID == recentItem } -> "이어보기" + mangaListing.entries.any { it.itemID == nextItem } -> "다음화보기" + else -> "첫화보기" + } + ) }, onClick = { - mangaListing.entries.lastOrNull()?.let { onOpenItem(it.itemID) } + when { + mangaListing.entries.any { it.itemID == recentItem } -> onOpenItem(recentItem!!) + mangaListing.entries.any { it.itemID == nextItem } -> onOpenItem(nextItem!!) + else -> mangaListing.entries.lastOrNull()?.let { onOpenItem(it.itemID) } + } } ) }, @@ -216,11 +229,9 @@ fun MangaListingBottomSheet( onOpenItem(entry.itemID) } .run { - rippleInteractionSource - .getOrNull(index) - ?.let { - indication(it, rememberRipple()) - } ?: this + rippleInteractionSource[entry.itemID]?.let { + indication(it, rememberRipple()) + } ?: this } .padding(16.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt index dbf5ffd3..e37cc522 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt @@ -18,6 +18,7 @@ package xyz.quaver.pupil.sources.manatoki.composable +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateFloatAsState @@ -82,6 +83,7 @@ fun Reader(navController: NavController) { val database: ManatokiDatabase by rememberInstance() val favoriteDao = remember { database.favoriteDao() } val bookmarkDao = remember { database.bookmarkDao() } + val historyDao = remember { database.historyDao() } val coroutineScope = rememberCoroutineScope() @@ -91,6 +93,9 @@ fun Reader(navController: NavController) { LaunchedEffect(Unit) { if (itemID != null) client.getItem(itemID, onReader = { + coroutineScope.launch { + historyDao.insert(it.itemID, it.listingItemID, 1) + } readerInfo = it model.load(it.urls) { set("User-Agent", imageUserAgent) @@ -102,17 +107,24 @@ fun Reader(navController: NavController) { val isFavorite by favoriteDao.contains(itemID ?: "").collectAsState(false) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) - var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } - val mangaListingRippleInteractionSource = remember { mutableStateListOf() } val navigationBarsPadding = LocalDensity.current.run { rememberInsetsPaddingValues( LocalWindowInsets.current.navigationBars ).calculateBottomPadding().toPx() } - val bottomSheetListState = rememberLazyListState() val readerListState = rememberLazyListState() + LaunchedEffect(readerListState.firstVisibleItemIndex) { + readerInfo?.let { readerInfo -> + historyDao.insert( + readerInfo.itemID, + readerInfo.listingItemID, + readerListState.firstVisibleItemIndex + ) + } + } + var scrollDirection by remember { mutableStateOf(0f) } BackHandler { @@ -123,7 +135,10 @@ fun Reader(navController: NavController) { } } + var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } + val mangaListingListState = rememberLazyListState() var mangaListingListSize: Size? by remember { mutableStateOf(null) } + val mangaListingRippleInteractionSource = remember { MutableInteractionSource() } ModalBottomSheetLayout( sheetState = sheetState, @@ -132,11 +147,10 @@ fun Reader(navController: NavController) { MangaListingBottomSheet( mangaListing, currentItemID = itemID, - onListSize = { - mangaListingListSize = it - }, - rippleInteractionSource = mangaListingRippleInteractionSource, - listState = bottomSheetListState + onListSize = { mangaListingListSize = it }, + rippleInteractionSource = if (itemID == null) emptyMap() else mapOf(itemID to mangaListingRippleInteractionSource), + listState = mangaListingListState, + nextItem = readerInfo?.nextItemID ) { navController.navigate("manatoki.net/reader/$it") { popUpTo("manatoki.net/") @@ -214,7 +228,7 @@ fun Reader(navController: NavController) { } } else { coroutineScope.launch { - sheetState.show() + sheetState.animateTo(ModalBottomSheetValue.Expanded) } coroutineScope.launch { @@ -222,42 +236,31 @@ fun Reader(navController: NavController) { client.getItem(it.listingItemID, onListing = { mangaListing = it - mangaListingRippleInteractionSource.addAll( - List( - max( - it.entries.size - mangaListingRippleInteractionSource.size, - 0 - ) - ) { - MutableInteractionSource() - } - ) - coroutineScope.launch { - while (bottomSheetListState.layoutInfo.totalItemsCount != it.entries.size) { + while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) { delay(100) } val targetIndex = it.entries.indexOfFirst { it.itemID == itemID } - bottomSheetListState.scrollToItem(targetIndex) + mangaListingListState.scrollToItem(targetIndex) mangaListingListSize?.let { sheetSize -> val targetItem = - bottomSheetListState.layoutInfo.visibleItemsInfo.first { + mangaListingListState.layoutInfo.visibleItemsInfo.first { it.key == itemID } if (targetItem.offset == 0) { - bottomSheetListState.animateScrollBy( + mangaListingListState.animateScrollBy( -(sheetSize.height - navigationBarsPadding - targetItem.size) ) } delay(200) - with(mangaListingRippleInteractionSource[targetIndex]) { + with(mangaListingRippleInteractionSource) { val interaction = PressInteraction.Press( Offset( diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt index 92eb5c19..7be0e7d7 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt @@ -24,16 +24,15 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.NavigateBefore -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.rememberInsetsPaddingValues @@ -44,7 +43,6 @@ import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberViewModel import xyz.quaver.pupil.sources.composable.OverscrollPager -import xyz.quaver.pupil.sources.manatoki.MangaListing import xyz.quaver.pupil.sources.manatoki.getItem import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel @@ -58,99 +56,74 @@ fun Recent(navController: NavController) { 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() + 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() + Scaffold( + topBar = { + TopAppBar( + title = { + Text("최신 업데이트") + }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + Icons.Default.NavigateBefore, + contentDescription = null + ) } - ) { - 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 - }) - } + }, + 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 { + client.getItem(it, onReader = { + navController.navigate("manatoki.net/reader/${it.itemID}") + }) } } } - - if (model.loading) - CircularProgressIndicator(Modifier.align(Alignment.Center)) } + + if (model.loading) + CircularProgressIndicator(Modifier.align(Alignment.Center)) } } } diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt index 27599553..e990f7f2 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt @@ -22,12 +22,12 @@ import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.GridCells -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyVerticalGrid -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -42,8 +42,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp @@ -56,15 +59,14 @@ import com.google.accompanist.insets.rememberInsetsPaddingValues import com.google.accompanist.insets.ui.Scaffold import com.google.accompanist.insets.ui.TopAppBar import io.ktor.client.* +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberViewModel import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout import xyz.quaver.pupil.sources.composable.ModalTopSheetState import xyz.quaver.pupil.sources.composable.OverscrollPager -import xyz.quaver.pupil.sources.manatoki.Chip -import xyz.quaver.pupil.sources.manatoki.MangaListing -import xyz.quaver.pupil.sources.manatoki.getItem +import xyz.quaver.pupil.sources.manatoki.* import xyz.quaver.pupil.sources.manatoki.viewmodel.* @ExperimentalFoundationApi @@ -75,14 +77,15 @@ fun Search(navController: NavController) { val client: HttpClient by rememberInstance() + val database: ManatokiDatabase by rememberInstance() + val historyDao = remember { database.historyDao() } + var searchFocused by remember { mutableStateOf(false) } val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp) val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) - var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } - val coroutineScope = rememberCoroutineScope() val focusManager = LocalFocusManager.current @@ -100,11 +103,28 @@ fun Search(navController: NavController) { } } + var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } + var recentItem: String? by rememberSaveable { mutableStateOf(null) } + val mangaListingListState = rememberLazyListState() + var mangaListingListSize: Size? by remember { mutableStateOf(null) } + val mangaListingInteractionSource = remember { mutableStateMapOf() } + val navigationBarsPadding = LocalDensity.current.run { + rememberInsetsPaddingValues( + LocalWindowInsets.current.navigationBars + ).calculateBottomPadding().toPx() + } + ModalBottomSheetLayout( sheetState = sheetState, sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp), sheetContent = { - MangaListingBottomSheet(mangaListing) { + MangaListingBottomSheet( + mangaListing, + onListSize = { mangaListingListSize = it }, + rippleInteractionSource = mangaListingInteractionSource, + listState = mangaListingListState, + recentItem = recentItem + ) { coroutineScope.launch { client.getItem(it, onReader = { launch { @@ -201,17 +221,6 @@ fun Search(navController: NavController) { .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - var expanded by remember { mutableStateOf(false) } - val suggestedArtists = remember(model.artist) { - if (model.artist.isEmpty()) - model.availableArtists - else - model - .availableArtists - .filter { it.contains(model.artist) } - .sortedBy { if (it.startsWith(model.artist)) 0 else 1 } - }.take(20) - Text("작가") TextField(model.artist, onValueChange = { model.artist = it }) @@ -266,7 +275,10 @@ fun Search(navController: NavController) { } } - Box(Modifier.fillMaxWidth().height(8.dp)) + Box( + Modifier + .fillMaxWidth() + .height(8.dp)) } } ) { @@ -301,11 +313,58 @@ fun Search(navController: NavController) { ) { coroutineScope.launch { mangaListing = null - sheetState.show() + sheetState.animateTo(ModalBottomSheetValue.Expanded) } coroutineScope.launch { client.getItem(it, onListing = { mangaListing = it + + coroutineScope.launch { + val recentItemID = historyDao.getAll(it.itemID).firstOrNull() ?: return@launch + recentItem = recentItemID + + while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) { + delay(100) + } + + val interactionSource = mangaListingInteractionSource.getOrPut(recentItemID) { + MutableInteractionSource() + } + + val targetIndex = + it.entries.indexOfFirst { entry -> entry.itemID == recentItemID } + + mangaListingListState.scrollToItem(targetIndex) + + mangaListingListSize?.let { sheetSize -> + val targetItem = + mangaListingListState.layoutInfo.visibleItemsInfo.first { + it.key == recentItemID + } + + if (targetItem.offset == 0) { + mangaListingListState.animateScrollBy( + -(sheetSize.height - navigationBarsPadding - targetItem.size) + ) + } + + delay(200) + + with(interactionSource) { + val interaction = + PressInteraction.Press( + Offset( + sheetSize.width / 2, + targetItem.size / 2f + ) + ) + + + emit(interaction) + emit(PressInteraction.Release(interaction)) + } + } + } }) } } 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 f6c0738c..f698bc68 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 @@ -103,7 +103,7 @@ fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) { } } -private val cache = LruCache(50) +private val cache = LruCache(100) suspend fun HttpClient.getItem( itemID: String, onListing: (MangaListing) -> Unit = { }, @@ -144,7 +144,7 @@ suspend fun HttpClient.getItem( }.toString() val urls = Jsoup.parse(htmlData) - .select("img[^data-]:not([style]):not([^class])") + .select("img[^data-]:not([style])") .map { it.attributes() .first { it.key.startsWith("data-") } 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 3325868b..c6c64cc7 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 @@ -19,6 +19,7 @@ package xyz.quaver.pupil.sources.manatoki.viewmodel import android.app.Application +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel @@ -36,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.HistoryDao import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail import xyz.quaver.pupil.sources.manatoki.manatokiUrl import xyz.quaver.pupil.sources.manatoki.waitForRateLimit diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 6b70126f..5d5e03c8 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -17,7 +17,7 @@ */ const val GROUP_ID = "xyz.quaver" -const val VERSION = "6.0.0-alpha01" +const val VERSION = "6.0.0-alpha02" object Versions { const val KOTLIN_VERSION = "1.5.31"