Reader bug fix

This commit is contained in:
tom5079
2021-12-20 18:04:29 +09:00
parent b82ef8695c
commit f6f0ed40c1
13 changed files with 497 additions and 368 deletions

View File

@@ -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>

View File

@@ -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")

View File

@@ -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()
} }
} } } }
} }

View File

@@ -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)
)
} }
} }

View File

@@ -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
)
}
} }
} }

View File

@@ -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
)
}
}
} }
} }

View File

@@ -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)
)
}
} }
} }

View File

@@ -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")!!

View File

@@ -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 != '/' }

View File

@@ -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 {

View File

@@ -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
} }
} }

View 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()) }

View File

@@ -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"))
}
} }