[Manatoki] Main/Reader OK

This commit is contained in:
tom5079
2021-12-20 11:44:13 +09:00
parent 0f4e1a8e0d
commit b82ef8695c
15 changed files with 1111 additions and 178 deletions

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown> <targetSelectedWithDropDown>
<Target> <Target>
<type value="RUNNING_DEVICE_TARGET" /> <type value="QUICK_BOOT_TARGET" />
<deviceKey> <deviceKey>
<Key> <Key>
<type value="SERIAL_NUMBER" /> <type value="VIRTUAL_DEVICE_PATH" />
<value value="ce021712e3b19b2b04" /> <value value="$USER_HOME$/.android/avd/Pixel_3a_API_30_x86.avd" />
</Key> </Key>
</deviceKey> </deviceKey>
</Target> </Target>
</runningDeviceTargetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-12-19T03:31:58.153375Z" /> <timeTargetWasSelectedWithDropDown value="2021-12-20T01:51:44.761422Z" />
</component> </component>
</project> </project>

View File

@@ -110,10 +110,10 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
implementation("androidx.room:room-runtime:2.3.0") implementation("androidx.room:room-runtime:2.4.0")
annotationProcessor("androidx.room:room-compiler:2.3.0") annotationProcessor("androidx.room:room-compiler:2.4.0")
kapt("androidx.room:room-compiler:2.3.0") kapt("androidx.room:room-compiler:2.4.0")
implementation("androidx.room:room-ktx:2.3.0") implementation("androidx.room:room-ktx:2.4.0")
implementation("androidx.datastore:datastore:1.0.0") implementation("androidx.datastore:datastore:1.0.0")
implementation("androidx.datastore:datastore-preferences: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:documentfilex:0.7.1")
implementation("xyz.quaver:subsampledimage:0.0.1-alpha11-SNAPSHOT") 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") implementation("org.kodein.log:kodein-log:0.11.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")

View File

