[Manatoki] Main/Reader OK
This commit is contained in:
@@ -110,10 +110,10 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
|
||||
|
||||
implementation("androidx.room:room-runtime:2.3.0")
|
||||
annotationProcessor("androidx.room:room-compiler:2.3.0")
|
||||
kapt("androidx.room:room-compiler:2.3.0")
|
||||
implementation("androidx.room:room-ktx:2.3.0")
|
||||
implementation("androidx.room:room-runtime:2.4.0")
|
||||
annotationProcessor("androidx.room:room-compiler:2.4.0")
|
||||
kapt("androidx.room:room-compiler:2.4.0")
|
||||
implementation("androidx.room:room-ktx:2.4.0")
|
||||
|
||||
implementation("androidx.datastore:datastore:1.0.0")
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
@@ -138,6 +138,8 @@ dependencies {
|
||||
implementation("xyz.quaver:documentfilex:0.7.1")
|
||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha11-SNAPSHOT")
|
||||
|
||||
implementation("com.google.guava:guava:31.0.1-android")
|
||||
|
||||
implementation("org.kodein.log:kodein-log:0.11.1")
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import org.kodein.di.*
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.sources.manatoki.Manatoki
|
||||
|
||||
abstract class Source {
|
||||
abstract val name: String
|
||||
@@ -39,7 +40,7 @@ val sourceModule = DI.Module(name = "source") {
|
||||
listOf<(Application) -> (Source)>(
|
||||
{ Hitomi(it) },
|
||||
//{ Hiyobi_io(it) },
|
||||
//{ Manatoki(it) }
|
||||
{ Manatoki(it) }
|
||||
).forEach { source ->
|
||||
inSet { singleton { source(instance()).let { it.name to it } } }
|
||||
}
|
||||
|
||||
@@ -41,13 +41,16 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsPadding
|
||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||
import com.google.accompanist.insets.ui.Scaffold
|
||||
import com.google.accompanist.insets.ui.TopAppBar
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -63,6 +66,7 @@ import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.ui.theme.Orange500
|
||||
import xyz.quaver.pupil.util.NetworkCache
|
||||
import xyz.quaver.pupil.util.activity
|
||||
import xyz.quaver.pupil.util.rememberFileXImageSource
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -75,9 +79,6 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
|
||||
|
||||
private val database: AppDatabase by instance()
|
||||
|
||||
private val historyDao = database.historyDao()
|
||||
private val bookmarkDao = database.bookmarkDao()
|
||||
|
||||
var error by mutableStateOf(false)
|
||||
|
||||
var title by mutableStateOf<String?>(null)
|
||||
@@ -171,6 +172,19 @@ fun ReaderBase(
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val snackbarCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(model.isFullscreen) {
|
||||
context.activity?.window?.let { window ->
|
||||
ViewCompat.getWindowInsetsController(window.decorView)?.let {
|
||||
if (model.isFullscreen) {
|
||||
it.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
it.hide(WindowInsetsCompat.Type.systemBars())
|
||||
} else
|
||||
it.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.error)
|
||||
stringResource(R.string.reader_failed_to_find_gallery).let {
|
||||
snackbarCoroutineScope.launch {
|
||||
@@ -181,16 +195,6 @@ fun ReaderBase(
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = MaterialTheme.colors.isLight
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!model.isFullscreen)
|
||||
@@ -225,6 +229,7 @@ fun ReaderBase(
|
||||
floatingActionButton = {
|
||||
if (!model.isFullscreen)
|
||||
MultipleFloatingActionButton(
|
||||
modifier = Modifier.navigationBarsPadding(),
|
||||
items = listOf(
|
||||
SubFabItem(
|
||||
icon = Icons.Default.Fullscreen,
|
||||
@@ -245,7 +250,8 @@ fun ReaderBase(
|
||||
Box(Modifier.padding(contentPadding)) {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||
) {
|
||||
itemsIndexed(model.imageList) { i, uri ->
|
||||
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
@@ -56,9 +55,8 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsPadding
|
||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||
import com.google.accompanist.insets.systemBarsPadding
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import com.google.accompanist.insets.ui.Scaffold
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.theme.LightBlue300
|
||||
import kotlin.math.*
|
||||
@@ -119,25 +117,15 @@ fun <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 searchBarDefaultOffset = systemBarsPaddingValues.calculateTopPadding() + 64.dp
|
||||
val searchBarDefaultOffset = statusBarsPaddingValues.calculateTopPadding() + 64.dp
|
||||
val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
|
||||
|
||||
var overscroll: Float? by remember { mutableStateOf(null) }
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = MaterialTheme.colors.isLight
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(navigationIconProgress) {
|
||||
navigationIcon.progress = navigationIconProgress
|
||||
}
|
||||
@@ -307,7 +295,7 @@ fun <T> SearchBase(
|
||||
|
||||
FloatingSearchBar(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.statusBarsPadding()
|
||||
.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
|
||||
query = model.query,
|
||||
onQueryChange = { model.query = it },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -13,13 +13,12 @@
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <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.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -29,13 +28,23 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.navigation.NavController
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
|
||||
@Composable
|
||||
fun SourceSelectDialog(navController: NavController, currentSource: String, onDismissRequest: () -> Unit = { }) {
|
||||
SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
|
||||
onDismissRequest()
|
||||
navController.navigate(it.name) {
|
||||
popUpTo(currentSource) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
|
||||
Row(
|
||||
@@ -86,4 +95,4 @@ fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
@@ -57,7 +56,6 @@ import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
||||
import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
|
||||
import xyz.quaver.pupil.sources.hitomi.lib.getReferer
|
||||
import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
|
||||
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
|
||||
|
||||
class Hitomi(app: Application) : Source(), DIAware {
|
||||
override val di by closestDI(app)
|
||||
@@ -73,9 +71,9 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
override val iconResID: Int = R.drawable.hitomi
|
||||
|
||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||
navigation(startDestination = "search", route = name) {
|
||||
composable("search") { Search(navController) }
|
||||
composable("reader/{itemID}") { Reader(navController) }
|
||||
navigation(startDestination = "hitomi.la/search", route = name) {
|
||||
composable("hitomi.la/search") { Search(navController) }
|
||||
composable("hitomi.la/reader/{itemID}") { Reader(navController) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +92,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
var sourceSelectDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (sourceSelectDialog)
|
||||
SourceSelectDialog(
|
||||
currentSource = name,
|
||||
onDismissRequest = { sourceSelectDialog = false }
|
||||
) {
|
||||
sourceSelectDialog = false
|
||||
navController.navigate("main/${it.name}") {
|
||||
launchSingleTop = true
|
||||
popUpTo("main/{source}") { inclusive = true }
|
||||
}
|
||||
}
|
||||
SourceSelectDialog(navController, name) { sourceSelectDialog = false }
|
||||
|
||||
LaunchedEffect(model.currentPage, model.sortByPopularity) {
|
||||
model.search()
|
||||
@@ -188,7 +177,10 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
}
|
||||
}
|
||||
) { result ->
|
||||
navController.navigate("reader/${result.itemID}")
|
||||
logger.info {
|
||||
result.toString()
|
||||
}
|
||||
navController.navigate("hitomi.la/reader/${result.itemID}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,21 +201,19 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
|
||||
val bookmark by bookmarkDao.contains(name, itemID).observeAsState(false)
|
||||
|
||||
LaunchedEffect(model) {
|
||||
launch(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val galleryID = itemID.toInt()
|
||||
LaunchedEffect(itemID) {
|
||||
runCatching {
|
||||
val galleryID = itemID.toInt()
|
||||
|
||||
val galleryInfo = getGalleryInfo(client, galleryID)
|
||||
val galleryInfo = getGalleryInfo(client, galleryID)
|
||||
|
||||
model.title = galleryInfo.title
|
||||
model.title = galleryInfo.title
|
||||
|
||||
model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
|
||||
append("Referer", getReferer(galleryID))
|
||||
}
|
||||
}.onFailure {
|
||||
model.error = true
|
||||
model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
|
||||
append("Referer", getReferer(galleryID))
|
||||
}
|
||||
}.onFailure {
|
||||
model.error = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +1,339 @@
|
||||
///*
|
||||
// * Pupil, Hitomi.la viewer for Android
|
||||
// * Copyright (C) 2021 tom5079
|
||||
// *
|
||||
// * This program is free software: you can redistribute it and/or modify
|
||||
// * it under the terms of the GNU General Public License as published by
|
||||
// * the Free Software Foundation, either version 3 of the License, or
|
||||
// * (at your option) any later version.
|
||||
// *
|
||||
// * This program is distributed in the hope that it will be useful,
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// * GNU General Public License for more details.
|
||||
// *
|
||||
// * You should have received a copy of the GNU General Public License
|
||||
// * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
// */
|
||||
//
|
||||
//package xyz.quaver.pupil.sources.manatoki
|
||||
//
|
||||
//import android.app.Application
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.channels.Channel
|
||||
//import kotlinx.coroutines.coroutineScope
|
||||
//import kotlinx.coroutines.withContext
|
||||
//import kotlinx.parcelize.Parcelize
|
||||
//import org.jsoup.Jsoup
|
||||
//import org.kodein.di.DIAware
|
||||
//import org.kodein.di.android.closestDI
|
||||
//import org.kodein.log.LoggerFactory
|
||||
//import org.kodein.log.newLogger
|
||||
//import xyz.quaver.pupil.R
|
||||
//import xyz.quaver.pupil.sources.ItemInfo
|
||||
//import xyz.quaver.pupil.sources.Source
|
||||
//
|
||||
//@Parcelize
|
||||
//class ManatokiItemInfo(
|
||||
// override val itemID: String,
|
||||
// override val title: String
|
||||
//) : ItemInfo {
|
||||
// override val source: String = "manatoki.net"
|
||||
//}
|
||||
//
|
||||
//class Manatoki(app: Application) : Source(), DIAware {
|
||||
// override val di by closestDI(app)
|
||||
//
|
||||
// private val logger = newLogger(LoggerFactory.default)
|
||||
//
|
||||
// override val name = "manatoki.net"
|
||||
// override val availableSortMode: List<String> = emptyList()
|
||||
// override val iconResID: Int = R.drawable.manatoki
|
||||
//
|
||||
// override suspend fun search(
|
||||
// query: String,
|
||||
// range: IntRange,
|
||||
// sortMode: Int
|
||||
// ): Pair<Channel<ItemInfo>, Int> {
|
||||
// TODO("Not yet implemented")
|
||||
// }
|
||||
//
|
||||
// override suspend fun images(itemID: String): List<String> = coroutineScope {
|
||||
// val jsoup = withContext(Dispatchers.IO) {
|
||||
// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
|
||||
// }
|
||||
//
|
||||
// val htmlData = jsoup
|
||||
// .selectFirst(".view-padding > script")!!
|
||||
// .data()
|
||||
// .splitToSequence('\n')
|
||||
// .fold(StringBuilder()) { sb, line ->
|
||||
// if (!line.startsWith("html_data")) return@fold sb
|
||||
//
|
||||
// line.drop(12).dropLast(2).split('.').forEach {
|
||||
// if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
|
||||
// }
|
||||
// sb
|
||||
// }.toString()
|
||||
//
|
||||
// Jsoup.parse(htmlData)
|
||||
// .select("img[^data-]:not([style])")
|
||||
// .map {
|
||||
// it.attributes()
|
||||
// .first { it.key.startsWith("data-") }
|
||||
// .value
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override suspend fun info(itemID: String): ItemInfo = coroutineScope {
|
||||
// val jsoup = withContext(Dispatchers.IO) {
|
||||
// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
|
||||
// }
|
||||
//
|
||||
// val title = jsoup.selectFirst(".toon-title")!!.ownText()
|
||||
//
|
||||
// ManatokiItemInfo(
|
||||
// itemID,
|
||||
// title
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//}
|
||||
/*
|
||||
* 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.app.Application
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsPadding
|
||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||
import com.google.accompanist.insets.ui.Scaffold
|
||||
import com.google.accompanist.insets.ui.TopAppBar
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.composable.ReaderBase
|
||||
import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
|
||||
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.BoardButton
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
||||
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class Manatoki(app: Application) : Source(), DIAware {
|
||||
override val di by closestDI(app)
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
override val name = "manatoki.net"
|
||||
override val iconResID = R.drawable.manatoki
|
||||
|
||||
private val readerInfoChannel = ConcurrentHashMap<String, Channel<ReaderInfo>>()
|
||||
|
||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||
navigation(route = name, startDestination = "manatoki.net/") {
|
||||
composable("manatoki.net/") { Main(navController) }
|
||||
composable("manatoki.net/reader/{itemID}") { Reader(navController) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun Main(navController: NavController) {
|
||||
val model: MainViewModel = viewModel()
|
||||
|
||||
val client: HttpClient by rememberInstance()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
||||
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val onListing: (MangaListing) -> Unit = {
|
||||
mangaListing = it
|
||||
logger.info {
|
||||
it.toString()
|
||||
}
|
||||
coroutineScope.launch {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
|
||||
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
||||
val channel = Channel<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.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
@@ -53,9 +59,34 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
ProvideWindowInsets {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController, startDestination = "hitomi.la") {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = MaterialTheme.colors.isLight
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(navController, startDestination = "main") {
|
||||
composable("main") {
|
||||
var launched by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!launched) {
|
||||
val source = it.arguments?.getString("source") ?: "hitomi.la"
|
||||
navController.navigate(source)
|
||||
launched = true
|
||||
} else {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
sources.forEach {
|
||||
it.second.run { navGraph(navController) }
|
||||
it.second.run {
|
||||
navGraph(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -82,3 +85,15 @@ fun rememberFileXImageSource(file: FileX) = remember {
|
||||
fun sha256(data: ByteArray) : ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
||||
}
|
||||
|
||||
val Context.activity: Activity?
|
||||
get() {
|
||||
var currentContext = this
|
||||
while (currentContext is ContextWrapper) {
|
||||
if (currentContext is Activity)
|
||||
return currentContext
|
||||
currentContext = currentContext.baseContext
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user