Added mirror selector

Developing new Downloader under xyz.quaver.pupil.util.download
This commit is contained in:
Pupil
2020-01-24 15:11:35 +09:00
parent 37123a2cd5
commit c204353220
27 changed files with 651 additions and 92 deletions

View File

@@ -0,0 +1,85 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_mirrors.view.*
import xyz.quaver.pupil.R
import java.util.*
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
it.split('|').let { split ->
Pair(split.first(), split.last())
}
}.toMap()
val list = mirrors.keys.toMutableList().apply {
PreferenceManager.getDefaultSharedPreferences(context)
.getString("mirrors", "")!!
.split(">")
.reversed()
.forEach {
if (this.contains(it)) {
this.remove(it)
this.add(0, it)
}
}
}
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
Collections.swap(list, from, to)
notifyItemMoved(from, to)
onItemMoved?.invoke(list)
}
var onStartDrag : ((ViewHolder) -> Unit)? = null
var onItemMoved : ((List<String>) -> (Unit))? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.view) {
mirror_name.text = mirrors[list.elementAt(position)]
mirror_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
onStartDrag?.invoke(holder)
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_mirrors, parent, false
).let {
ViewHolder(it)
}
}
override fun getItemCount() = mirrors.size
}

View File

@@ -40,6 +40,8 @@ class ReaderAdapter(private val glide: RequestManager,
var isFullScreen = false
var onItemClickListener : ((Int) -> (Unit))? = null
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -60,12 +62,16 @@ class ReaderAdapter(private val glide: RequestManager,
var reader: Reader? = null
with (GalleryDownloader[galleryID]?.reader) {
if (this?.isCompleted == true)
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)

View File

@@ -413,7 +413,11 @@ class MainActivity : AppCompatActivity() {
val downloader = GalleryDownloader.get(galleryID)
if (downloader == null)
GalleryDownloader(context, galleryID, true).start()
GalleryDownloader(
context,
galleryID,
true
).start()
else {
downloader.cancel()
downloader.clearNotification()

View File

@@ -48,7 +48,6 @@ 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.ItemClickSupport
class ReaderActivity : AppCompatActivity() {
@@ -333,7 +332,19 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() {
with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images)
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images).apply {
onItemClickListener = {
if (isScroll) {
isScroll = false
isFullscreen = true
scrollMode(false)
fullscreen(true)
} else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
}
}
}
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -353,19 +364,6 @@ class ReaderActivity : AppCompatActivity() {
this@ReaderActivity.reader_progressbar.progress = currentPage
}
})
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, _, _ ->
if (isScroll) {
isScroll = false
isFullscreen = true
scrollMode(false)
fullscreen(true)
} else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
}
}
}
with(reader_fab_download) {

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
@@ -28,6 +29,7 @@ import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_default_query.*
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags
@@ -50,21 +52,41 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.default_query_dialog_title)
setView(dialogView)
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
with(default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
super.onCreate(savedInstanceState)
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
initView()
setContentView(dialogView)
}
private fun initView() {
@SuppressLint("InflateParams")
private fun initDialog() {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse(
preferences.getString("default_query", "") ?: ""
)
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
with(dialogView.default_query_dialog_language_selector) {
adapter =
ArrayAdapter(
@@ -105,7 +127,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
@@ -113,29 +141,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(java.util.Locale.getDefault()))
s.replace(
0,
s.length,
s.toString().toLowerCase(java.util.Locale.getDefault())
)
}
})
}
dialogView.default_query_dialog_ok.setOnClickListener {
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
with(dialogView.default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
}
}

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
@@ -37,7 +38,7 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
private val buttons = mutableListOf<RadioButton>()
var onDownloadLocationChangedListener : ((Int) -> (Unit))? = null
init {
override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
ContextCompat.getExternalFilesDirs(context, null).forEachIndexed { index, dir ->
@@ -73,6 +74,8 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss()
}
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.MirrorAdapter
class MirrorDialog(context: Context) : AlertDialog(context) {
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
var onMoveItem : ((Int, Int) -> (Unit))? = null
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
private lateinit var recyclerView: RecyclerView
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.settings_mirror_title)
setView(recyclerView)
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun initDialog() {
recyclerView = RecyclerView(context).apply recyclerview@{
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context)
adapter = MirrorAdapter(context).apply adapter@{
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
onMoveItem = this@adapter.onItemMove
}).apply {
attachToRecyclerView(this@recyclerview)
}
onStartDrag = {
itemTouchHelper.startDrag(it)
}
onItemMoved = {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString("mirrors", it.joinToString(">"))
.apply()
}
}
}
}
}

View File

@@ -34,6 +34,7 @@ import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.util.*
import java.io.File
@@ -137,8 +138,6 @@ class SettingsFragment :
onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString()
dismiss() //This sucks
// TODO: make dialog dissmiss itself :P
}
}.show()
}
@@ -146,6 +145,10 @@ class SettingsFragment :
val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
}
"mirrors" -> {
MirrorDialog(context)
.show()
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
File(getDownloadDirectory(context), "favorites.json"),
@@ -259,6 +262,9 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment
}

