Reader bug fix
This commit is contained in:
8
.idea/deploymentTargetDropDown.xml
generated
8
.idea/deploymentTargetDropDown.xml
generated
@@ -1,9 +1,9 @@
|
|||||||
<?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">
|
||||||
<targetSelectedWithDropDown>
|
<runningDeviceTargetSelectedWithDropDown>
|
||||||
<Target>
|
<Target>
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
<type value="RUNNING_DEVICE_TARGET" />
|
||||||
<deviceKey>
|
<deviceKey>
|
||||||
<Key>
|
<Key>
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</Key>
|
</Key>
|
||||||
</deviceKey>
|
</deviceKey>
|
||||||
</Target>
|
</Target>
|
||||||
</targetSelectedWithDropDown>
|
</runningDeviceTargetSelectedWithDropDown>
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-12-20T01:51:44.761422Z" />
|
<timeTargetWasSelectedWithDropDown value="2021-12-20T09:02:43.106748Z" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -136,7 +136,7 @@ dependencies {
|
|||||||
implementation("ru.noties.markwon:core:3.1.0")
|
implementation("ru.noties.markwon:core:3.1.0")
|
||||||
|
|
||||||
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-alpha13-SNAPSHOT")
|
||||||
|
|
||||||
implementation("com.google.guava:guava:31.0.1-android")
|
implementation("com.google.guava:guava:31.0.1-android")
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.google.firebase.ktx.Firebase
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.features.*
|
||||||
|
import io.ktor.client.features.cache.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
import io.ktor.client.features.json.serializer.*
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
@@ -73,8 +74,12 @@ class Pupil : Application(), DIAware {
|
|||||||
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
connectTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
connectTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
}
|
}
|
||||||
|
install(HttpCache)
|
||||||
|
|
||||||
BrowserUserAgent()
|
install(UserAgent) {
|
||||||
|
agent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36"
|
||||||
|
}
|
||||||
|
//BrowserUserAgent()
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BrokenImage
|
import androidx.compose.material.icons.filled.BrokenImage
|
||||||
import androidx.compose.material.icons.filled.Fullscreen
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.material.icons.filled.StarOutline
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -39,7 +36,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -47,19 +43,22 @@ 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.TopAppBar
|
|
||||||
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
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
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
|
||||||
|
import org.kodein.log.LoggerFactory
|
||||||
|
import org.kodein.log.newLogger
|
||||||
import xyz.quaver.graphics.subsampledimage.*
|
import xyz.quaver.graphics.subsampledimage.*
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -68,33 +67,47 @@ 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.activity
|
||||||
import xyz.quaver.pupil.util.rememberFileXImageSource
|
import xyz.quaver.pupil.util.rememberFileXImageSource
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
|
|
||||||
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
private val cache: NetworkCache by instance()
|
private val cache: NetworkCache by instance()
|
||||||
|
|
||||||
var isFullscreen by mutableStateOf(false)
|
var fullscreen by mutableStateOf(false)
|
||||||
|
|
||||||
private val database: AppDatabase by instance()
|
private val database: AppDatabase by instance()
|
||||||
|
|
||||||
var error by mutableStateOf(false)
|
var error by mutableStateOf(false)
|
||||||
|
|
||||||
var title by mutableStateOf<String?>(null)
|
|
||||||
|
|
||||||
var imageCount by mutableStateOf(0)
|
var imageCount by mutableStateOf(0)
|
||||||
|
|
||||||
val imageList = mutableStateListOf<Uri?>()
|
val imageList = mutableStateListOf<Uri?>()
|
||||||
val progressList = mutableStateListOf<Float>()
|
val progressList = mutableStateListOf<Float>()
|
||||||
|
|
||||||
|
private val progressCollectJobs = ConcurrentHashMap<Int, Job>()
|
||||||
|
|
||||||
private val totalProgressMutex = Mutex()
|
private val totalProgressMutex = Mutex()
|
||||||
var totalProgress by mutableStateOf(0)
|
var totalProgress by mutableStateOf(0)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private var urls: List<String>? = null
|
||||||
|
|
||||||
|
var loadJob: Job? = null
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun load(urls: List<String>, headerBuilder: HeadersBuilder.() -> Unit = { }) {
|
fun load(urls: List<String>, headerBuilder: HeadersBuilder.() -> Unit = { }) {
|
||||||
|
this.urls = urls
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
loadJob?.cancelAndJoin()
|
||||||
|
progressList.clear()
|
||||||
|
imageList.clear()
|
||||||
|
totalProgressMutex.withLock {
|
||||||
|
totalProgress = 0
|
||||||
|
}
|
||||||
|
|
||||||
imageCount = urls.size
|
imageCount = urls.size
|
||||||
|
|
||||||
progressList.addAll(List(imageCount) { 0f })
|
progressList.addAll(List(imageCount) { 0f })
|
||||||
@@ -103,79 +116,61 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
|
|||||||
totalProgress = 0
|
totalProgress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
urls.forEachIndexed { index, url ->
|
loadJob = launch {
|
||||||
when (val scheme = url.takeWhile { it != ':' }) {
|
urls.forEachIndexed { index, url ->
|
||||||
"http", "https" -> {
|
when (val scheme = url.takeWhile { it != ':' }) {
|
||||||
val (channel, file) = cache.load {
|
"http", "https" -> {
|
||||||
url(url)
|
val (flow, file) = cache.load {
|
||||||
headers(headerBuilder)
|
url(url)
|
||||||
}
|
headers(headerBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
if (channel.isClosedForReceive) {
|
|
||||||
imageList[index] = Uri.fromFile(file)
|
imageList[index] = Uri.fromFile(file)
|
||||||
totalProgressMutex.withLock {
|
progressCollectJobs[index] = launch {
|
||||||
totalProgress++
|
flow.takeWhile { it.isFinite() }.collect {
|
||||||
}
|
progressList[index] = it
|
||||||
} else {
|
|
||||||
channel.invokeOnClose { e ->
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (e == null) {
|
|
||||||
imageList[index] = Uri.fromFile(file)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
error(index)
|
|
||||||
}
|
|
||||||
imageList[index] = Uri.fromFile(file)
|
|
||||||
totalProgressMutex.withLock {
|
|
||||||
totalProgress++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
progressList[index] = flow.value
|
||||||
kotlin.runCatching {
|
|
||||||
for (progress in channel) {
|
|
||||||
progressList[index] = progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"content" -> {
|
||||||
|
imageList[index] = Uri.parse(url)
|
||||||
|
progressList[index] = Float.POSITIVE_INFINITY
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
|
||||||
}
|
}
|
||||||
"content" -> {
|
|
||||||
imageList[index] = Uri.parse(url)
|
|
||||||
progressList[index] = 1f
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun error(index: Int) {
|
fun error(index: Int) {
|
||||||
progressList[index] = -1f
|
progressList[index] = Float.NEGATIVE_INFINITY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
urls?.let { cache.free(it) }
|
||||||
|
cache.cleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderBase(
|
fun ReaderBase(
|
||||||
model: ReaderBaseViewModel,
|
modifier: Modifier = Modifier,
|
||||||
icon: @Composable () -> Unit = { },
|
model: ReaderBaseViewModel
|
||||||
bookmark: Boolean = false,
|
|
||||||
onToggleBookmark: () -> Unit = { }
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
|
||||||
|
|
||||||
val scaffoldState = rememberScaffoldState()
|
val scaffoldState = rememberScaffoldState()
|
||||||
val snackbarCoroutineScope = rememberCoroutineScope()
|
val snackbarCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(model.isFullscreen) {
|
LaunchedEffect(model.fullscreen) {
|
||||||
context.activity?.window?.let { window ->
|
context.activity?.window?.let { window ->
|
||||||
ViewCompat.getWindowInsetsController(window.decorView)?.let {
|
ViewCompat.getWindowInsetsController(window.decorView)?.let {
|
||||||
if (model.isFullscreen) {
|
if (model.fullscreen) {
|
||||||
it.systemBarsBehavior =
|
it.systemBarsBehavior =
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
it.hide(WindowInsetsCompat.Type.systemBars())
|
it.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
@@ -195,130 +190,79 @@ fun ReaderBase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Box(modifier) {
|
||||||
topBar = {
|
LazyColumn(
|
||||||
if (!model.isFullscreen)
|
Modifier
|
||||||
TopAppBar(
|
.fillMaxSize()
|
||||||
title = {
|
.align(Alignment.TopStart),
|
||||||
Text(
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
model.title ?: stringResource(R.string.reader_loading),
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||||
color = MaterialTheme.colors.onSecondary,
|
) {
|
||||||
maxLines = 1,
|
itemsIndexed(model.imageList) { i, uri ->
|
||||||
overflow = TextOverflow.Ellipsis
|
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { }) {
|
|
||||||
icon()
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = onToggleBookmark) {
|
Box(
|
||||||
Icon(
|
Modifier
|
||||||
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
|
.wrapContentHeight(state, 500.dp)
|
||||||
contentDescription = null,
|
.fillMaxWidth()
|
||||||
tint = Orange500
|
.border(1.dp, Color.Gray),
|
||||||
)
|
contentAlignment = Alignment.Center
|
||||||
}
|
) {
|
||||||
},
|
val progress = model.progressList.getOrNull(i) ?: 0f
|
||||||
contentPadding = rememberInsetsPaddingValues(
|
|
||||||
LocalWindowInsets.current.statusBars,
|
if (progress == Float.NEGATIVE_INFINITY)
|
||||||
applyBottom = false
|
Icon(Icons.Filled.BrokenImage, null, tint = Orange500)
|
||||||
)
|
else if (progress.isFinite())
|
||||||
)
|
Column(
|
||||||
},
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
floatingActionButton = {
|
|
||||||
if (!model.isFullscreen)
|
|
||||||
MultipleFloatingActionButton(
|
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
|
||||||
items = listOf(
|
|
||||||
SubFabItem(
|
|
||||||
icon = Icons.Default.Fullscreen,
|
|
||||||
label = stringResource(id = R.string.reader_fab_fullscreen)
|
|
||||||
) {
|
) {
|
||||||
model.isFullscreen = true
|
LinearProgressIndicator(progress)
|
||||||
|
Text((i + 1).toString())
|
||||||
}
|
}
|
||||||
),
|
else if (uri != null && progress == Float.POSITIVE_INFINITY) {
|
||||||
targetState = isFABExpanded,
|
val imageSource = kotlin.runCatching {
|
||||||
onStateChanged = {
|
rememberFileXImageSource(FileX(context, uri))
|
||||||
isFABExpanded = it
|
}.getOrNull()
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scaffoldState = scaffoldState,
|
|
||||||
snackbarHost = { scaffoldState.snackbarHostState }
|
|
||||||
) { contentPadding ->
|
|
||||||
Box(Modifier.padding(contentPadding)) {
|
|
||||||
LazyColumn(
|
|
||||||
Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
|
||||||
) {
|
|
||||||
itemsIndexed(model.imageList) { i, uri ->
|
|
||||||
val state = rememberSubSampledImageState(ScaleTypes.FIT_WIDTH)
|
|
||||||
|
|
||||||
Box(
|
if (imageSource != null)
|
||||||
Modifier
|
SubSampledImage(
|
||||||
.wrapContentHeight(state, 500.dp)
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.border(1.dp, Color.Gray),
|
.run {
|
||||||
contentAlignment = Alignment.Center
|
if (model.fullscreen)
|
||||||
) {
|
doubleClickCycleZoom(state, 2f)
|
||||||
if (uri == null)
|
else
|
||||||
model.progressList.getOrNull(i)?.let { progress ->
|
combinedClickable(
|
||||||
if (progress < 0f)
|
onLongClick = {
|
||||||
Icon(Icons.Filled.BrokenImage, null)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
LinearProgressIndicator(progress)
|
|
||||||
Text((i + 1).toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val imageSource = kotlin.runCatching {
|
|
||||||
rememberFileXImageSource(FileX(context, uri))
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
if (imageSource == null)
|
|
||||||
Icon(Icons.Default.BrokenImage, contentDescription = null)
|
|
||||||
else
|
|
||||||
SubSampledImage(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.run {
|
|
||||||
if (model.isFullscreen)
|
|
||||||
doubleClickCycleZoom(state, 2f)
|
|
||||||
else
|
|
||||||
combinedClickable(
|
|
||||||
onLongClick = {
|
|
||||||
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
model.isFullscreen = true
|
|
||||||
}
|
}
|
||||||
},
|
) {
|
||||||
imageSource = imageSource,
|
model.fullscreen = true
|
||||||
state = state
|
}
|
||||||
)
|
},
|
||||||
}
|
imageSource = imageSource,
|
||||||
|
state = state,
|
||||||
|
onError = {
|
||||||
|
model.error(i)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.totalProgress != model.imageCount)
|
|
||||||
LinearProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.TopCenter),
|
|
||||||
progress = model.progressList.map { abs(it) }.sum() / model.progressList.size,
|
|
||||||
color = MaterialTheme.colors.secondary
|
|
||||||
)
|
|
||||||
|
|
||||||
SnackbarHost(
|
|
||||||
scaffoldState.snackbarHostState,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.progressList.any { it.isFinite() })
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
progress = model.progressList.map { if (it.isInfinite()) 1f else abs(it) }.sum() / model.progressList.size,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
|
||||||
|
SnackbarHost(
|
||||||
|
scaffoldState.snackbarHostState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,27 +19,32 @@
|
|||||||
package xyz.quaver.pupil.sources.hitomi
|
package xyz.quaver.pupil.sources.hitomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Shuffle
|
|
||||||
import androidx.compose.material.icons.filled.Sort
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
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 com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
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 io.ktor.client.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
@@ -53,9 +58,11 @@ import xyz.quaver.pupil.db.AppDatabase
|
|||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
import xyz.quaver.pupil.sources.composable.*
|
import xyz.quaver.pupil.sources.composable.*
|
||||||
import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
|
||||||
|
import xyz.quaver.pupil.sources.hitomi.lib.GalleryInfo
|
||||||
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.theme.Orange500
|
||||||
|
|
||||||
class Hitomi(app: Application) : Source(), DIAware {
|
class Hitomi(app: Application) : Source(), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
@@ -195,44 +202,76 @@ class Hitomi(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID") ?: ""
|
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
|
||||||
|
|
||||||
if (itemID.isEmpty()) model.error = true
|
if (itemID == null) model.error = true
|
||||||
|
|
||||||
val bookmark by bookmarkDao.contains(name, itemID).observeAsState(false)
|
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
|
||||||
|
val galleryInfo by produceState<GalleryInfo?>(null) {
|
||||||
LaunchedEffect(itemID) {
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val galleryID = itemID.toInt()
|
val galleryID = itemID!!.toInt()
|
||||||
|
|
||||||
val galleryInfo = getGalleryInfo(client, galleryID)
|
value = getGalleryInfo(client, galleryID).also {
|
||||||
|
model.load(it.files.map { imageUrlFromImage(galleryID, it, false) }) {
|
||||||
model.title = galleryInfo.title
|
append("Referer", getReferer(galleryID))
|
||||||
|
}
|
||||||
model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
|
|
||||||
append("Referer", getReferer(galleryID))
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
model.error = true
|
model.error = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaderBase(
|
BackHandler {
|
||||||
model,
|
if (model.fullscreen) model.fullscreen = false
|
||||||
icon = {
|
else navController.popBackStack()
|
||||||
Image(
|
}
|
||||||
painter = painterResource(R.drawable.hitomi),
|
|
||||||
contentDescription = null,
|
Scaffold(
|
||||||
modifier = Modifier.size(24.dp)
|
topBar = {
|
||||||
)
|
if (!model.fullscreen)
|
||||||
},
|
TopAppBar(
|
||||||
bookmark = bookmark,
|
title = {
|
||||||
onToggleBookmark = {
|
Text(
|
||||||
coroutineScope.launch {
|
galleryInfo?.title ?: stringResource(R.string.reader_loading),
|
||||||
if (itemID.isEmpty() || bookmark) bookmarkDao.delete(name, itemID)
|
maxLines = 1,
|
||||||
else bookmarkDao.insert(name, itemID)
|
overflow = TextOverflow.Ellipsis
|
||||||
}
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton({ }) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.hitomi),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
itemID?.let {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (bookmark) bookmarkDao.delete(name, it)
|
||||||
|
else bookmarkDao.insert(name, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Orange500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.statusBars,
|
||||||
|
applyBottom = false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
) { contentPadding ->
|
||||||
|
ReaderBase(
|
||||||
|
Modifier.padding(contentPadding),
|
||||||
|
model
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,24 +18,27 @@
|
|||||||
package xyz.quaver.pupil.sources.manatoki
|
package xyz.quaver.pupil.sources.manatoki
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.util.LruCache
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.filled.StarOutline
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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
|
||||||
@@ -50,11 +53,13 @@ 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 io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
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.compose.rememberInstance
|
import org.kodein.di.compose.rememberInstance
|
||||||
|
import org.kodein.di.instance
|
||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -67,17 +72,20 @@ import xyz.quaver.pupil.sources.manatoki.composable.BoardButton
|
|||||||
import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
|
import xyz.quaver.pupil.sources.manatoki.composable.MangaListingBottomSheet
|
||||||
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
|
||||||
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
|
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import xyz.quaver.pupil.ui.theme.Orange500
|
||||||
|
|
||||||
class Manatoki(app: Application) : Source(), DIAware {
|
class Manatoki(app: Application) : Source(), DIAware {
|
||||||
override val di by closestDI(app)
|
override val di by closestDI(app)
|
||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
|
private val client: HttpClient by instance()
|
||||||
|
|
||||||
override val name = "manatoki.net"
|
override val name = "manatoki.net"
|
||||||
override val iconResID = R.drawable.manatoki
|
override val iconResID = R.drawable.manatoki
|
||||||
|
|
||||||
private val readerInfoChannel = ConcurrentHashMap<String, Channel<ReaderInfo>>()
|
private val readerInfoMutex = Mutex()
|
||||||
|
private val readerInfoCache = LruCache<String, ReaderInfo>(25)
|
||||||
|
|
||||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||||
navigation(route = name, startDestination = "manatoki.net/") {
|
navigation(route = name, startDestination = "manatoki.net/") {
|
||||||
@@ -91,8 +99,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
fun Main(navController: NavController) {
|
fun Main(navController: NavController) {
|
||||||
val model: MainViewModel = viewModel()
|
val model: MainViewModel = viewModel()
|
||||||
|
|
||||||
val client: HttpClient by rememberInstance()
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
||||||
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
@@ -100,22 +106,16 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
val onListing: (MangaListing) -> Unit = {
|
val onListing: (MangaListing) -> Unit = {
|
||||||
mangaListing = it
|
mangaListing = it
|
||||||
logger.info {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
coroutineScope.launch {
|
|
||||||
sheetState.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
||||||
val channel = Channel<ReaderInfo>()
|
|
||||||
readerInfoChannel[readerInfo.itemID] = channel
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
channel.send(readerInfo)
|
readerInfoMutex.withLock {
|
||||||
|
readerInfoCache.put(readerInfo.itemID, readerInfo)
|
||||||
|
}
|
||||||
|
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
||||||
|
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
|
||||||
}
|
}
|
||||||
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceSelectDialog by remember { mutableStateOf(false) }
|
var sourceSelectDialog by remember { mutableStateOf(false) }
|
||||||
@@ -124,11 +124,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
SourceSelectDialog(navController, name) { sourceSelectDialog = false }
|
SourceSelectDialog(navController, name) { sourceSelectDialog = false }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
navController.backQueue.forEach {
|
|
||||||
logger.info {
|
|
||||||
it.destination.route.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.load()
|
model.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +193,10 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
) {
|
) {
|
||||||
items(model.recentUpload) { item ->
|
items(model.recentUpload) { item ->
|
||||||
Thumbnail(item) {
|
Thumbnail(item) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mangaListing = null
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
client.getItem(it, onListing, onReader)
|
client.getItem(it, onListing, onReader)
|
||||||
}
|
}
|
||||||
@@ -237,6 +236,10 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
) {
|
) {
|
||||||
items(model.mangaList) { item ->
|
items(model.mangaList) { item ->
|
||||||
Thumbnail(item) {
|
Thumbnail(item) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mangaListing = null
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
client.getItem(it, onListing, onReader)
|
client.getItem(it, onListing, onReader)
|
||||||
}
|
}
|
||||||
@@ -250,29 +253,49 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
model.topWeekly.forEachIndexed { index, item ->
|
model.topWeekly.forEachIndexed { index, item ->
|
||||||
Row(
|
Card(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
modifier = Modifier.clickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mangaListing = null
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
client.getItem(item.itemID, onListing, onReader)
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
(index + 1).toString(),
|
modifier = Modifier.height(IntrinsicSize.Min),
|
||||||
modifier = Modifier
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
.background(Color(0xFF64C3F5))
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
.width(24.dp),
|
) {
|
||||||
color = Color.White,
|
Box(
|
||||||
textAlign = TextAlign.Center
|
modifier = Modifier
|
||||||
)
|
.background(Color(0xFF64C3F5))
|
||||||
|
.width(24.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
(index + 1).toString(),
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
item.title,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f).padding(0.dp, 4.dp),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
item.count,
|
item.count,
|
||||||
color = Color(0xFFFF4500)
|
color = Color(0xFFFF4500)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,6 +307,7 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Reader(navController: NavController) {
|
fun Reader(navController: NavController) {
|
||||||
val model: ReaderBaseViewModel = viewModel()
|
val model: ReaderBaseViewModel = viewModel()
|
||||||
@@ -294,46 +318,127 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
|
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
|
||||||
|
var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(itemID) {
|
||||||
val channel = itemID?.let { readerInfoChannel.remove(it) }
|
if (itemID != null)
|
||||||
|
readerInfoMutex.withLock {
|
||||||
if (channel == null)
|
readerInfoCache.get(itemID)?.let {
|
||||||
model.error = true
|
readerInfo = it
|
||||||
else {
|
model.load(it.urls)
|
||||||
val readerInfo = channel.receive()
|
} ?: run {
|
||||||
|
model.error = true
|
||||||
model.title = readerInfo.title
|
}
|
||||||
model.load(readerInfo.urls)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
|
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
|
||||||
|
|
||||||
|
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
||||||
|
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
if (model.isFullscreen)
|
when {
|
||||||
model.isFullscreen = false
|
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
|
||||||
else
|
model.fullscreen -> model.fullscreen = false
|
||||||
navController.popBackStack()
|
else -> navController.popBackStack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReaderBase(
|
ModalBottomSheetLayout(
|
||||||
model,
|
sheetState = sheetState,
|
||||||
icon = {
|
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
|
||||||
Image(
|
sheetContent = {
|
||||||
painter = painterResource(R.drawable.manatoki),
|
MangaListingBottomSheet(mangaListing) {
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
bookmark = bookmark,
|
|
||||||
onToggleBookmark = {
|
|
||||||
if (itemID != null)
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (bookmark) bookmarkDao.delete(name, itemID)
|
client.getItem(
|
||||||
else bookmarkDao.insert(name, itemID)
|
it,
|
||||||
|
onReader = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
readerInfoMutex.withLock {
|
||||||
|
readerInfoCache.put(it.itemID, it)
|
||||||
|
}
|
||||||
|
navController.navigate("manatoki.net/reader/${it.itemID}") {
|
||||||
|
popUpTo("manatoki.net/reader/$itemID") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
if (!model.fullscreen)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
readerInfo?.title ?: stringResource(R.string.reader_loading),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton({ }) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.manatoki),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
itemID?.let {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (bookmark) bookmarkDao.delete(name, it)
|
||||||
|
else bookmarkDao.insert(name, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Orange500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(
|
||||||
|
LocalWindowInsets.current.statusBars,
|
||||||
|
applyBottom = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
|
onClick = {
|
||||||
|
readerInfo?.let {
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (mangaListing?.itemID != it.listingItemID)
|
||||||
|
client.getItem(it.listingItemID, onListing = {
|
||||||
|
mangaListing = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.List,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
ReaderBase(
|
||||||
|
Modifier.padding(contentPadding),
|
||||||
|
model
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,12 +98,16 @@ fun MangaListingBottomSheet(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
mangaListing?.run {
|
if (mangaListing == null)
|
||||||
|
CircularProgressIndicator(Modifier.navigationBarsPadding().padding(16.dp).align(Alignment.Center))
|
||||||
|
else
|
||||||
MangaListingBottomSheetLayout(
|
MangaListingBottomSheetLayout(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text("첫화보기") },
|
text = { Text("첫화보기") },
|
||||||
onClick = { entries.lastOrNull()?.let { onOpenItem(it.itemID) } }
|
onClick = {
|
||||||
|
mangaListing.entries.lastOrNull()?.let { onOpenItem(it.itemID) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
top = {
|
top = {
|
||||||
@@ -114,7 +118,7 @@ fun MangaListingBottomSheet(
|
|||||||
.padding(0.dp, 0.dp, 0.dp, 4.dp),
|
.padding(0.dp, 0.dp, 0.dp, 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
val painter = rememberImagePainter(thumbnail)
|
val painter = rememberImagePainter(mangaListing.thumbnail)
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -135,13 +139,13 @@ fun MangaListingBottomSheet(
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
mangaListing.title,
|
||||||
style = MaterialTheme.typography.h5,
|
style = MaterialTheme.typography.h5,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentAlpha provides 0.7f) {
|
CompositionLocalProvider(LocalContentAlpha provides 0.7f) {
|
||||||
Text("작가: $author")
|
Text("작가: ${mangaListing.author}")
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("분류: ")
|
Text("분류: ")
|
||||||
@@ -151,7 +155,7 @@ fun MangaListingBottomSheet(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
mainAxisSpacing = 8.dp
|
mainAxisSpacing = 8.dp
|
||||||
) {
|
) {
|
||||||
tags.forEach {
|
mangaListing.tags.forEach {
|
||||||
Card(
|
Card(
|
||||||
elevation = 4.dp
|
elevation = 4.dp
|
||||||
) {
|
) {
|
||||||
@@ -166,7 +170,7 @@ fun MangaListingBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("발행구분: $type")
|
Text("발행구분: ${mangaListing.type}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +181,7 @@ fun MangaListingBottomSheet(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||||
) {
|
) {
|
||||||
items(entries) { entry ->
|
items(mangaListing.entries) { entry ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -200,10 +204,5 @@ fun MangaListingBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} ?: run {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
Modifier.align(Alignment.Center).navigationBarsPadding().padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,13 +71,14 @@ data class MangaListing(
|
|||||||
data class ReaderInfo(
|
data class ReaderInfo(
|
||||||
val itemID: String,
|
val itemID: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val urls: List<String>
|
val urls: List<String>,
|
||||||
|
val listingItemID: String
|
||||||
): Parcelable
|
): Parcelable
|
||||||
|
|
||||||
suspend fun HttpClient.getItem(
|
suspend fun HttpClient.getItem(
|
||||||
itemID: String,
|
itemID: String,
|
||||||
onListing: (MangaListing) -> Unit,
|
onListing: (MangaListing) -> Unit = { },
|
||||||
onReader: (ReaderInfo) -> Unit
|
onReader: (ReaderInfo) -> Unit = { }
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
waitForRateLimit()
|
waitForRateLimit()
|
||||||
val content: String = get("https://manatoki116.net/comic/$itemID")
|
val content: String = get("https://manatoki116.net/comic/$itemID")
|
||||||
@@ -110,7 +111,16 @@ suspend fun HttpClient.getItem(
|
|||||||
|
|
||||||
val title = doc.getElementsByClass("toon-title").first()!!.ownText()
|
val title = doc.getElementsByClass("toon-title").first()!!.ownText()
|
||||||
|
|
||||||
onReader(ReaderInfo(itemID, title, urls))
|
val listingItemID = doc.select("a:contains(전체목록)").first()!!.attr("href").takeLastWhile { it != '/' }
|
||||||
|
|
||||||
|
onReader(
|
||||||
|
ReaderInfo(
|
||||||
|
itemID,
|
||||||
|
title,
|
||||||
|
urls,
|
||||||
|
listingItemID
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val titleBlock = doc.selectFirst("div.view-title")!!
|
val titleBlock = doc.selectFirst("div.view-title")!!
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
misoPostGallery[1]
|
misoPostGallery[1]
|
||||||
.select(".post-image > a").also { logger.info { it.size.toString() } }
|
.select(".post-image > a")
|
||||||
.forEach { entry ->
|
.forEach { entry ->
|
||||||
val itemID = entry.attr("href").takeLastWhile { it != '/' }
|
val itemID = entry.attr("href").takeLastWhile { it != '/' }
|
||||||
val title = entry.selectFirst("div.in-subject")!!.ownText()
|
val title = entry.selectFirst("div.in-subject")!!.ownText()
|
||||||
@@ -99,7 +99,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
val misoPostList = doc.select(".miso-post-list")
|
val misoPostList = doc.select(".miso-post-list")
|
||||||
|
|
||||||
misoPostList[4]
|
misoPostList[4]
|
||||||
.select(".post-row > a")
|
.select(".post-row > a").also { logger.info { it.size.toString() } }
|
||||||
.forEach { entry ->
|
.forEach { entry ->
|
||||||
yield()
|
yield()
|
||||||
val itemID = entry.attr("href").takeLastWhile { it != '/' }
|
val itemID = entry.attr("href").takeLastWhile { it != '/' }
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!launched) {
|
if (!launched) {
|
||||||
val source = it.arguments?.getString("source") ?: "hitomi.la"
|
val source = it.arguments?.getString("source") ?: "manatoki.net"
|
||||||
navController.navigate(source)
|
navController.navigate(source)
|
||||||
launched = true
|
launched = true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
@@ -30,14 +31,17 @@ import io.ktor.util.collections.*
|
|||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
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
|
||||||
import org.kodein.log.LoggerFactory
|
import org.kodein.log.LoggerFactory
|
||||||
import org.kodein.log.newLogger
|
import org.kodein.log.newLogger
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -55,8 +59,13 @@ class NetworkCache(context: Context) : DIAware {
|
|||||||
|
|
||||||
private val cacheDir = File(context.cacheDir, "networkcache")
|
private val cacheDir = File(context.cacheDir, "networkcache")
|
||||||
|
|
||||||
private val channel = ConcurrentHashMap<String, Channel<Float>>()
|
private val flowMutex = Mutex()
|
||||||
|
private val flow = ConcurrentHashMap<String, MutableStateFlow<Float>>()
|
||||||
|
|
||||||
|
private val requestsMutex = Mutex()
|
||||||
private val requests = ConcurrentHashMap<String, Job>()
|
private val requests = ConcurrentHashMap<String, Job>()
|
||||||
|
|
||||||
|
private val activeFilesMutex = Mutex()
|
||||||
private val activeFiles = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
private val activeFiles = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
||||||
|
|
||||||
private fun urlToFilename(url: String): String {
|
private fun urlToFilename(url: String): String {
|
||||||
@@ -69,21 +78,33 @@ class NetworkCache(context: Context) : DIAware {
|
|||||||
cacheDir.listFiles { file -> file.name !in activeFiles }?.forEach { it.delete() }
|
cacheDir.listFiles { file -> file.name !in activeFiles }?.forEach { it.delete() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun free(urls: List<String>) = urls.forEach {
|
fun free(urls: List<String>) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
requests[it]?.cancel()
|
requestsMutex.withLock {
|
||||||
channel.remove(it)
|
urls.forEach {
|
||||||
activeFiles.remove(urlToFilename(it))
|
requests[it]?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flowMutex.withLock {
|
||||||
|
urls.forEach {
|
||||||
|
flow.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeFilesMutex.withLock {
|
||||||
|
urls.forEach {
|
||||||
|
activeFiles.remove(urlToFilename(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() = CoroutineScope(Dispatchers.IO).launch {
|
fun clear() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
requests.values.forEach { it.cancel() }
|
requests.values.forEach { it.cancel() }
|
||||||
channel.clear()
|
flow.clear()
|
||||||
activeFiles.clear()
|
activeFiles.clear()
|
||||||
cacheDir.listFiles()?.forEach { it.delete() }
|
cacheDir.listFiles()?.forEach { it.delete() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): Pair<Channel<Float>, File> = coroutineScope {
|
suspend fun load(force: Boolean = false, requestBuilder: HttpRequestBuilder.() -> Unit): Pair<StateFlow<Float>, File> = coroutineScope {
|
||||||
val request = HttpRequestBuilder().apply(requestBuilder)
|
val request = HttpRequestBuilder().apply(requestBuilder)
|
||||||
|
|
||||||
val url = request.url.buildString()
|
val url = request.url.buildString()
|
||||||
@@ -92,56 +113,65 @@ class NetworkCache(context: Context) : DIAware {
|
|||||||
val file = File(cacheDir, fileName)
|
val file = File(cacheDir, fileName)
|
||||||
activeFiles.add(fileName)
|
activeFiles.add(fileName)
|
||||||
|
|
||||||
val progressChannel = if (channel[url]?.isClosedForSend == false)
|
val progressFlow = flowMutex.withLock {
|
||||||
channel[url]!!
|
if (flow.contains(url)) {
|
||||||
else
|
flow[url]!!
|
||||||
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { channel[url] = it }
|
} else MutableStateFlow(0f).also { flow[url] = it }
|
||||||
|
}
|
||||||
|
|
||||||
if (file.exists())
|
requestsMutex.withLock {
|
||||||
progressChannel.close()
|
if (!requests.contains(url) || force) {
|
||||||
else
|
if (force) requests[url]?.cancelAndJoin()
|
||||||
requests[url] = networkScope.launch {
|
|
||||||
kotlin.runCatching {
|
|
||||||
cacheDir.mkdirs()
|
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
client.request<HttpStatement>(request).execute { httpResponse ->
|
requests[url] = networkScope.launch {
|
||||||
val responseChannel: ByteReadChannel = httpResponse.receive()
|
runCatching {
|
||||||
val contentLength = httpResponse.contentLength() ?: -1
|
cacheDir.mkdirs()
|
||||||
var readBytes = 0f
|
file.createNewFile()
|
||||||
|
|
||||||
file.outputStream().use { outputStream ->
|
client.request<HttpStatement>(request).execute { httpResponse ->
|
||||||
while (!responseChannel.isClosedForRead) {
|
if (!httpResponse.status.isSuccess()) throw IOException("${request.url} failed with code ${httpResponse.status.value}")
|
||||||
if (!isActive) {
|
val responseChannel: ByteReadChannel = httpResponse.receive()
|
||||||
file.delete()
|
val contentLength = httpResponse.contentLength() ?: -1
|
||||||
break
|
var readBytes = 0f
|
||||||
}
|
|
||||||
|
|
||||||
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
file.outputStream().use { outputStream ->
|
||||||
while (!packet.isEmpty) {
|
outputStream.channel.truncate(0)
|
||||||
|
while (!responseChannel.isClosedForRead) {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
file.delete()
|
file.delete()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val bytes = packet.readBytes()
|
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
outputStream.write(bytes)
|
while (!packet.isEmpty) {
|
||||||
|
if (!isActive) {
|
||||||
|
file.delete()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
readBytes += bytes.size
|
val bytes = packet.readBytes()
|
||||||
progressChannel.trySend(readBytes / contentLength)
|
outputStream.write(bytes)
|
||||||
|
|
||||||
|
readBytes += bytes.size
|
||||||
|
progressFlow.emit(readBytes / contentLength)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
progressFlow.emit(Float.POSITIVE_INFINITY)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Log.d("PUPILD-NC", it.message.toString())
|
||||||
|
file.delete()
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
|
progressFlow.emit(Float.NEGATIVE_INFINITY)
|
||||||
|
requestsMutex.withLock {
|
||||||
|
requests.remove(url)
|
||||||
}
|
}
|
||||||
progressChannel.close()
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
|
||||||
logger.warning(it)
|
|
||||||
file.delete()
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
|
||||||
progressChannel.close(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return@coroutineScope progressChannel to file
|
return@coroutineScope progressFlow to file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,11 @@ fun View.show() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FileXImageSource(val file: FileX): ImageSource {
|
class FileXImageSource(val file: FileX): ImageSource {
|
||||||
private val decoder = newBitmapRegionDecoder(file.inputStream()!!)
|
private val decoder by lazy {
|
||||||
|
file.inputStream()!!.use {
|
||||||
|
newBitmapRegionDecoder(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val imageSize by lazy { Size(decoder.width.toFloat(), decoder.height.toFloat()) }
|
override val imageSize by lazy { Size(decoder.width.toFloat(), decoder.height.toFloat()) }
|
||||||
|
|
||||||
|
|||||||
@@ -20,40 +20,33 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Test
|
||||||
|
import xyz.quaver.pupil.sources.manatoki.getItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.*
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.junit.Test
|
|
||||||
import xyz.quaver.hitomi.getGalleryInfo
|
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
|
||||||
import xyz.quaver.pupil.sources.Hiyobi_io
|
|
||||||
import java.lang.reflect.ParameterizedType
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
class ExampleUnitTest {
|
class ExampleUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
val galleryID = 479010
|
val itemID = 232566
|
||||||
val files = getGalleryInfo(galleryID).files
|
|
||||||
|
|
||||||
files.forEachIndexed { i, it ->
|
val client = HttpClient()
|
||||||
println("$i: ${imageUrlFromImage(galleryID, it, true)}")
|
|
||||||
|
runBlocking {
|
||||||
|
client.getItem(
|
||||||
|
itemID.toString(),
|
||||||
|
onReader = {
|
||||||
|
print(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test2() {
|
|
||||||
print(Hiyobi_io.parseQuery("female:loli female:big_breast tag:group"))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user