Applying changed Download routines

This commit is contained in:
Pupil
2020-01-31 10:12:44 +09:00
parent 615b52c4fa
commit 48f90faf4e
18 changed files with 428 additions and 341 deletions

View File

@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
@@ -37,6 +38,7 @@ import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries
@@ -139,4 +141,17 @@ class ExampleInstrumentedTest {
Log.i("PUPILD", "DONE!!")
}
@Test
fun test_getReaderOrNull() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
}
}

View File

@@ -18,7 +18,9 @@
package xyz.quaver.pupil.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
@@ -29,7 +31,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.RequestManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
@@ -37,28 +39,22 @@ import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType {
NEXT,
@@ -66,10 +62,13 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
PREV
}
private val glide = Glide.with(context)
private lateinit var favorites: Histories
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
var timerTask: TimerTask? = null
fun bind(galleryBlock: GalleryBlock) {
with(view) {
val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map {
@@ -78,16 +77,14 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
}.toMap()
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
val artists = galleryBlock.artists
val series = galleryBlock.series
CoroutineScope(Dispatchers.Main).launch {
val cache = thumbnail.await()
val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
glide
.load(cache)
.load(thumbnail)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
@@ -99,75 +96,67 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
//Check cache
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
val cache = Cache(context).getCachedGallery(galleryBlock.id)
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
try {
Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
} catch(e: Exception) {
readerCache.invoke().delete()
}
if (readerCache.invoke().exists()) {
val reader = Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
if (cache != null && reader != null) {
val count = cache.listFiles { file ->
file.nameWithoutExtension.toIntOrNull() != null
}?.size ?: 0
with(galleryblock_progressbar) {
max = reader.galleryInfo.size
progress = imageCache.invoke().list()?.size ?: 0
progress = count
visibility = View.VISIBLE
}
} else {
} else
galleryblock_progressbar.visibility = View.GONE
}
if (refreshTasks[this@GalleryViewHolder] == null) {
val refresh = Timer(false).schedule(0, 1000) {
post {
if (timerTask == null)
timerTask = Timer().schedule(0, 1000) {
CoroutineScope(Dispatchers.Main).launch {
val _cache = Cache(context).getCachedGallery(galleryBlock.id)
val _reader = Cache(context).getReaderOrNull(galleryBlock.id)
if (_reader == null) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@launch
}
with(view.galleryblock_progressbar) {
progress = imageCache.invoke().list()?.size ?: 0
if (!readerCache.invoke().exists()) {
visibility = View.GONE
max = 0
progress = 0
progress = _cache?.listFiles { file ->
file.nameWithoutExtension.toIntOrNull() != null
}?.size ?: 0
view.galleryblock_progress_complete.visibility = View.INVISIBLE
} else {
if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
max = reader.galleryInfo.size
visibility = View.VISIBLE
}
if (progress == max) {
if (completeFlag.get(galleryBlock.id, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryBlock.id, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = _reader.galleryInfo.size
}
if (progress == max) {
if (completeFlag.get(galleryBlock.id, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryBlock.id, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
}
}
refreshTasks[this@GalleryViewHolder] = refresh
}
galleryblock_title.text = galleryBlock.title
with(galleryblock_artist) {
text = artists.joinToString(", ") { it.wordCapitalize() }
@@ -277,7 +266,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
}
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
@@ -336,10 +324,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = when(GalleryDownloader.get(gallery.first.id)) {
null -> holder.view.context.getString(R.string.main_download)
else -> holder.view.context.getString(android.R.string.cancel)
}
holder.view.galleryblock_download.text =
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
holder.view.context.getString(R.string.main_download)
else
holder.view.context.getString(android.R.string.cancel)
}
override fun onClose(layout: SwipeLayout?) {}
@@ -354,12 +343,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder)
if (holder is GalleryViewHolder) {
val task = refreshTasks[holder] ?: return
task.cancel()
refreshTasks.remove(holder)
}
if (holder is GalleryViewHolder)
holder.timerTask?.cancel()
}
override fun getItemCount() =

View File

@@ -18,30 +18,47 @@
package xyz.quaver.pupil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.getCachedGallery
import java.io.File
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class ReaderAdapter(private val glide: RequestManager,
private val galleryID: Int,
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
class ReaderAdapter(private val context: Context,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false
var reader: Reader? = null
private val glide = Glide.with(context)
var onItemClickListener : ((Int) -> (Unit))? = null
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -60,38 +77,62 @@ class ReaderAdapter(private val glide: RequestManager,
else
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
var reader: Reader? = null
with (GalleryDownloader[galleryID]?.reader) {
if (reader == null && this?.isCompleted == true)
runBlocking {
reader = await()
}
}
holder.view.image.setOnPhotoTapListener { _, _, _ ->
onItemClickListener?.invoke(position)
}
glide
.load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
else {
val galleryInfo = reader?.galleryInfo?.get(position)
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
if (galleryInfo != null) {
(holder.view.image.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${galleryInfo.width}:${galleryInfo.height}"
}
holder.view.reader_item_progressbar.progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)?.roundToInt() ?: 0
holder.view.reader_index.text = (position+1).toString()
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (progress?.isFinite() == false) {
when {
progress.isInfinite() -> {
var image = Cache(context).getImages(galleryID)
while (image?.get(position) == null)
image = Cache(context).getImages(galleryID)
glide
.load(image[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
}
progress.isNaN() -> {
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
Snackbar
.make(
holder.view,
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
?: context.getText(R.string.default_error_msg),
Snackbar.LENGTH_INDEFINITE
)
.show()
}
}
.into(holder.view.image)
} else {
holder.view.image.setImageDrawable(null)
Timer().schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position)
}
}
}
}
override fun getItemCount() = images.size
override fun getItemCount() = reader?.galleryInfo?.size ?: 0
}

View File

@@ -45,7 +45,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.*
@@ -64,11 +63,10 @@ import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.ceil
@@ -89,7 +87,7 @@ class MainActivity : AppCompatActivity() {
POPULAR
}
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
private val galleries = ArrayList<GalleryBlock>()
private var query = ""
set(value) {
@@ -386,7 +384,7 @@ class MainActivity : AppCompatActivity() {
private fun setupRecyclerView() {
with(main_recyclerview) {
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
@@ -399,20 +397,18 @@ class MainActivity : AppCompatActivity() {
}
}
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].first.id
val galleryID = galleries[position].id
if (!completeFlag.get(galleryID, false)) {
val downloader = GalleryDownloader.get(galleryID)
val worker = DownloadWorker.getInstance(context)
if (downloader == null)
GalleryDownloader(
context,
galleryID,
true
).start()
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
worker.cancel(galleryID)
else {
downloader.cancel()
downloader.clearNotification()
Cache(context).moveToDownload(galleryID)
if (!worker.queue.contains(galleryID))
worker.queue.add(galleryID)
}
}
@@ -420,39 +416,28 @@ class MainActivity : AppCompatActivity() {
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position].first.id
val galleryID = galleries[position].id
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryID]) {
this?.cancelAndJoin()
this?.clearNotification()
DownloadWorker.getInstance(context).cancel(galleryID)
var cache = Cache(context).getCachedGallery(galleryID)
while (cache != null) {
cache.deleteRecursively()
cache = Cache(context).getCachedGallery(galleryID)
}
val cache = File(cacheDir, "imageCache/${galleryID}")
val data = getCachedGallery(context, galleryID)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryID)
if (this@MainActivity.mode == Mode.DOWNLOAD) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
histories.remove(galleryID)
if (this@MainActivity.mode == Mode.HISTORY) {
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
completeFlag.put(galleryID, false)
}
@@ -466,7 +451,7 @@ class MainActivity : AppCompatActivity() {
return@setOnItemClickListener
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position].first
val gallery = galleries[position]
intent.putExtra("galleryID", gallery.id)
//TODO: Maybe sprinkling some transitions will be nice :D
@@ -478,7 +463,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView)
return@setOnItemLongClickListener true
val galleryID = galleries[position].first.id
val galleryID = galleries[position].id
GalleryDialog(
this@MainActivity,
@@ -1030,41 +1015,21 @@ class MainActivity : AppCompatActivity() {
val json = Json(JsonConfiguration.Stable)
val serializer = GalleryBlock.serializer()
val galleryBlock =
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
when {
cache.exists() -> json.parse(serializer, cache.readText())
else -> {
getGalleryBlock(galleryID).apply {
this ?: return@apply
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
when {
cache.exists() -> json.parse(serializer, cache.readText())
else -> {
getGalleryBlock(galleryID).apply {
this ?: return@apply
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, this))
}
cache.writeText(json.stringify(serializer, this))
}
}
} ?: return@async null
val thumbnail = async {
val ext = galleryBlock.thumbnails[0].split('.').last()
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
if (!exists())
try {
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
if (this@apply.parentFile?.exists() == false)
this@apply.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(this@apply))
}
} catch (e: Exception) {
delete()
}
}.absolutePath
}
Pair(galleryBlock, thumbnail)
}
} ?: return@async null
} catch (e: Exception) {
null
}

View File

@@ -21,39 +21,39 @@ package xyz.quaver.pupil.ui
import android.content.Intent
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.ImplicitReflectionSerializer
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import java.util.*
import kotlin.concurrent.schedule
class ReaderActivity : AppCompatActivity() {
private var galleryID = 0
private val images = ArrayList<String>()
private var gallerySize = 0
private var currentPage = 0
private var isScroll = true
@@ -69,7 +69,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
private lateinit var downloader: GalleryDownloader
private val timer = Timer()
private val snapHelper = PagerSnapHelper()
@@ -101,12 +101,8 @@ class ReaderActivity : AppCompatActivity() {
return
}
initDownloader()
initView()
if (!downloader.download)
downloader.start()
initDownloader()
}
override fun onNewIntent(intent: Intent) {
@@ -168,7 +164,7 @@ class ReaderActivity : AppCompatActivity() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
with(view.dialog_number_picker) {
minValue=1
maxValue=gallerySize
maxValue=reader_recyclerview.adapter?.itemCount ?: 0
value=currentPage
}
val dialog = AlertDialog.Builder(this).apply {
@@ -201,8 +197,10 @@ class ReaderActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
if (::downloader.isInitialized && !downloader.download)
downloader.cancel()
timer.cancel()
if (!getDownloadDirectory(this).isParentOf(Cache(this).getCachedGallery(galleryID)))
DownloadWorker.getInstance(this).cancel(galleryID)
}
override fun onBackPressed() {
@@ -238,101 +236,54 @@ class ReaderActivity : AppCompatActivity() {
}
private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
if (d == null)
d = GalleryDownloader(this, galleryID)
downloader = d.apply {
onReaderLoadedHandler = {
CoroutineScope(Dispatchers.Main).launch {
title = it.title
with(reader_download_progressbar) {
max = it.galleryInfo.size
progress = 0
}
with(reader_progressbar) {
max = it.galleryInfo.size
progress = 0
}
gallerySize = it.galleryInfo.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.galleryInfo.size}"
}
}
onProgressHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_download_progressbar.progress = it
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
}
}
onDownloadedHandler = {
val item = it.toList()
CoroutineScope(Dispatchers.Main).launch {
if (images.isEmpty()) {
images.addAll(item)
reader_recyclerview.adapter?.notifyDataSetChanged()
} else {
images.add(item.last())
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
}
}
}
onErrorHandler = {
Snackbar
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.reader_help) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
}
.show()
downloader.download = false
}
onCompleteHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_download_progressbar.visibility = View.GONE
}
}
onNotifyChangedHandler = { notify ->
val fab = reader_fab_download
runOnUiThread {
if (notify) {
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
if (downloader.download)
fab.post {
icon.start()
fab.labelText = getString(R.string.reader_fab_download_cancel)
}
else
fab.post {
fab.setImageResource(R.drawable.ic_download)
fab.labelText = getString(R.string.reader_fab_download)
}
}
})
fab.setImageDrawable(icon)
icon?.start()
} else {
runOnUiThread {
fab.setImageResource(R.drawable.ic_download)
}
}
}
}
val worker = DownloadWorker.getInstance(this).apply {
queue.add(galleryID)
}
if (downloader.download) {
downloader.invokeOnReaderLoaded()
downloader.invokeOnNotifyChanged()
timer.schedule(0, 1000) {
if (worker.progress.indexOfKey(galleryID) < 0) //loading
return@schedule
if (worker.progress[galleryID] == null) { //Gallery not found
timer.cancel()
Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show()
}
runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = worker.progress[galleryID]!!.count { !it.isFinite() }
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) {
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
if (reader != null) {
title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
when (reader.code) {
Reader.Code.HITOMI -> R.drawable.hitomi
Reader.Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
})
}
}
if (worker.progress[galleryID]!!.all { !it.isFinite() }) { //Download finished
reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false)
}
}
}
}
private fun initView() {
with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images).apply {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = {
if (isScroll) {
isScroll = false
@@ -360,19 +311,19 @@ class ReaderActivity : AppCompatActivity() {
if (layoutManager.findFirstVisibleItemPosition() == -1)
return
currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
this@ReaderActivity.reader_progressbar.progress = currentPage
}
})
}
with(reader_fab_download) {
setImageResource(R.drawable.ic_download)
setOnClickListener {
downloader.download = !downloader.download
animateDownloadFAB(getDownloadDirectory(context).isParentOf(Cache(context).getCachedGallery(galleryID))) //If download in progress, animate button
if (!downloader.download)
downloader.clearNotification()
setOnClickListener {
Cache(context).moveToDownload(galleryID)
animateDownloadFAB(true)
}
}
@@ -414,4 +365,32 @@ class ReaderActivity : AppCompatActivity() {
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
}
private fun animateDownloadFAB(animate: Boolean) {
with(reader_fab_download) {
if (animate) {
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
}
else // Or continue animating
post {
icon.start()
labelText = getString(R.string.reader_fab_download_cancel)
}
}
})
setImageDrawable(icon)
icon?.start()
} else
setImageResource(R.drawable.ic_download)
}
}
}

