[Manatoki] Main/Reader OK
This commit is contained in:
12
.idea/deploymentTargetDropDown.xml
generated
12
.idea/deploymentTargetDropDown.xml
generated
@@ -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>
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 } } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 = { }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
Normal file
183
app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
Normal 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user