View File

@@ -1,6 +1,6 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
* 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
@@ -65,7 +65,10 @@ class GalleryDownloader(
notificationManager.notify(galleryID, notificationBuilder.build())
if (reader?.isActive == false && downloadJob?.isActive != true) {
val data = File(getDownloadDirectory(this), galleryID.toString())
val data = File(
getDownloadDirectory(
this
), galleryID.toString())
val cache = File(cacheDir, "imageCache/$galleryID")
if (File(cache, "images").exists() && !data.exists()) {
@@ -111,7 +114,11 @@ class GalleryDownloader(
val serializer = Reader.serializer()
//Check cache
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
val cache = File(
getCachedGallery(
this@GalleryDownloader,
galleryID
), "reader.json")
try {
json.parse(serializer, cache.readText())
@@ -197,7 +204,11 @@ class GalleryDownloader(
val name = "$index".padStart(4, '0')
val ext = url.split('.').last()
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
val cache = File(
getCachedGallery(
this@GalleryDownloader,
galleryID
), "images/$name.$ext")
if (!cache.exists())
try {
@@ -255,7 +266,10 @@ class GalleryDownloader(
if (download) {
File(cacheDir, "imageCache/${galleryID}").let {
if (it.exists()) {
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
val target = File(
getDownloadDirectory(
this@GalleryDownloader
), galleryID.toString())
if (!target.exists())
target.mkdirs()

View File

@@ -0,0 +1,142 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import androidx.core.content.ContextCompat
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import java.io.File
class Cache(context: Context) : ContextWrapper(context) {
// Search in this order
// Download -> Cache
fun getCachedGallery(galleryID: Int) : File? {
var file : File
ContextCompat.getExternalFilesDirs(this, null).forEach {
file = File(it, galleryID.toString())
if (file.exists())
return file
}
file = File(cacheDir, "imageCache/$galleryID")
return if (file.exists())
file
else
null
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = File(getCachedGallery(galleryID) ?: return null, ".metadata")
if (!file.exists())
return null
return try {
Json.parse(file.readText())
} catch (e: Exception) {
//File corrupted
file.delete()
null
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = File(getCachedGallery(galleryID), ".metadata")
if (!file.exists())
return
try {
file.writeText(Json.stringify(metadata))
} catch (e: Exception) {
}
}
fun getGalleryBlock(galleryID: Int): GalleryBlock {
var meta = Cache(this).getCachedMetadata(galleryID)
if (meta == null) {
meta = Metadata(galleryBlock = xyz.quaver.hitomi.getGalleryBlock(galleryID))
Cache(this).setCachedMetadata(
galleryID,
meta
)
} else if (meta.galleryBlock == null)
Cache(this).setCachedMetadata(
galleryID,
meta.apply {
galleryBlock = xyz.quaver.hitomi.getGalleryBlock(galleryID)
}
)
return meta.galleryBlock!!
}
fun getReaders(galleryID: Int): List<Reader> {
var meta = getCachedMetadata(galleryID)
if (meta == null) {
meta = Metadata(reader = mutableListOf(xyz.quaver.hitomi.getReader(galleryID)))
setCachedMetadata(
galleryID,
meta
)
} else if (meta.reader == null)
setCachedMetadata(
galleryID,
meta.apply {
reader = mutableListOf(xyz.quaver.hitomi.getReader(galleryID))
}
)
else if (!meta.reader!!.any { it.code == Reader.Code.HITOMI })
setCachedMetadata(
galleryID,
meta.apply {
reader!!.add(xyz.quaver.hitomi.getReader(galleryID))
}
)
return meta.reader!!
}
fun getImage(galleryID: Int, index: Int): File {
val cache = getCachedGallery(galleryID)
if (cache == null)
;//TODO: initiate image download
return File(cache, "%04d".format(index))
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okio.*
import java.util.concurrent.Executors
class DownloadWorker(context: Context) : ContextWrapper(context) {
interface ProgressListener {
fun update(bytesRead : Long, contentLength: Long, done: Boolean)
}
//region ProgressResponseBody
class ProgressResponseBody(
val responseBody: ResponseBody,
val progressListener : ProgressListener
) : ResponseBody() {
var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType()
override fun source(): BufferedSource {
if (bufferedSource == null)
bufferedSource = source(responseBody.source()).buffer()
return bufferedSource!!
}
private fun source(source: Source) = object: ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
//endregion
val queue = Channel<Int>()
val worker = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
val progressListener = object: ProgressListener {
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
}
}
val client = OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
chain.proceed(chain.request()).let { originalResponse ->
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body!!, progressListener))
.build()
}
}.build()
init {
CoroutineScope(Dispatchers.IO).launch {
while (true) {
val galleryID = queue.receive()
val reader = Cache(context).getReaders(galleryID)
}
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.util.download
import kotlinx.serialization.Serializable
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
@Serializable
data class Metadata(
var thumbnail: String? = null,
var galleryBlock: GalleryBlock? = null,
var reader: MutableList<Reader>? = null
)

View File

@@ -49,6 +49,7 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)