View File

@@ -35,7 +35,10 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.gallery_details.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery
@@ -221,9 +224,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(glide, galleries).apply {
val adapter = GalleryBlockAdapter(context, galleries).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
@@ -238,7 +241,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
}.let {
val galleryBlock = it.await() ?: return@let
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() }))
galleries.add(galleryBlock)
adapter.notifyItemInserted(i)
}
}
@@ -254,14 +257,14 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position].first.id)
putExtra("galleryID", galleries[position].id)
})
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
(context.applicationContext as Pupil).histories.add(galleries[position].id)
}
.setOnItemLongClickListener { _, position, _ ->
GalleryDialog(
context,
galleries[position].first.id
galleries[position].id
).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }

View File

@@ -78,16 +78,14 @@ class Cache(context: Context) : ContextWrapper(context) {
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = File(getCachedGallery(galleryID), ".metadata")
val file = File(getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID"), ".metadata")
if (!file.exists())
return
if (file.parentFile?.exists() != true)
file.parentFile?.mkdirs()
try {
file.writeText(Json.stringify(metadata))
} catch (e: Exception) {
file.createNewFile()
}
file.writeText(Json.stringify(metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
@@ -135,6 +133,16 @@ class Cache(context: Context) : ContextWrapper(context) {
return galleryBlock
}
fun getReaderOrNull(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", "")!!.split('>')
return metadata?.readers?.firstOrNull {
mirrors.contains(it.code.name)
}
}
suspend fun getReader(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
@@ -173,7 +181,7 @@ class Cache(context: Context) : ContextWrapper(context) {
gallery.listFiles { file ->
file.nameWithoutExtension.toIntOrNull() != null
}?.forEach {
append(it.nameWithoutExtension.toInt(), it)
put(it.nameWithoutExtension.toInt(), it)
}
}
}
@@ -197,14 +205,17 @@ class Cache(context: Context) : ContextWrapper(context) {
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID")
val cache = getCachedGallery(galleryID)
val download = getDownloadDirectory(this)
if (cache != null) {
val download = getDownloadDirectory(this)
if (!download.isParentOf(cache)) {
cache.copyRecursively(download)
cache.deleteRecursively()
}
if (!download.isParentOf(cache)) {
cache.copyRecursively(download, true)
cache.deleteRecursively()
}
} else
File(getDownloadDirectory(this), galleryID.toString()).mkdirs()
}
}

View File

@@ -49,8 +49,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
if (!done && progress[galleryID]!![index] != Float.POSITIVE_INFINITY)
progress[galleryID]!![index] = bytesRead * 100F / contentLength
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
}
}
@@ -156,14 +156,20 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
.build()
fun stop() {
queue.clear()
loop.cancel()
for (i in 0..worker.size())
worker[worker.keyAt(i)]?.cancel()
client.dispatcher.cancelAll()
progress.clear()
exception.clear()
}
fun cancel(galleryID: Int) {
queue.remove(galleryID)
worker[galleryID]?.cancel()
client.dispatcher.queuedCalls()
@@ -171,6 +177,13 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
.forEach {
it.cancel()
}
if (progress.indexOfKey(galleryID) >= 0) {
progress.remove(galleryID)
exception.remove(galleryID)
nRunners--
}
}
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
@@ -179,7 +192,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
//Cache exists :P
cache?.get(index)?.let {
progress[galleryID]!![index] = Float.POSITIVE_INFINITY
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
return
}
@@ -231,11 +244,15 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (Fabric.isInitialized())
Crashlytics.logException(e)
progress[galleryID]!![i] = Float.NaN
exception[galleryID]!![i] = e
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
if (progress[galleryID]?.all { !it.isFinite() } == true) {
progress.remove(galleryID)
exception.remove(galleryID)
if (progress[galleryID]!!.all { !it.isFinite() })
nRunners--
}
}
override fun onResponse(call: Call, response: Response) {
@@ -245,11 +262,15 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
call.request().url.encodedPath.split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
progress[galleryID]!![i] = Float.POSITIVE_INFINITY
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
}
if (progress[galleryID]!!.all { !it.isFinite() })
if (progress[galleryID]?.all { !it.isFinite() } == true) {
progress.remove(galleryID)
exception.remove(galleryID)
nRunners--
}
}
}