@@ -23,6 +23,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import org.kodein.di.* import org.kodein.di.*
import xyz.quaver.pupil.sources.hitomi.Hitomi import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.sources.manatoki.Manatoki
abstract class Source { abstract class Source {
abstract val name: String abstract val name: String
@@ -39,7 +40,7 @@ val sourceModule = DI.Module(name = "source") {
listOf<(Application) -> (Source)>( listOf<(Application) -> (Source)>(
{ Hitomi(it) }, { Hitomi(it) },
//{ Hiyobi_io(it) }, //{ Hiyobi_io(it) },
//{ Manatoki(it) } { Manatoki(it) }
).forEach { source -> ).forEach { source ->
inSet { singleton { source(instance()).let { it.name to it } } } inSet { singleton { source(instance()).let { it.name to it } } }
} }

View File

@@ -41,13 +41,16 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues 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 com.google.accompanist.systemuicontroller.rememberSystemUiController
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +66,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.ui.theme.Orange500 import xyz.quaver.pupil.ui.theme.Orange500
import xyz.quaver.pupil.util.NetworkCache import xyz.quaver.pupil.util.NetworkCache
import xyz.quaver.pupil.util.activity
import xyz.quaver.pupil.util.rememberFileXImageSource import xyz.quaver.pupil.util.rememberFileXImageSource
import kotlin.math.abs import kotlin.math.abs
@@ -75,9 +79,6 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
private val database: AppDatabase by instance() private val database: AppDatabase by instance()
private val historyDao = database.historyDao()
private val bookmarkDao = database.bookmarkDao()
var error by mutableStateOf(false) var error by mutableStateOf(false)
var title by mutableStateOf<String?>(null) var title by mutableStateOf<String?>(null)
@@ -171,6 +172,19 @@ fun ReaderBase(
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
val snackbarCoroutineScope = rememberCoroutineScope() 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) if (model.error)
stringResource(R.string.reader_failed_to_find_gallery).let { stringResource(R.string.reader_failed_to_find_gallery).let {
snackbarCoroutineScope.launch { 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( Scaffold(
topBar = { topBar = {
if (!model.isFullscreen) if (!model.isFullscreen)
@@ -225,6 +229,7 @@ fun ReaderBase(
floatingActionButton = { floatingActionButton = {
if (!model.isFullscreen) if (!model.isFullscreen)
MultipleFloatingActionButton( MultipleFloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
items = listOf( items = listOf(
SubFabItem( SubFabItem(
icon = Icons.Default.Fullscreen, icon = Icons.Default.Fullscreen,
@@ -245,7 +250,8 @@ fun ReaderBase(
Box(Modifier.padding(contentPadding)) { Box(Modifier.padding(contentPadding)) {
LazyColumn( LazyColumn(
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
) { ) {
itemsIndexed(model.imageList) { i, uri -> itemsIndexed(model.imageList) { i, uri ->
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH) val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)

View File

@@ -35,7 +35,6 @@ import androidx.compose.runtime.*
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.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource 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.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues 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.insets.ui.Scaffold
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.theme.LightBlue300 import xyz.quaver.pupil.ui.theme.LightBlue300
import kotlin.math.* import kotlin.math.*
@@ -119,25 +117,15 @@ fun <T> SearchBase(
} }
} }
val systemBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.systemBars) val statusBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars)
val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() } 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() } val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
var overscroll: Float? by remember { mutableStateOf(null) } 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) { LaunchedEffect(navigationIconProgress) {
navigationIcon.progress = navigationIconProgress navigationIcon.progress = navigationIconProgress
} }
@@ -307,7 +295,7 @@ fun <T> SearchBase(
FloatingSearchBar( FloatingSearchBar(
modifier = Modifier modifier = Modifier
.systemBarsPadding() .statusBarsPadding()
.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }), .offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
query = model.query, query = model.query,
onQueryChange = { model.query = it }, onQueryChange = { model.query = it },

View File

@@ -1,6 +1,6 @@
/* /*
* Pupil, Hitomi.la viewer for Android * 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 * 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 * it under the terms of the GNU General Public License as published by
@@ -13,13 +13,12 @@
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.sources.composable
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
@@ -29,13 +28,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.navigation.NavController
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.SourceEntries 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 @Composable
fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) { fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
Row( Row(
@@ -86,4 +95,4 @@ fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }
} }
} }
} }
} }

View File

@@ -41,7 +41,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI 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.getGalleryInfo
import xyz.quaver.pupil.sources.hitomi.lib.getReferer import xyz.quaver.pupil.sources.hitomi.lib.getReferer
import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
class Hitomi(app: Application) : Source(), DIAware { class Hitomi(app: Application) : Source(), DIAware {
override val di by closestDI(app) override val di by closestDI(app)
@@ -73,9 +71,9 @@ class Hitomi(app: Application) : Source(), DIAware {
override val iconResID: Int = R.drawable.hitomi override val iconResID: Int = R.drawable.hitomi
override fun NavGraphBuilder.navGraph(navController: NavController) { override fun NavGraphBuilder.navGraph(navController: NavController) {
navigation(startDestination = "search", route = name) { navigation(startDestination = "hitomi.la/search", route = name) {
composable("search") { Search(navController) } composable("hitomi.la/search") { Search(navController) }
composable("reader/{itemID}") { Reader(navController) } composable("hitomi.la/reader/{itemID}") { Reader(navController) }
} }
} }
@@ -94,16 +92,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var sourceSelectDialog by remember { mutableStateOf(false) } var sourceSelectDialog by remember { mutableStateOf(false) }
if (sourceSelectDialog) if (sourceSelectDialog)
SourceSelectDialog( SourceSelectDialog(navController, name) { sourceSelectDialog = false }
currentSource = name,
onDismissRequest = { sourceSelectDialog = false }
) {
sourceSelectDialog = false
navController.navigate("main/${it.name}") {
launchSingleTop = true
popUpTo("main/{source}") { inclusive = true }
}
}
LaunchedEffect(model.currentPage, model.sortByPopularity) { LaunchedEffect(model.currentPage, model.sortByPopularity) {
model.search() model.search()
@@ -188,7 +177,10 @@ class Hitomi(app: Application) : Source(), DIAware {
} }
} }
) { result -> ) { 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) val bookmark by bookmarkDao.contains(name, itemID).observeAsState(false)
LaunchedEffect(model) { LaunchedEffect(itemID) {
launch(Dispatchers.IO) { runCatching {
kotlin.runCatching { val galleryID = itemID.toInt()
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) }) { model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
append("Referer", getReferer(galleryID)) append("Referer", getReferer(galleryID))
}
}.onFailure {
model.error = true
} }
}.onFailure {
model.error = true
} }
} }

View File

@@ -1,101 +1,339 @@
///* /*
// * Pupil, Hitomi.la viewer for Android * Pupil, Hitomi.la viewer for Android
// * Copyright (C) 2021 tom5079 * Copyright (C) 2021 tom5079
// * *
// * This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
// * the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
// * (at your option) any later version. * (at your option) any later version.
// * *
// * This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// * GNU General Public License for more details. * GNU General Public License for more details.
// * *
// * You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
// * along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
// */ */
// package xyz.quaver.pupil.sources.manatoki
//package xyz.quaver.pupil.sources.manatoki
// import android.app.Application
//import android.app.Application import androidx.activity.compose.BackHandler
//import kotlinx.coroutines.Dispatchers import androidx.compose.foundation.Image
//import kotlinx.coroutines.channels.Channel import androidx.compose.foundation.background
//import kotlinx.coroutines.coroutineScope import androidx.compose.foundation.layout.*
//import kotlinx.coroutines.withContext import androidx.compose.foundation.lazy.LazyRow
//import kotlinx.parcelize.Parcelize import androidx.compose.foundation.lazy.items
//import org.jsoup.Jsoup import androidx.compose.foundation.rememberScrollState
//import org.kodein.di.DIAware import androidx.compose.foundation.shape.RoundedCornerShape
//import org.kodein.di.android.closestDI import androidx.compose.foundation.verticalScroll
//import org.kodein.log.LoggerFactory import androidx.compose.material.*
//import org.kodein.log.newLogger import androidx.compose.material.icons.Icons
//import xyz.quaver.pupil.R import androidx.compose.material.icons.filled.Settings
//import xyz.quaver.pupil.sources.ItemInfo import androidx.compose.runtime.*
//import xyz.quaver.pupil.sources.Source import androidx.compose.runtime.livedata.observeAsState
// import androidx.compose.runtime.saveable.rememberSaveable
//@Parcelize import androidx.compose.ui.Modifier
//class ManatokiItemInfo( import androidx.compose.ui.graphics.Color
// override val itemID: String, import androidx.compose.ui.res.painterResource
// override val title: String import androidx.compose.ui.text.style.TextAlign
//) : ItemInfo { import androidx.compose.ui.text.style.TextOverflow
// override val source: String = "manatoki.net" import androidx.compose.ui.unit.dp
//} import androidx.lifecycle.viewmodel.compose.viewModel
// import androidx.navigation.NavController
//class Manatoki(app: Application) : Source(), DIAware { import androidx.navigation.NavGraphBuilder
// override val di by closestDI(app) import androidx.navigation.compose.composable
// import androidx.navigation.navigation
// private val logger = newLogger(LoggerFactory.default) import com.google.accompanist.insets.LocalWindowInsets
// import com.google.accompanist.insets.navigationBarsPadding
// override val name = "manatoki.net" import com.google.accompanist.insets.rememberInsetsPaddingValues
// override val availableSortMode: List<String> = emptyList() import com.google.accompanist.insets.ui.Scaffold
// override val iconResID: Int = R.drawable.manatoki import com.google.accompanist.insets.ui.TopAppBar
// import io.ktor.client.*
// override suspend fun search( import kotlinx.coroutines.channels.Channel
// query: String, import kotlinx.coroutines.launch
// range: IntRange, import org.kodein.di.DIAware
// sortMode: Int import org.kodein.di.android.closestDI
// ): Pair<Channel<ItemInfo>, Int> { import org.kodein.di.compose.rememberInstance
// TODO("Not yet implemented") import org.kodein.log.LoggerFactory
// } import org.kodein.log.newLogger
// import xyz.quaver.pupil.R
// override suspend fun images(itemID: String): List<String> = coroutineScope { import xyz.quaver.pupil.db.AppDatabase
// val jsoup = withContext(Dispatchers.IO) { import xyz.quaver.pupil.sources.Source
// Jsoup.connect("https://manatoki116.net/comic/$itemID").get() import xyz.quaver.pupil.sources.composable.ReaderBase
// } import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
// import xyz.quaver.pupil.sources.composable.SourceSelectDialog
// val htmlData = jsoup import xyz.quaver.pupil.sources.manatoki.composable.BoardButton
// .selectFirst(".view-padding > script")!! import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
// .data() import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
// .splitToSequence('\n') import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
// .fold(StringBuilder()) { sb, line -> import java.util.concurrent.ConcurrentHashMap
// if (!line.startsWith("html_data")) return@fold sb
// class Manatoki(app: Application) : Source(), DIAware {
// line.drop(12).dropLast(2).split('.').forEach { override val di by closestDI(app)
// if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
// } private val logger = newLogger(LoggerFactory.default)
// sb
// }.toString() override val name = "manatoki.net"
// override val iconResID = R.drawable.manatoki
// Jsoup.parse(htmlData)
// .select("img[^data-]:not([style])") private val readerInfoChannel = ConcurrentHashMap<String, Channel<ReaderInfo>>()
// .map {
// it.attributes() override fun NavGraphBuilder.navGraph(navController: NavController) {
// .first { it.key.startsWith("data-") } navigation(route = name, startDestination = "manatoki.net/") {
// .value composable("manatoki.net/") { Main(navController) }
// } composable("manatoki.net/reader/{itemID}") { Reader(navController) }
// } }
// }
// override suspend fun info(itemID: String): ItemInfo = coroutineScope {
// val jsoup = withContext(Dispatchers.IO) { @OptIn(ExperimentalMaterialApi::class)
// Jsoup.connect("https://manatoki116.net/comic/$itemID").get() @Composable
// } fun Main(navController: NavController) {
// val model: MainViewModel = viewModel()
// val title = jsoup.selectFirst(".toon-title")!!.ownText()
// val client: HttpClient by rememberInstance()
// ManatokiItemInfo(
// itemID, val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
// title 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<ReaderInfo>()
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)
}
}
)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
)
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
)
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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
)
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>,
val type: String,
val thumbsUpCount: Int,
val entries: List<MangaListingEntry>
): Parcelable
@Parcelize
@Serializable
data class ReaderInfo(
val itemID: String,
val title: String,
val urls: List<String>
): 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
)
)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Thumbnail>()
val mangaList = mutableStateListOf<Thumbnail>()
val topWeekly = mutableStateListOf<TopWeekly>()
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)
}
}
}
}
}

View File

@@ -22,10 +22,16 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi 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.core.view.WindowCompat
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
@@ -53,9 +59,34 @@ class MainActivity : ComponentActivity(), DIAware {
ProvideWindowInsets { ProvideWindowInsets {
val navController = rememberNavController() 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 { sources.forEach {
it.second.run { navGraph(navController) } it.second.run {
navGraph(navController)
}
} }
} }
} }

View File

@@ -18,6 +18,9 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.view.View import android.view.View
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -82,3 +85,15 @@ fun rememberFileXImageSource(file: FileX) = remember {
fun sha256(data: ByteArray) : ByteArray { fun sha256(data: ByteArray) : ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data) 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
}