Prepare to export sources
This commit is contained in:
@@ -20,7 +20,7 @@ android {
|
||||
minSdk = 21
|
||||
targetSdk = 31
|
||||
versionCode = 600
|
||||
versionName = "6.0.0-alpha1"
|
||||
versionName = VERSION
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 600,
|
||||
"versionName": "6.0.0-alpha1",
|
||||
"versionName": "6.0.0-alpha02",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<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 fun favoriteDao(): FavoriteDao
|
||||
abstract fun bookmarkDao(): BookmarkDao
|
||||
|
||||
@@ -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<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)
|
||||
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<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(
|
||||
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 {
|
||||
|
||||
@@ -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<MutableInteractionSource> = emptyList(),
|
||||
rippleInteractionSource: Map<String, MutableInteractionSource> = 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,
|
||||
|
||||
@@ -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<MutableInteractionSource>() }
|
||||
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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, MutableInteractionSource>() }
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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-") }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user