View File

@@ -64,4 +64,4 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
}
}
fun File.isParentOf(file: File) = file.absolutePath.startsWith(this.absolutePath)
fun File.isParentOf(file: File?) = file?.absolutePath?.startsWith(this.absolutePath) ?: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp">
<shape android:shape="rectangle" >
<stroke
android:width="1dp"
android:color="#555555" />
<solid android:color="@color/transparent" />
</shape>
</item>
</layer-list>

View File

@@ -23,15 +23,49 @@
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:layout_width="0dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingBottom="8dp"/>
android:background="@drawable/reader_item_boundary">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ProgressBar
android:id="@+id/reader_item_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:progressBarStyleHorizontal"
android:indeterminate="false"
android:progress="0"
android:max="100"
android:visibility="visible"/>
<TextView
android:id="@+id/reader_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Caption"/>
</LinearLayout>
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -25,9 +25,8 @@
android:icon="@drawable/avd_star"
app:showAsAction="always"/>
<item android:id="@+id/reader_menu_use_hiyobi"
<item android:id="@+id/reader_type"
android:title=""
android:icon="@drawable/ic_hiyobi"
app:showAsAction="ifRoom"
android:visible="false"/>

View File

@@ -64,7 +64,7 @@
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
<string name="main_drawer_favorite">お気に入り</string>
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="main_open_gallery_by_id_error">エラーが発生しました</string>
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string>

View File

@@ -62,7 +62,7 @@
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
<string name="main_drawer_favorite">즐겨찾기</string>
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
<string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string>
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
<string name="settings_storage">저장 공간</string>
<string name="main_drawer_grouop_contact_discord">디스코드</string>
<string name="settings_app_lock">앱 잠금</string>

View File

@@ -60,7 +60,7 @@
<string name="main_jump_title">Jump to page</string>
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
<string name="main_open_gallery_by_id">Open Gallery by ID</string>
<string name="main_open_gallery_by_id_error">Failed to open gallery</string>
<string name="reader_failed_to_find_gallery">Failed to open gallery</string>
<string name="main_move">Move to page %1$d</string>