diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 8ec0174f..8a4d8845 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -1,17 +1,17 @@
-
+
-
+
-
-
+
+
-
-
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 69c047e2..d36af219 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -110,10 +110,10 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
- implementation("androidx.room:room-runtime:2.3.0")
- annotationProcessor("androidx.room:room-compiler:2.3.0")
- kapt("androidx.room:room-compiler:2.3.0")
- implementation("androidx.room:room-ktx:2.3.0")
+ implementation("androidx.room:room-runtime:2.4.0")
+ annotationProcessor("androidx.room:room-compiler:2.4.0")
+ kapt("androidx.room:room-compiler:2.4.0")
+ implementation("androidx.room:room-ktx:2.4.0")
implementation("androidx.datastore:datastore:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
@@ -138,6 +138,8 @@ dependencies {
implementation("xyz.quaver:documentfilex:0.7.1")
implementation("xyz.quaver:subsampledimage:0.0.1-alpha11-SNAPSHOT")
+ implementation("com.google.guava:guava:31.0.1-android")
+
implementation("org.kodein.log:kodein-log:0.11.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
index a747af3f..33975834 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
@@ -23,6 +23,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import org.kodein.di.*
import xyz.quaver.pupil.sources.hitomi.Hitomi
+import xyz.quaver.pupil.sources.manatoki.Manatoki
abstract class Source {
abstract val name: String
@@ -39,7 +40,7 @@ val sourceModule = DI.Module(name = "source") {
listOf<(Application) -> (Source)>(
{ Hitomi(it) },
//{ Hiyobi_io(it) },
- //{ Manatoki(it) }
+ { Manatoki(it) }
).forEach { source ->
inSet { singleton { source(instance()).let { it.name to it } } }
}
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 3bac274e..7e05f5aa 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
@@ -41,13 +41,16 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.insets.LocalWindowInsets
+import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
-import com.google.accompanist.systemuicontroller.rememberSystemUiController
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +66,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.ui.theme.Orange500
import xyz.quaver.pupil.util.NetworkCache
+import xyz.quaver.pupil.util.activity
import xyz.quaver.pupil.util.rememberFileXImageSource
import kotlin.math.abs
@@ -75,9 +79,6 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
private val database: AppDatabase by instance()
- private val historyDao = database.historyDao()
- private val bookmarkDao = database.bookmarkDao()
-
var error by mutableStateOf(false)
var title by mutableStateOf(null)
@@ -171,6 +172,19 @@ fun ReaderBase(
val scaffoldState = rememberScaffoldState()
val snackbarCoroutineScope = rememberCoroutineScope()
+ LaunchedEffect(model.isFullscreen) {
+ context.activity?.window?.let { window ->
+ ViewCompat.getWindowInsetsController(window.decorView)?.let {
+ if (model.isFullscreen) {
+ it.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ it.hide(WindowInsetsCompat.Type.systemBars())
+ } else
+ it.show(WindowInsetsCompat.Type.systemBars())
+ }
+ }
+ }
+
if (model.error)
stringResource(R.string.reader_failed_to_find_gallery).let {
snackbarCoroutineScope.launch {
@@ -181,16 +195,6 @@ fun ReaderBase(
}
}
- val systemUiController = rememberSystemUiController()
- val useDarkIcons = MaterialTheme.colors.isLight
-
- SideEffect {
- systemUiController.setSystemBarsColor(
- color = Color.Transparent,
- darkIcons = useDarkIcons
- )
- }
-
Scaffold(
topBar = {
if (!model.isFullscreen)
@@ -225,6 +229,7 @@ fun ReaderBase(
floatingActionButton = {
if (!model.isFullscreen)
MultipleFloatingActionButton(
+ modifier = Modifier.navigationBarsPadding(),
items = listOf(
SubFabItem(
icon = Icons.Default.Fullscreen,
@@ -245,7 +250,8 @@ fun ReaderBase(
Box(Modifier.padding(contentPadding)) {
LazyColumn(
Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
) {
itemsIndexed(model.imageList) { i, uri ->
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
index cde02388..111a2c8e 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
@@ -35,7 +35,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -56,9 +55,8 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.systemBarsPadding
+import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.systemuicontroller.rememberSystemUiController
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.theme.LightBlue300
import kotlin.math.*
@@ -119,25 +117,15 @@ fun SearchBase(
}
}
- val systemBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.systemBars)
+ val statusBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars)
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
- val searchBarDefaultOffset = systemBarsPaddingValues.calculateTopPadding() + 64.dp
+ val searchBarDefaultOffset = statusBarsPaddingValues.calculateTopPadding() + 64.dp
val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
var overscroll: Float? by remember { mutableStateOf(null) }
- val systemUiController = rememberSystemUiController()
- val useDarkIcons = MaterialTheme.colors.isLight
-
- SideEffect {
- systemUiController.setSystemBarsColor(
- color = Color.Transparent,
- darkIcons = useDarkIcons
- )
- }
-
LaunchedEffect(navigationIconProgress) {
navigationIcon.progress = navigationIconProgress
}
@@ -307,7 +295,7 @@ fun SearchBase(
FloatingSearchBar(
modifier = Modifier
- .systemBarsPadding()
+ .statusBarsPadding()
.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
query = model.query,
onQueryChange = { model.query = it },
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt
similarity index 82%
rename from app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt
rename to app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt
index 4e68bf32..c1360d33 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt
@@ -1,6 +1,6 @@
/*
* Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
+ * Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -13,13 +13,12 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
+ * along with this program. If not, see .
*/
-package xyz.quaver.pupil.ui.dialog
+package xyz.quaver.pupil.sources.composable
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -29,13 +28,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import androidx.navigation.NavController
import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.SourceEntries
+@Composable
+fun SourceSelectDialog(navController: NavController, currentSource: String, onDismissRequest: () -> Unit = { }) {
+ SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
+ onDismissRequest()
+ navController.navigate(it.name) {
+ popUpTo(currentSource) { inclusive = true }
+ }
+ }
+}
+
@Composable
fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
Row(
@@ -86,4 +95,4 @@ fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
index e09bafa9..3a24a881 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
@@ -41,7 +41,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import io.ktor.client.*
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
@@ -57,7 +56,6 @@ import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
import xyz.quaver.pupil.sources.hitomi.lib.getReferer
import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
-import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
class Hitomi(app: Application) : Source(), DIAware {
override val di by closestDI(app)
@@ -73,9 +71,9 @@ class Hitomi(app: Application) : Source(), DIAware {
override val iconResID: Int = R.drawable.hitomi
override fun NavGraphBuilder.navGraph(navController: NavController) {
- navigation(startDestination = "search", route = name) {
- composable("search") { Search(navController) }
- composable("reader/{itemID}") { Reader(navController) }
+ navigation(startDestination = "hitomi.la/search", route = name) {
+ composable("hitomi.la/search") { Search(navController) }
+ composable("hitomi.la/reader/{itemID}") { Reader(navController) }
}
}
@@ -94,16 +92,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var sourceSelectDialog by remember { mutableStateOf(false) }
if (sourceSelectDialog)
- SourceSelectDialog(
- currentSource = name,
- onDismissRequest = { sourceSelectDialog = false }
- ) {
- sourceSelectDialog = false
- navController.navigate("main/${it.name}") {
- launchSingleTop = true
- popUpTo("main/{source}") { inclusive = true }
- }
- }
+ SourceSelectDialog(navController, name) { sourceSelectDialog = false }
LaunchedEffect(model.currentPage, model.sortByPopularity) {
model.search()
@@ -188,7 +177,10 @@ class Hitomi(app: Application) : Source(), DIAware {
}
}
) { result ->
- navController.navigate("reader/${result.itemID}")
+ logger.info {
+ result.toString()
+ }
+ navController.navigate("hitomi.la/reader/${result.itemID}")
}
}
}
@@ -209,21 +201,19 @@ class Hitomi(app: Application) : Source(), DIAware {
val bookmark by bookmarkDao.contains(name, itemID).observeAsState(false)
- LaunchedEffect(model) {
- launch(Dispatchers.IO) {
- kotlin.runCatching {
- val galleryID = itemID.toInt()
+ LaunchedEffect(itemID) {
+ runCatching {
+ val galleryID = itemID.toInt()
- val galleryInfo = getGalleryInfo(client, galleryID)
+ val galleryInfo = getGalleryInfo(client, galleryID)
- model.title = galleryInfo.title
+ model.title = galleryInfo.title
- model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
- append("Referer", getReferer(galleryID))
- }
- }.onFailure {
- model.error = true
+ model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
+ append("Referer", getReferer(galleryID))
}
+ }.onFailure {
+ model.error = true
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
index 1d9c8eac..a3decdf4 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
@@ -1,101 +1,339 @@
-///*
-// * Pupil, Hitomi.la viewer for Android
-// * Copyright (C) 2021 tom5079
-// *
-// * This program is free software: you can redistribute it and/or modify
-// * it under the terms of the GNU General Public License as published by
-// * the Free Software Foundation, either version 3 of the License, or
-// * (at your option) any later version.
-// *
-// * This program is distributed in the hope that it will be useful,
-// * but WITHOUT ANY WARRANTY; without even the implied warranty of
-// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// * GNU General Public License for more details.
-// *
-// * You should have received a copy of the GNU General Public License
-// * along with this program. If not, see .
-// */
-//
-//package xyz.quaver.pupil.sources.manatoki
-//
-//import android.app.Application
-//import kotlinx.coroutines.Dispatchers
-//import kotlinx.coroutines.channels.Channel
-//import kotlinx.coroutines.coroutineScope
-//import kotlinx.coroutines.withContext
-//import kotlinx.parcelize.Parcelize
-//import org.jsoup.Jsoup
-//import org.kodein.di.DIAware
-//import org.kodein.di.android.closestDI
-//import org.kodein.log.LoggerFactory
-//import org.kodein.log.newLogger
-//import xyz.quaver.pupil.R
-//import xyz.quaver.pupil.sources.ItemInfo
-//import xyz.quaver.pupil.sources.Source
-//
-//@Parcelize
-//class ManatokiItemInfo(
-// override val itemID: String,
-// override val title: String
-//) : ItemInfo {
-// override val source: String = "manatoki.net"
-//}
-//
-//class Manatoki(app: Application) : Source(), DIAware {
-// override val di by closestDI(app)
-//
-// private val logger = newLogger(LoggerFactory.default)
-//
-// override val name = "manatoki.net"
-// override val availableSortMode: List = emptyList()
-// override val iconResID: Int = R.drawable.manatoki
-//
-// override suspend fun search(
-// query: String,
-// range: IntRange,
-// sortMode: Int
-// ): Pair, Int> {
-// TODO("Not yet implemented")
-// }
-//
-// override suspend fun images(itemID: String): List = coroutineScope {
-// val jsoup = withContext(Dispatchers.IO) {
-// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
-// }
-//
-// val htmlData = jsoup
-// .selectFirst(".view-padding > script")!!
-// .data()
-// .splitToSequence('\n')
-// .fold(StringBuilder()) { sb, line ->
-// if (!line.startsWith("html_data")) return@fold sb
-//
-// line.drop(12).dropLast(2).split('.').forEach {
-// if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
-// }
-// sb
-// }.toString()
-//
-// Jsoup.parse(htmlData)
-// .select("img[^data-]:not([style])")
-// .map {
-// it.attributes()
-// .first { it.key.startsWith("data-") }
-// .value
-// }
-// }
-//
-// override suspend fun info(itemID: String): ItemInfo = coroutineScope {
-// val jsoup = withContext(Dispatchers.IO) {
-// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
-// }
-//
-// val title = jsoup.selectFirst(".toon-title")!!.ownText()
-//
-// ManatokiItemInfo(
-// itemID,
-// title
-// )
-// }
-//
-//}
\ No newline at end of file
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package xyz.quaver.pupil.sources.manatoki
+
+import android.app.Application
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.navigation
+import com.google.accompanist.insets.LocalWindowInsets
+import com.google.accompanist.insets.navigationBarsPadding
+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.channels.Channel
+import kotlinx.coroutines.launch
+import org.kodein.di.DIAware
+import org.kodein.di.android.closestDI
+import org.kodein.di.compose.rememberInstance
+import org.kodein.log.LoggerFactory
+import org.kodein.log.newLogger
+import xyz.quaver.pupil.R
+import xyz.quaver.pupil.db.AppDatabase
+import xyz.quaver.pupil.sources.Source
+import xyz.quaver.pupil.sources.composable.ReaderBase
+import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
+import xyz.quaver.pupil.sources.composable.SourceSelectDialog
+import xyz.quaver.pupil.sources.manatoki.composable.BoardButton
+import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
+import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
+import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
+import java.util.concurrent.ConcurrentHashMap
+
+class Manatoki(app: Application) : Source(), DIAware {
+ override val di by closestDI(app)
+
+ private val logger = newLogger(LoggerFactory.default)
+
+ override val name = "manatoki.net"
+ override val iconResID = R.drawable.manatoki
+
+ private val readerInfoChannel = ConcurrentHashMap>()
+
+ override fun NavGraphBuilder.navGraph(navController: NavController) {
+ navigation(route = name, startDestination = "manatoki.net/") {
+ composable("manatoki.net/") { Main(navController) }
+ composable("manatoki.net/reader/{itemID}") { Reader(navController) }
+ }
+ }
+
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun Main(navController: NavController) {
+ val model: MainViewModel = viewModel()
+
+ val client: HttpClient by rememberInstance()
+
+ val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
+ var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
+
+ val coroutineScope = rememberCoroutineScope()
+
+ val onListing: (MangaListing) -> Unit = {
+ mangaListing = it
+ logger.info {
+ it.toString()
+ }
+ coroutineScope.launch {
+ sheetState.show()
+ }
+ }
+
+ val onReader: (ReaderInfo) -> Unit = { readerInfo ->
+ val channel = Channel()
+ readerInfoChannel[readerInfo.itemID] = channel
+
+ coroutineScope.launch {
+ channel.send(readerInfo)
+ }
+ navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
+ }
+
+ var sourceSelectDialog by remember { mutableStateOf(false) }
+
+ if (sourceSelectDialog)
+ SourceSelectDialog(navController, name) { sourceSelectDialog = false }
+
+ LaunchedEffect(Unit) {
+ navController.backQueue.forEach {
+ logger.info {
+ it.destination.route.toString()
+ }
+ }
+ model.load()
+ }
+
+ BackHandler {
+ if (sheetState.currentValue == ModalBottomSheetValue.Hidden)
+ navController.popBackStack()
+ else
+ coroutineScope.launch {
+ sheetState.hide()
+ }
+ }
+
+ ModalBottomSheetLayout(
+ sheetState = sheetState,
+ sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
+ sheetContent = {
+ MangaListingBottomSheet(mangaListing) {
+ coroutineScope.launch {
+ client.getItem(it, onListing, onReader)
+ }
+ }
+ }
+ ) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text("박사장 게섯거라")
+ },
+ actions = {
+ IconButton(onClick = { sourceSelectDialog = true }) {
+ Image(
+ painter = painterResource(id = R.drawable.manatoki),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ IconButton(onClick = { navController.navigate("settings") }) {
+ Icon(Icons.Default.Settings, contentDescription = null)
+ }
+ },
+ contentPadding = rememberInsetsPaddingValues(
+ insets = LocalWindowInsets.current.statusBars,
+ applyBottom = false
+ )
+ )
+ }
+ ) { contentPadding ->
+ Box(Modifier.padding(contentPadding)) {
+ Column(
+ Modifier
+ .padding(8.dp, 0.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ "최신화",
+ style = MaterialTheme.typography.h5
+ )
+
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(210.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(model.recentUpload) { item ->
+ Thumbnail(item) {
+ coroutineScope.launch {
+ client.getItem(it, onListing, onReader)
+ }
+ }
+ }
+ }
+
+ Divider()
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ BoardButton("마나게시판", Color(0xFF007DB4))
+ BoardButton("유머/가십", Color(0xFFF09614))
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ BoardButton("역식자게시판", Color(0xFFA0C850))
+ BoardButton("원본게시판", Color(0xFFFF4500))
+ }
+ }
+
+ Text("만화 목록", style = MaterialTheme.typography.h5)
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(210.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(model.mangaList) { item ->
+ Thumbnail(item) {
+ coroutineScope.launch {
+ client.getItem(it, onListing, onReader)
+ }
+ }
+ }
+ }
+
+ Text("주간 베스트", style = MaterialTheme.typography.h5)
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ model.topWeekly.forEachIndexed { index, item ->
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ (index + 1).toString(),
+ modifier = Modifier
+ .background(Color(0xFF64C3F5))
+ .width(24.dp),
+ color = Color.White,
+ textAlign = TextAlign.Center
+ )
+
+ Text(
+ item.title,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Text(
+ item.count,
+ color = Color(0xFFFF4500)
+ )
+ }
+ }
+ }
+
+ Box(Modifier.navigationBarsPadding())
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun Reader(navController: NavController) {
+ val model: ReaderBaseViewModel = viewModel()
+
+ val database: AppDatabase by rememberInstance()
+ val bookmarkDao = database.bookmarkDao()
+
+ val coroutineScope = rememberCoroutineScope()
+
+ val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
+
+ LaunchedEffect(Unit) {
+ val channel = itemID?.let { readerInfoChannel.remove(it) }
+
+ if (channel == null)
+ model.error = true
+ else {
+ val readerInfo = channel.receive()
+
+ model.title = readerInfo.title
+ model.load(readerInfo.urls)
+ }
+ }
+
+ val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
+
+ BackHandler {
+ if (model.isFullscreen)
+ model.isFullscreen = false
+ else
+ navController.popBackStack()
+ }
+
+ ReaderBase(
+ model,
+ icon = {
+ Image(
+ painter = painterResource(R.drawable.manatoki),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ },
+ bookmark = bookmark,
+ onToggleBookmark = {
+ if (itemID != null)
+ coroutineScope.launch {
+ if (bookmark) bookmarkDao.delete(name, itemID)
+ else bookmarkDao.insert(name, itemID)
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt
new file mode 100644
index 00000000..628fa27e
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt
@@ -0,0 +1,66 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.manatoki.composable
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun RowScope.BoardButton(
+ text: String,
+ color: Color
+) {
+ Card(
+ modifier = Modifier.height(64.dp).weight(1f),
+ shape = RoundedCornerShape(12.dp),
+ elevation = 8.dp
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text,
+ modifier = Modifier.padding(8.dp, 0.dp).weight(1f),
+ style = MaterialTheme.typography.h6
+ )
+
+ Icon(
+ Icons.Default.ArrowForward,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier
+ .width(48.dp)
+ .fillMaxHeight()
+ .background(color)
+ )
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..4bc30ec9
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt
@@ -0,0 +1,209 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.manatoki.composable
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberImagePainter
+import com.google.accompanist.flowlayout.FlowRow
+import com.google.accompanist.insets.LocalWindowInsets
+import com.google.accompanist.insets.navigationBarsPadding
+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 enum class MangaListingBottomSheetLayoutContent { Top, Bottom, Fab }
+
+@Composable
+fun MangaListingBottomSheetLayout(
+ modifier: Modifier = Modifier,
+ floatingActionButton: @Composable () -> Unit,
+ top: @Composable () -> Unit,
+ bottom: @Composable () -> Unit
+) {
+ SubcomposeLayout { constraints ->
+ val layoutWidth = constraints.maxWidth
+ val layoutHeight = constraints.maxHeight * HeightPercentage / 100
+
+ layout(layoutWidth, layoutHeight) {
+ val topPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Top, top).map {
+ it.measure(constraints)
+ }
+
+ val topPlaceableHeight = topPlaceables.maxOfOrNull { it.height } ?: 0
+
+ val bottomConstraints = constraints.copy(
+ maxHeight = layoutHeight - topPlaceableHeight
+ )
+
+ val bottomPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Bottom, bottom).map {
+ it.measure(bottomConstraints)
+ }
+
+ val fabPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Fab, floatingActionButton).mapNotNull {
+ it.measure(constraints).takeIf { it.height != 0 && it.width != 0 }
+ }
+
+ topPlaceables.forEach { it.place(0, 0) }
+ bottomPlaceables.forEach { it.place(0, topPlaceableHeight) }
+
+ if (fabPlaceables.isNotEmpty()) {
+ val fabWidth = fabPlaceables.maxOf { it.width }
+ val fabHeight = fabPlaceables.maxOf { it.height }
+
+ fabPlaceables.forEach {
+ it.place(
+ layoutWidth - fabWidth - FabSpacing.roundToPx(),
+ topPlaceableHeight - fabHeight / 2
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MangaListingBottomSheet(
+ mangaListing: MangaListing? = null,
+ onOpenItem: (String) -> Unit = { }
+) {
+ Box(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ mangaListing?.run {
+ MangaListingBottomSheetLayout(
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ text = { Text("첫화보기") },
+ onClick = { entries.lastOrNull()?.let { onOpenItem(it.itemID) } }
+ )
+ },
+ top = {
+ Row(
+ modifier = Modifier
+ .height(IntrinsicSize.Min)
+ .background(MaterialTheme.colors.primary)
+ .padding(0.dp, 0.dp, 0.dp, 4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val painter = rememberImagePainter(thumbnail)
+
+ Image(
+ modifier = Modifier
+ .width(150.dp)
+ .aspectRatio(
+ with(painter.intrinsicSize) { if (this == androidx.compose.ui.geometry.Size.Unspecified) 1f else width / height },
+ true
+ ),
+ painter = painter,
+ contentDescription = null
+ )
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(0.dp, 8.dp)
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ title,
+ style = MaterialTheme.typography.h5,
+ modifier = Modifier.weight(1f)
+ )
+
+ CompositionLocalProvider(LocalContentAlpha provides 0.7f) {
+ Text("작가: $author")
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("분류: ")
+
+ CompositionLocalProvider(LocalContentAlpha provides 1f) {
+ FlowRow(
+ modifier = Modifier.weight(1f),
+ mainAxisSpacing = 8.dp
+ ) {
+ tags.forEach {
+ Card(
+ elevation = 4.dp
+ ) {
+ Text(
+ it,
+ style = MaterialTheme.typography.caption,
+ modifier = Modifier.padding(4.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Text("발행구분: $type")
+ }
+ }
+ }
+ },
+ bottom = {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
+ ) {
+ items(entries) { entry ->
+ Row(
+ modifier = Modifier
+ .clickable {
+ onOpenItem(entry.itemID)
+ }
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ entry.title,
+ style = MaterialTheme.typography.h6,
+ modifier = Modifier.weight(1f)
+ )
+
+ Text("★ ${entry.starRating}")
+ }
+ Divider()
+ }
+ }
+ }
+ )
+ } ?: run {
+ CircularProgressIndicator(
+ Modifier.align(Alignment.Center).navigationBarsPadding().padding(16.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt
new file mode 100644
index 00000000..e8dfdc52
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt
@@ -0,0 +1,79 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.manatoki.composable
+
+import android.os.Parcelable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberImagePainter
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@Serializable
+data class Thumbnail(
+ val itemID: String,
+ val title: String,
+ val thumbnail: String
+): Parcelable
+
+@Composable
+fun Thumbnail(
+ thumbnail: Thumbnail,
+ onClick: (String) -> Unit = { }
+) {
+ Card(
+ shape = RoundedCornerShape(12.dp),
+ elevation = 8.dp,
+ modifier = Modifier.clickable { onClick(thumbnail.itemID) }
+ ) {
+ Box(
+ modifier = Modifier.width(IntrinsicSize.Min)
+ ) {
+ Image(
+ modifier = Modifier.size(180.dp, 210.dp),
+ painter = rememberImagePainter(thumbnail.thumbnail),
+ contentDescription = null
+ )
+
+ Text(
+ thumbnail.title,
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .fillMaxWidth()
+ .background(Color.Black.copy(alpha = 0.7f))
+ .padding(8.dp),
+ softWrap = true,
+ style = MaterialTheme.typography.subtitle1
+ )
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..9c20f9b4
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
@@ -0,0 +1,183 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.manatoki
+
+import android.os.Parcelable
+import com.google.common.util.concurrent.RateLimiter
+import io.ktor.client.*
+import io.ktor.client.request.*
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+import org.jsoup.Jsoup
+import java.util.concurrent.Executors
+
+private val rateLimitCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+private val rateLimiter = RateLimiter.create(10.0)
+
+suspend fun waitForRateLimit() {
+ withContext(rateLimitCoroutineDispatcher) {
+ rateLimiter.acquire()
+ }
+ yield()
+}
+
+@Parcelize
+@Serializable
+data class MangaListingEntry(
+ val itemID: String,
+ val episode: Int,
+ val title: String,
+ val starRating: Float,
+ val date: String,
+ val viewCount: Int,
+ val thumbsUpCount: Int
+): Parcelable
+
+@Parcelize
+@Serializable
+data class MangaListing(
+ val itemID: String,
+ val title: String,
+ val thumbnail: String,
+ val author: String,
+ val tags: List,
+ val type: String,
+ val thumbsUpCount: Int,
+ val entries: List
+): Parcelable
+
+@Parcelize
+@Serializable
+data class ReaderInfo(
+ val itemID: String,
+ val title: String,
+ val urls: List
+): Parcelable
+
+suspend fun HttpClient.getItem(
+ itemID: String,
+ onListing: (MangaListing) -> Unit,
+ onReader: (ReaderInfo) -> Unit
+) = coroutineScope {
+ waitForRateLimit()
+ val content: String = get("https://manatoki116.net/comic/$itemID")
+
+ val doc = Jsoup.parse(content)
+
+ yield()
+
+ if (doc.getElementsByClass("serial-list").size == 0) {
+ val htmlData = doc
+ .selectFirst(".view-padding > script")!!
+ .data()
+ .splitToSequence('\n')
+ .fold(StringBuilder()) { sb, line ->
+ if (!line.startsWith("html_data")) return@fold sb
+
+ line.drop(12).dropLast(2).split('.').forEach {
+ if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
+ }
+ sb
+ }.toString()
+
+ val urls = Jsoup.parse(htmlData)
+ .select("img[^data-]:not([style])")
+ .map {
+ it.attributes()
+ .first { it.key.startsWith("data-") }
+ .value
+ }
+
+ val title = doc.getElementsByClass("toon-title").first()!!.ownText()
+
+ onReader(ReaderInfo(itemID, title, urls))
+ } else {
+ val titleBlock = doc.selectFirst("div.view-title")!!
+
+ val title = titleBlock.select("div.view-content:not([itemprop])").first()!!.text()
+
+ val author =
+ titleBlock
+ .select("div.view-content:not([itemprop]):contains(작가)")
+ .first()!!
+ .getElementsByTag("a")
+ .first()!!
+ .text()
+
+ val tags =
+ titleBlock
+ .select("div.view-content:not([itemprop]):contains(분류)")
+ .first()!!
+ .getElementsByTag("a")
+ .map { it.text() }
+
+ val type =
+ titleBlock
+ .select("div.view-content:not([itemprop]):contains(발행구분)")
+ .first()!!
+ .getElementsByTag("a")
+ .first()!!
+ .text()
+
+ val thumbnail =
+ titleBlock.getElementsByTag("img").first()!!.attr("src")
+
+ val thumbsUpCount =
+ titleBlock.select("i.fa-thumbs-up + b").text().toInt()
+
+ val entries =
+ doc.select("div.serial-list .list-item").map {
+ val episode = it.getElementsByClass("wr-num").first()!!.text().toInt()
+ val (itemID, title) = it.getElementsByClass("item-subject").first()!!.let { subject ->
+ subject.attr("href").dropLastWhile { it != '?' }.dropLast(1).takeLastWhile { it != '/' } to subject.ownText()
+ }
+ val starRating = it.getElementsByClass("wr-star").first()!!.text().drop(1).takeWhile { it != ')' }.toFloat()
+ val date = it.getElementsByClass("wr-date").first()!!.text()
+ val viewCount = it.getElementsByClass("wr-hit").first()!!.text().replace(",", "").toInt()
+ val thumbsUpCount = it.getElementsByClass("wr-good").first()!!.text().replace(",", "").toInt()
+
+ MangaListingEntry(
+ itemID,
+ episode,
+ title,
+ starRating,
+ date,
+ viewCount,
+ thumbsUpCount
+ )
+ }
+
+ onListing(
+ MangaListing(
+ itemID,
+ title,
+ thumbnail,
+ author,
+ tags,
+ type,
+ thumbsUpCount,
+ entries
+ )
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..befdd738
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.manatoki.viewmodel
+
+import android.app.Application
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import io.ktor.client.*
+import io.ktor.client.request.*
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+import kotlinx.serialization.Serializable
+import org.jsoup.Jsoup
+import org.kodein.di.DIAware
+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.composable.Thumbnail
+import xyz.quaver.pupil.sources.manatoki.waitForRateLimit
+
+@Serializable
+data class TopWeekly(
+ val itemID: String,
+ val title: String,
+ val count: String
+)
+
+class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
+ override val di by closestDI(app)
+
+ private val logger = newLogger(LoggerFactory.default)
+
+ private val client: HttpClient by instance()
+
+ val recentUpload = mutableStateListOf()
+ val mangaList = mutableStateListOf()
+ val topWeekly = mutableStateListOf()
+
+ private var loadJob: Job? = null
+
+ fun load() {
+ viewModelScope.launch {
+ loadJob?.cancelAndJoin()
+ recentUpload.clear()
+ mangaList.clear()
+ topWeekly.clear()
+
+ loadJob = launch {
+ runCatching {
+ waitForRateLimit()
+ val doc = Jsoup.parse(client.get("https://manatoki116.net/"))
+
+ yield()
+
+ val misoPostGallery = doc.select(".miso-post-gallery")
+
+ misoPostGallery[0]
+ .select(".post-image > a")
+ .forEach { entry ->
+ val itemID = entry.attr("href").takeLastWhile { it != '/' }
+ val title = entry.selectFirst("div.in-subject > b")!!.ownText()
+ val thumbnail = entry.selectFirst("img")!!.attr("src")
+
+ yield()
+ recentUpload.add(Thumbnail(itemID, title, thumbnail))
+ }
+
+ misoPostGallery[1]
+ .select(".post-image > a").also { logger.info { it.size.toString() } }
+ .forEach { entry ->
+ val itemID = entry.attr("href").takeLastWhile { it != '/' }
+ val title = entry.selectFirst("div.in-subject")!!.ownText()
+ val thumbnail = entry.selectFirst("img")!!.attr("src")
+
+ yield()
+ mangaList.add(Thumbnail(itemID, title, thumbnail))
+ }
+
+ val misoPostList = doc.select(".miso-post-list")
+
+ misoPostList[4]
+ .select(".post-row > a")
+ .forEach { entry ->
+ yield()
+ val itemID = entry.attr("href").takeLastWhile { it != '/' }
+ val title = entry.ownText()
+ val count = entry.selectFirst("span.count")!!.text()
+ topWeekly.add(TopWeekly(itemID, title, count))
+ }
+ }.onFailure {
+ logger.warning(it)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index 4f6ef99f..d70c97cf 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -22,10 +22,16 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.graphics.Color
import androidx.core.view.WindowCompat
import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.insets.ProvideWindowInsets
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
@@ -53,9 +59,34 @@ class MainActivity : ComponentActivity(), DIAware {
ProvideWindowInsets {
val navController = rememberNavController()
- NavHost(navController, startDestination = "hitomi.la") {
+ val systemUiController = rememberSystemUiController()
+ val useDarkIcons = MaterialTheme.colors.isLight
+
+ SideEffect {
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = useDarkIcons
+ )
+ }
+
+ NavHost(navController, startDestination = "main") {
+ composable("main") {
+ var launched by rememberSaveable { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ if (!launched) {
+ val source = it.arguments?.getString("source") ?: "hitomi.la"
+ navController.navigate(source)
+ launched = true
+ } else {
+ onBackPressed()
+ }
+ }
+ }
sources.forEach {
- it.second.run { navGraph(navController) }
+ it.second.run {
+ navGraph(navController)
+ }
}
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
index 721c8940..ffc9cf62 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -18,6 +18,9 @@
package xyz.quaver.pupil.util
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
import android.graphics.BitmapFactory
import android.view.View
import androidx.compose.runtime.Composable
@@ -82,3 +85,15 @@ fun rememberFileXImageSource(file: FileX) = remember {
fun sha256(data: ByteArray) : ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data)
}
+
+val Context.activity: Activity?
+ get() {
+ var currentContext = this
+ while (currentContext is ContextWrapper) {
+ if (currentContext is Activity)
+ return currentContext
+ currentContext = currentContext.baseContext
+ }
+
+ return null
+ }
\ No newline at end of file