Prepare to export sources

This commit is contained in:
tom5079
2021-12-30 13:00:22 +09:00
parent 0e19d6c9b2
commit 2e11a4907a
14 changed files with 371 additions and 171 deletions

View File

@@ -20,7 +20,7 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 31 targetSdk = 31
versionCode = 600 versionCode = 600
versionName = "6.0.0-alpha1" versionName = VERSION
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 600, "versionCode": 600,
"versionName": "6.0.0-alpha1", "versionName": "6.0.0-alpha02",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -243,7 +243,10 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
totalProgress++ 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
}
} }
} }
} }

View File

@@ -44,7 +44,7 @@ interface FavoritesDao {
suspend fun delete(item: String) = delete(Favorite(item)) 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 class HitomiDatabase : RoomDatabase() {
abstract fun favoritesDao(): FavoritesDao abstract fun favoritesDao(): FavoritesDao
} }

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources.manatoki
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.sql.Timestamp
@Entity @Entity
data class Favorite( data class Favorite(
@@ -35,7 +36,9 @@ data class Bookmark(
@Entity @Entity
data class History( data class History(
@PrimaryKey val itemID: String, @PrimaryKey val itemID: String,
val page: Int val parent: String,
val page: Int,
val timestamp: Long = System.currentTimeMillis()
) )
@Dao @Dao
@@ -59,10 +62,21 @@ interface BookmarkDao {
@Dao @Dao
interface HistoryDao { 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<List<String>>
@Query("SELECT itemID FROM history WHERE parent = :parent ORDER BY timestamp DESC")
suspend fun getAll(parent: String): List<String>
} }
@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 class ManatokiDatabase: RoomDatabase() {
abstract fun favoriteDao(): FavoriteDao abstract fun favoriteDao(): FavoriteDao
abstract fun bookmarkDao(): BookmarkDao abstract fun bookmarkDao(): BookmarkDao

View File

@@ -20,9 +20,13 @@ package xyz.quaver.pupil.sources.manatoki.composable
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* 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.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -33,8 +37,11 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
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.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.Scaffold
import com.google.accompanist.insets.ui.TopAppBar import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.proto.settingsDataStore import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.sources.composable.SourceSelectDialog 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.MangaListing
import xyz.quaver.pupil.sources.manatoki.ReaderInfo import xyz.quaver.pupil.sources.manatoki.ReaderInfo
import xyz.quaver.pupil.sources.manatoki.getItem import xyz.quaver.pupil.sources.manatoki.getItem
@@ -64,15 +74,28 @@ fun Main(navController: NavController) {
val client: HttpClient by rememberInstance() 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<Thumbnail>() }
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) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val onListing: (MangaListing) -> Unit = {
mangaListing = it
}
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.settingsDataStore.updateData { 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) } var sourceSelectDialog by remember { mutableStateOf(false) }
if (sourceSelectDialog) 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<String, MutableInteractionSource>() }
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( ModalBottomSheetLayout(
sheetState = sheetState, sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp), sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = { sheetContent = {
MangaListingBottomSheet(mangaListing) { MangaListingBottomSheet(
mangaListing,
onListSize = { mangaListingListSize = it },
rippleInteractionSource = mangaListingInteractionSource,
listState = mangaListingListState,
recentItem = recentItem
) {
coroutineScope.launch { coroutineScope.launch {
client.getItem(it, onListing, onReader) client.getItem(it, onListing, onReader)
} }
@@ -164,6 +255,50 @@ fun Main(navController: NavController) {
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -195,7 +330,7 @@ fun Main(navController: NavController) {
.aspectRatio(6 / 7f)) { .aspectRatio(6 / 7f)) {
coroutineScope.launch { coroutineScope.launch {
mangaListing = null mangaListing = null
sheetState.show() sheetState.animateTo(ModalBottomSheetValue.Expanded)
} }
coroutineScope.launch { coroutineScope.launch {
client.getItem(it, onListing, onReader) client.getItem(it, onListing, onReader)
@@ -254,7 +389,7 @@ fun Main(navController: NavController) {
.aspectRatio(6f / 7)) { .aspectRatio(6f / 7)) {
coroutineScope.launch { coroutineScope.launch {
mangaListing = null mangaListing = null
sheetState.show() sheetState.animateTo(ModalBottomSheetValue.Expanded)
} }
coroutineScope.launch { coroutineScope.launch {
client.getItem(it, onListing, onReader) client.getItem(it, onListing, onReader)
@@ -273,7 +408,7 @@ fun Main(navController: NavController) {
modifier = Modifier.clickable { modifier = Modifier.clickable {
coroutineScope.launch { coroutineScope.launch {
mangaListing = null mangaListing = null
sheetState.show() sheetState.animateTo(ModalBottomSheetValue.Expanded)
} }
coroutineScope.launch { coroutineScope.launch {

View File

@@ -18,14 +18,16 @@
package xyz.quaver.pupil.sources.manatoki.composable package xyz.quaver.pupil.sources.manatoki.composable
import android.util.Log
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* 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.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowRight 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.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize import androidx.compose.ui.unit.toSize
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
@@ -50,7 +51,7 @@ import com.google.accompanist.insets.rememberInsetsPaddingValues
import xyz.quaver.pupil.sources.manatoki.MangaListing import xyz.quaver.pupil.sources.manatoki.MangaListing
private val FabSpacing = 8.dp 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 } private enum class MangaListingBottomSheetLayoutContent { Top, Bottom, Fab }
@Composable @Composable
@@ -107,7 +108,9 @@ fun MangaListingBottomSheet(
currentItemID: String? = null, currentItemID: String? = null,
onListSize: (Size) -> Unit = { }, onListSize: (Size) -> Unit = { },
listState: LazyListState = rememberLazyListState(), listState: LazyListState = rememberLazyListState(),
rippleInteractionSource: List<MutableInteractionSource> = emptyList(), rippleInteractionSource: Map<String, MutableInteractionSource> = emptyMap(),
recentItem: String? = null,
nextItem: String? = null,
onOpenItem: (String) -> Unit = { }, onOpenItem: (String) -> Unit = { },
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -125,9 +128,19 @@ fun MangaListingBottomSheet(
MangaListingBottomSheetLayout( MangaListingBottomSheetLayout(
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("첫화보기") }, text = { Text(
when {
mangaListing.entries.any { it.itemID == recentItem } -> "이어보기"
mangaListing.entries.any { it.itemID == nextItem } -> "다음화보기"
else -> "첫화보기"
}
) },
onClick = { 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) onOpenItem(entry.itemID)
} }
.run { .run {
rippleInteractionSource rippleInteractionSource[entry.itemID]?.let {
.getOrNull(index) indication(it, rememberRipple())
?.let { } ?: this
indication(it, rememberRipple())
} ?: this
} }
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.sources.manatoki.composable package xyz.quaver.pupil.sources.manatoki.composable
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -82,6 +83,7 @@ fun Reader(navController: NavController) {
val database: ManatokiDatabase by rememberInstance() val database: ManatokiDatabase by rememberInstance()
val favoriteDao = remember { database.favoriteDao() } val favoriteDao = remember { database.favoriteDao() }
val bookmarkDao = remember { database.bookmarkDao() } val bookmarkDao = remember { database.bookmarkDao() }
val historyDao = remember { database.historyDao() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -91,6 +93,9 @@ fun Reader(navController: NavController) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (itemID != null) if (itemID != null)
client.getItem(itemID, onReader = { client.getItem(itemID, onReader = {
coroutineScope.launch {
historyDao.insert(it.itemID, it.listingItemID, 1)
}
readerInfo = it readerInfo = it
model.load(it.urls) { model.load(it.urls) {
set("User-Agent", imageUserAgent) set("User-Agent", imageUserAgent)
@@ -102,17 +107,24 @@ fun Reader(navController: NavController) {
val isFavorite by favoriteDao.contains(itemID ?: "").collectAsState(false) val isFavorite by favoriteDao.contains(itemID ?: "").collectAsState(false)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val mangaListingRippleInteractionSource = remember { mutableStateListOf<MutableInteractionSource>() }
val navigationBarsPadding = LocalDensity.current.run { val navigationBarsPadding = LocalDensity.current.run {
rememberInsetsPaddingValues( rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars LocalWindowInsets.current.navigationBars
).calculateBottomPadding().toPx() ).calculateBottomPadding().toPx()
} }
val bottomSheetListState = rememberLazyListState()
val readerListState = rememberLazyListState() val readerListState = rememberLazyListState()
LaunchedEffect(readerListState.firstVisibleItemIndex) {
readerInfo?.let { readerInfo ->
historyDao.insert(
readerInfo.itemID,
readerInfo.listingItemID,
readerListState.firstVisibleItemIndex
)
}
}
var scrollDirection by remember { mutableStateOf(0f) } var scrollDirection by remember { mutableStateOf(0f) }
BackHandler { 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) } var mangaListingListSize: Size? by remember { mutableStateOf(null) }
val mangaListingRippleInteractionSource = remember { MutableInteractionSource() }
ModalBottomSheetLayout( ModalBottomSheetLayout(
sheetState = sheetState, sheetState = sheetState,
@@ -132,11 +147,10 @@ fun Reader(navController: NavController) {
MangaListingBottomSheet( MangaListingBottomSheet(
mangaListing, mangaListing,
currentItemID = itemID, currentItemID = itemID,
onListSize = { onListSize = { mangaListingListSize = it },
mangaListingListSize = it rippleInteractionSource = if (itemID == null) emptyMap() else mapOf(itemID to mangaListingRippleInteractionSource),
}, listState = mangaListingListState,
rippleInteractionSource = mangaListingRippleInteractionSource, nextItem = readerInfo?.nextItemID
listState = bottomSheetListState
) { ) {
navController.navigate("manatoki.net/reader/$it") { navController.navigate("manatoki.net/reader/$it") {
popUpTo("manatoki.net/") popUpTo("manatoki.net/")
@@ -214,7 +228,7 @@ fun Reader(navController: NavController) {
} }
} else { } else {
coroutineScope.launch { coroutineScope.launch {
sheetState.show() sheetState.animateTo(ModalBottomSheetValue.Expanded)
} }
coroutineScope.launch { coroutineScope.launch {
@@ -222,42 +236,31 @@ fun Reader(navController: NavController) {
client.getItem(it.listingItemID, onListing = { client.getItem(it.listingItemID, onListing = {
mangaListing = it mangaListing = it
mangaListingRippleInteractionSource.addAll(
List(
max(
it.entries.size - mangaListingRippleInteractionSource.size,
0
)
) {
MutableInteractionSource()
}
)
coroutineScope.launch { coroutineScope.launch {
while (bottomSheetListState.layoutInfo.totalItemsCount != it.entries.size) { while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) {
delay(100) delay(100)
} }
val targetIndex = val targetIndex =
it.entries.indexOfFirst { it.itemID == itemID } it.entries.indexOfFirst { it.itemID == itemID }
bottomSheetListState.scrollToItem(targetIndex) mangaListingListState.scrollToItem(targetIndex)
mangaListingListSize?.let { sheetSize -> mangaListingListSize?.let { sheetSize ->
val targetItem = val targetItem =
bottomSheetListState.layoutInfo.visibleItemsInfo.first { mangaListingListState.layoutInfo.visibleItemsInfo.first {
it.key == itemID it.key == itemID
} }
if (targetItem.offset == 0) { if (targetItem.offset == 0) {
bottomSheetListState.animateScrollBy( mangaListingListState.animateScrollBy(
-(sheetSize.height - navigationBarsPadding - targetItem.size) -(sheetSize.height - navigationBarsPadding - targetItem.size)
) )
} }
delay(200) delay(200)
with(mangaListingRippleInteractionSource[targetIndex]) { with(mangaListingRippleInteractionSource) {
val interaction = val interaction =
PressInteraction.Press( PressInteraction.Press(
Offset( Offset(

View File

@@ -24,16 +24,15 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.rememberInsetsPaddingValues 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.rememberInstance
import org.kodein.di.compose.rememberViewModel import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.sources.composable.OverscrollPager 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.getItem
import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
@@ -58,99 +56,74 @@ fun Recent(navController: NavController) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.load() model.load()
} }
BackHandler { BackHandler {
if (state.isVisible) coroutineScope.launch { state.hide() } navController.popBackStack()
else navController.popBackStack()
} }
ModalBottomSheetLayout( Scaffold(
sheetState = state, topBar = {
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp), TopAppBar(
sheetContent = { title = {
MangaListingBottomSheet(mangaListing) { Text("최신 업데이트")
coroutineScope.launch { },
client.getItem(it, onReader = { navigationIcon = {
launch { IconButton(onClick = { navController.navigateUp() }) {
state.snapTo(ModalBottomSheetValue.Hidden) Icon(
navController.navigate("manatoki.net/reader/${it.itemID}") Icons.Default.NavigateBefore,
} contentDescription = null
}) )
}
}
}
) {
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()) { contentPadding = rememberInsetsPaddingValues(
LazyVerticalGrid( LocalWindowInsets.current.statusBars,
GridCells.Adaptive(minSize = 200.dp), applyBottom = false
contentPadding = rememberInsetsPaddingValues( )
LocalWindowInsets.current.navigationBars )
) }
) { ) { contentPadding ->
items(model.result) { Box(Modifier.padding(contentPadding)) {
Thumbnail( OverscrollPager(
it, currentPage = model.page,
modifier = Modifier prevPageAvailable = model.page > 1,
.fillMaxWidth() nextPageAvailable = model.page < 10,
.aspectRatio(3f / 4) nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
.padding(8.dp) LocalWindowInsets.current.navigationBars
) { ).calculateBottomPadding(),
coroutineScope.launch { onPageTurn = {
mangaListing = null model.page = it
state.show() model.load()
} }
coroutineScope.launch { ) {
client.getItem(it, onListing = { Box(Modifier.fillMaxSize()) {
mangaListing = it 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))
} }
} }
} }

View File

@@ -22,12 +22,12 @@ import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.detectTapGestures 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.layout.*
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged 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.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp 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.Scaffold
import com.google.accompanist.insets.ui.TopAppBar import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout
import xyz.quaver.pupil.sources.composable.ModalTopSheetState import xyz.quaver.pupil.sources.composable.ModalTopSheetState
import xyz.quaver.pupil.sources.composable.OverscrollPager import xyz.quaver.pupil.sources.composable.OverscrollPager
import xyz.quaver.pupil.sources.manatoki.Chip import xyz.quaver.pupil.sources.manatoki.*
import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.getItem
import xyz.quaver.pupil.sources.manatoki.viewmodel.* import xyz.quaver.pupil.sources.manatoki.viewmodel.*
@ExperimentalFoundationApi @ExperimentalFoundationApi
@@ -75,14 +77,15 @@ fun Search(navController: NavController) {
val client: HttpClient by rememberInstance() val client: HttpClient by rememberInstance()
val database: ManatokiDatabase by rememberInstance()
val historyDao = remember { database.historyDao() }
var searchFocused by remember { mutableStateOf(false) } var searchFocused by remember { mutableStateOf(false) }
val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp) val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp)
val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden) val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current 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<String, MutableInteractionSource>() }
val navigationBarsPadding = LocalDensity.current.run {
rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding().toPx()
}
ModalBottomSheetLayout( ModalBottomSheetLayout(
sheetState = sheetState, sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp), sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = { sheetContent = {
MangaListingBottomSheet(mangaListing) { MangaListingBottomSheet(
mangaListing,
onListSize = { mangaListingListSize = it },
rippleInteractionSource = mangaListingInteractionSource,
listState = mangaListingListState,
recentItem = recentItem
) {
coroutineScope.launch { coroutineScope.launch {
client.getItem(it, onReader = { client.getItem(it, onReader = {
launch { launch {
@@ -201,17 +221,6 @@ fun Search(navController: NavController) {
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp) 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("작가") Text("작가")
TextField(model.artist, onValueChange = { model.artist = it }) 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 { coroutineScope.launch {
mangaListing = null mangaListing = null
sheetState.show() sheetState.animateTo(ModalBottomSheetValue.Expanded)
} }
coroutineScope.launch { coroutineScope.launch {
client.getItem(it, onListing = { client.getItem(it, onListing = {
mangaListing = it 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))
}
}
}
}) })
} }
} }

View File

@@ -103,7 +103,7 @@ fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
} }
} }
private val cache = LruCache<String, Any>(50) private val cache = LruCache<String, Any>(100)
suspend fun HttpClient.getItem( suspend fun HttpClient.getItem(
itemID: String, itemID: String,
onListing: (MangaListing) -> Unit = { }, onListing: (MangaListing) -> Unit = { },
@@ -144,7 +144,7 @@ suspend fun HttpClient.getItem(
}.toString() }.toString()
val urls = Jsoup.parse(htmlData) val urls = Jsoup.parse(htmlData)
.select("img[^data-]:not([style]):not([^class])") .select("img[^data-]:not([style])")
.map { .map {
it.attributes() it.attributes()
.first { it.key.startsWith("data-") } .first { it.key.startsWith("data-") }

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.sources.manatoki.viewmodel package xyz.quaver.pupil.sources.manatoki.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -36,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.HistoryDao
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.manatokiUrl
import xyz.quaver.pupil.sources.manatoki.waitForRateLimit import xyz.quaver.pupil.sources.manatoki.waitForRateLimit

View File

@@ -17,7 +17,7 @@
*/ */
const val GROUP_ID = "xyz.quaver" const val GROUP_ID = "xyz.quaver"
const val VERSION = "6.0.0-alpha01" const val VERSION = "6.0.0-alpha02"
object Versions { object Versions {
const val KOTLIN_VERSION = "1.5.31" const val KOTLIN_VERSION = "1.5.31"