what i got so far

This commit is contained in:
tom5079
2020-09-01 18:07:16 +09:00
parent c96d609803
commit 7704c96955
28 changed files with 611 additions and 444 deletions

View File

@@ -60,7 +60,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
@@ -96,15 +96,15 @@ dependencies {
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation "ru.noties.markwon:core:3.1.0"
implementation ("xyz.quaver:libpupil:1.1") {
implementation ("xyz.quaver:libpupil:1.3") {
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
}
implementation "xyz.quaver:documentfilex:0.2.2"
implementation "xyz.quaver:documentfilex:0.2.11-alpha6"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
androidExtensions {

View File

@@ -21,6 +21,7 @@
#-renamesourcefileattribute SourceFile
-dontobfuscate
-dontoptimize
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
@@ -47,3 +48,4 @@
kotlinx.serialization.KSerializer serializer(...);
}
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.util.Preferences

View File

@@ -28,6 +28,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader
@@ -121,4 +122,9 @@ class ExampleInstrumentedTest {
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
}
@Test
fun test_suggestion() {
getSuggestionsForQuery("female:l")
}
}

View File

@@ -22,7 +22,6 @@
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme"
android:requestLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
<provider

View File

@@ -23,6 +23,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
@@ -38,6 +39,7 @@ import kotlinx.coroutines.launch
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import xyz.quaver.io.FileX
import xyz.quaver.pupil.util.*
import xyz.quaver.setClient
import java.io.File
@@ -91,11 +93,11 @@ class Pupil : Application() {
try {
Preferences.get<String>("download_folder").also {
if (!File(it).canWrite())
if (!FileX(this, it).canWrite())
throw Exception()
}
} catch (e: Exception) {
Preferences.remove("dl_location")
Preferences.remove("download_folder")
}
histories = GalleryList(File(ContextCompat.getDataDir(this), "histories.json"))

View File

@@ -25,7 +25,6 @@ import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.InputStream
@GlideModule

View File

@@ -23,7 +23,6 @@ import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
@@ -46,22 +45,20 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.GalleryList
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import xyz.quaver.pupil.util.wordCapitalize
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType {
NEXT,
@@ -77,22 +74,23 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
var timerTask: TimerTask? = null
private fun updateProgress(context: Context, galleryID: Int) {
val reader = Cache(context).getReaderOrNull(galleryID)
val cache = Cache.getInstance(context, galleryID)
CoroutineScope(Dispatchers.Main).launch {
if (reader == null || Preferences["cache_disable"]) {
if (cache.metadata.reader == null || Preferences["cache_disable"]) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@launch
}
with(view.galleryblock_progressbar) {
val imageList = cache.metadata.imageList!!
progress = Cache(context).getImages(galleryID)?.size ?: 0
progress = imageList.filterNotNull().size
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = reader.galleryInfo.files.size
max = imageList.size
}
if (progress == max) {
@@ -116,7 +114,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
}
fun bind(galleryBlock: GalleryBlock) {
fun bind(galleryID: Int) {
val cache = Cache.getInstance(view.context, galleryID)
val galleryBlock = cache.metadata.galleryBlock!!
with(view) {
val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map {
@@ -136,13 +138,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
it.start()
})
CoroutineScope(Dispatchers.Main).launch {
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
if (it != null)
Base64.decode(it, Base64.DEFAULT)
else
null
}
CoroutineScope(Dispatchers.IO).launch {
val thumbnail = cache.getThumbnail()
galleryblock_thumbnail.post {
glide
@@ -158,27 +155,9 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
}
//Check cache
val cache = Cache(context).getCachedGallery(galleryBlock.id)
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
if (reader != null) {
val count = cache.listFiles()?.count {
Regex("^[0-9]+.+\$").matches(it.name)
} ?: 0
with(galleryblock_progressbar) {
max = reader.galleryInfo.files.size
progress = count
visibility = View.VISIBLE
}
} else
galleryblock_progressbar.visibility = View.GONE
if (timerTask == null)
timerTask = timer.schedule(0, 1000) {
updateProgress(context, galleryBlock.id)
updateProgress(context, galleryID)
}
galleryblock_title.text = galleryBlock.title
@@ -339,9 +318,9 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) {
val gallery = galleries[position-(if (showPrev) 1 else 0)]
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
holder.bind(gallery)
holder.bind(galleryID)
with(holder.view.galleryblock_primary) {
setOnClickListener {
@@ -367,7 +346,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text =
if (Cache(holder.view.context).isDownloading(gallery.id))
if (DownloadFolderManager.getInstance(holder.view.context).isDownloading(galleryID))
holder.view.context.getString(android.R.string.cancel)
else
holder.view.context.getString(R.string.main_download)
@@ -392,8 +371,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
override fun getItemCount() =
(if (galleries.isEmpty()) 0 else galleries.size)+
(if (showNext) 1 else 0)+
galleries.size +
(if (showNext) 1 else 0) +
(if (showPrev) 1 else 0)
override fun getItemViewType(position: Int): Int {

View File

@@ -23,7 +23,7 @@ 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.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
@@ -38,28 +38,29 @@ import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.io.util.readBytes
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.downloader.Cache
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class ReaderAdapter(private val glide: RequestManager,
class ReaderAdapter(private val activity: ReaderActivity,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var reader: Reader? = null
val timer = Timer()
private val glide = Glide.with(activity)
var isFullScreen = false
var onItemClickListener : ((Int) -> (Unit))? = null
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
var downloadWorker: DownloadWorker? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false
@@ -68,11 +69,12 @@ class ReaderAdapter(private val glide: RequestManager,
}
}
private var cache: Cache? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout
if (downloadWorker == null)
downloadWorker = DownloadWorker.getInstance(holder.view.context)
if (cache == null)
cache = Cache.getInstance(holder.view.context, galleryID)
if (isFullScreen) {
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
@@ -124,15 +126,15 @@ class ReaderAdapter(private val glide: RequestManager,
.into(holder.view.image)
}
} else {
val image = Cache(holder.view.context).getImage(galleryID, position)
val progress = downloadWorker!!.progress[galleryID]?.get(position)
val image = cache!!.getImage(position)
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
if (progress?.isInfinite() == true && image != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
holder.view.image.post {
glide
.load(image)
.load(image.readBytes())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.fitCenter()

View File

@@ -18,24 +18,37 @@
package xyz.quaver.pupil.services
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.util.Log
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import okhttp3.ResponseBody
import okio.*
import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import xyz.quaver.pupil.util.requestBuilders
import xyz.quaver.pupil.util.startForegroundServiceCompat
import java.io.IOException
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
class DownloadService : Service() {
data class Tag(val galleryID: Int, val index: Int)
//region Notification
@@ -50,13 +63,60 @@ class DownloadService : Service() {
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
}
private val notification = SparseArray<NotificationCompat.Builder?>()
private fun initNotification(galleryID: Int) {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
}
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_notification) // had to use this because old android doesn't support VectorDrawable on Notification :P
setContentIntent(pendingIntent)
setProgress(0, 0, true)
setOngoing(true)
})
notify(galleryID)
}
private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
val notification = notification[galleryID] ?: return
if (isCompleted(galleryID)) {
notification
.setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager.cancel(galleryID)
} else
notification
.setProgress(max, progress, false)
.setContentText("$progress/$max")
if (DownloadFolderManager.getInstance(this).getDownloadFolder(galleryID) != null)
notification.let { notificationManager.notify(galleryID, it.build()) }
else
notificationManager.cancel(galleryID)
}
//endregion
//region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
}
private class ProgressResponseBody(
@@ -107,6 +167,7 @@ class DownloadService : Service() {
}
//endregion
//region Downloader
/**
* KEY
* primary galleryID
@@ -120,11 +181,163 @@ class DownloadService : Service() {
*/
val progress = SparseArray<MutableList<Float>?>()
override fun onCreate() {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
interceptors[Tag::class] = interceptor
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it.isInfinite() } == true
private val callback = object: Callback {
override fun onFailure(call: Call, e: IOException) {
if (e.message?.contains("cancel", true) == false) {
val galleryID = (call.request().tag() as Tag).galleryID
Log.i("PUPILD", "$galleryID ERR-RETRYING $e ${e.message}")
// Retry
cancel(galleryID)
download(galleryID)
}
}
override fun onResponse(call: Call, response: Response) {
val (galleryID, index) = call.request().tag() as Tag
val ext = call.request().url().encodedPath().split('.').last()
kotlin.runCatching {
val image = response.body()?.use { it.bytes() } ?: throw Exception()
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "$index.$ext", image)
}.onSuccess {
notify(galleryID)
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
}.onFailure {
Log.i("PUPILD", "$galleryID-$index DLERR-RETRYING $it ${it.message}")
cancel(galleryID)
download(galleryID)
}
}
}
}
}
fun cancel() {
client.dispatcher().queuedCalls().filter {
it.request().tag() is Tag
}.forEach {
it.cancel()
}
client.dispatcher().runningCalls().filter {
it.request().tag() is Tag
}.forEach {
it.cancel()
}
progress.clear()
notification.clear()
notificationManager.cancelAll()
}
fun cancel(galleryID: Int) {
client.dispatcher().queuedCalls().filter {
(it.request().tag() as Tag).galleryID == galleryID
}.forEach {
it.cancel()
}
client.dispatcher().runningCalls().filter {
(it.request().tag() as Tag).galleryID == galleryID
}.forEach {
it.cancel()
}
progress.remove(galleryID)
notification.remove(galleryID)
notificationManager.cancel(galleryID)
}
fun delete(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
cancel(galleryID)
Cache.delete(galleryID)
DownloadFolderManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
}
fun download(galleryID: Int): Job = CoroutineScope(Dispatchers.IO).launch {
if (progress.indexOfKey(galleryID) >= 0)
cancel(galleryID)
val cache = Cache.getInstance(this@DownloadService, galleryID)
initNotification(galleryID)
val reader = cache.getReader()
// Gallery doesn't exist
if (reader == null) {
delete(galleryID)
progress.put(galleryID, null)
return@launch
}
if (progress.indexOfKey(galleryID) < 0)
progress.put(galleryID, mutableListOf())
cache.metadata.imageList?.forEach {
progress[galleryID]?.add(if (it != null) Float.POSITIVE_INFINITY else 0F)
}
notification[galleryID]?.setContentTitle(reader.galleryInfo.title)
notify(galleryID)
reader.requestBuilders.filterIndexed { index, _ -> !progress[galleryID]!![index].isInfinite() }.forEachIndexed { index, it ->
val request = it.tag(Tag(galleryID, index)).build()
client.newCall(request).enqueue(callback)
}
}
//endregion
companion object {
const val KEY_COMMAND = "COMMAND" // String
const val KEY_ID = "ID" // Int
const val COMMAND_DOWNLOAD = "DOWNLOAD"
const val COMMAND_CANCEL = "CANCEL"
const val COMMAND_DELETE = "DELETE"
private fun command(context: Context, extras: Intent.() -> Unit) {
context.startForegroundServiceCompat(Intent(context, DownloadService::class.java).apply(extras))
}
fun download(context: Context, galleryID: Int) {
command(context) {
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
putExtra(KEY_ID, galleryID)
}
}
fun cancel(context: Context, galleryID: Int? = null) {
command(context) {
putExtra(KEY_COMMAND, COMMAND_CANCEL)
galleryID?.let { putExtra(KEY_ID, it) }
}
}
fun delete(context: Context, galleryID: Int) {
command(context) {
putExtra(KEY_COMMAND, COMMAND_DELETE)
putExtra(KEY_ID, galleryID)
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getStringExtra(KEY_COMMAND)) {
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) download(it) }
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it) else cancel() }
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it) }
}
return START_NOT_STICKY
}
inner class Binder : android.os.Binder() {
val service = this@DownloadService
@@ -133,20 +346,13 @@ class DownloadService : Service() {
private val binder = Binder()
override fun onBind(p0: Intent?) = binder
val cache = SparseArray<Cache>()
fun load(galleryID: Int) {
if (progress.indexOfKey(galleryID) < 0)
progress.put(galleryID, mutableListOf())
if (cache.indexOfKey(galleryID) < 0)
cache.put(galleryID, Cache.getInstance(this, galleryID))
cache[galleryID].metadata.imageList?.forEach {
progress[galleryID]?.add(if (it == null) Float.POSITIVE_INFINITY else 0F)
}
override fun onCreate() {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
interceptors[Tag::class] = interceptor
}
fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
override fun onDestroy() {
interceptors.remove(Tag::class)
cancel()
}
}

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.types
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {

View File

@@ -55,7 +55,6 @@ import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
@@ -68,8 +67,8 @@ 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 xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
@@ -92,7 +91,7 @@ class MainActivity : AppCompatActivity() {
POPULAR
}
private val galleries = ArrayList<GalleryBlock>()
private val galleries = ArrayList<Int>()
private var query = ""
set(value) {
@@ -112,6 +111,8 @@ class MainActivity : AppCompatActivity() {
private var loadingJob: Job? = null
private var currentPage = 0
private lateinit var downloadFolderManager: DownloadFolderManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -146,15 +147,7 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
Intent(this, DownloadService::class.java).let {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
startForegroundService(it)
else ->
startService(it)
}
}
downloadFolderManager = DownloadFolderManager.getInstance(this)
checkUpdate(this)
initView()
@@ -336,7 +329,7 @@ class MainActivity : AppCompatActivity() {
with(main_fab_cancel) {
setImageResource(R.drawable.cancel)
setOnClickListener {
DownloadWorker.getInstance(context).stop()
DownloadService.cancel(this@MainActivity)
}
}
@@ -447,19 +440,15 @@ class MainActivity : AppCompatActivity() {
}
}
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id
val worker = DownloadWorker.getInstance(context)
val galleryID = galleries[position]
if (Preferences["cache_disable"])
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else {
if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress
Cache(context).setDownloading(galleryID, false)
worker.cancel(galleryID)
if (downloadFolderManager.isDownloading(galleryID)) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID)
}
else {
Cache(context).setDownloading(galleryID, true)
worker.queue.add(galleryID)
DownloadService.download(this@MainActivity, galleryID)
}
}
@@ -467,25 +456,20 @@ class MainActivity : AppCompatActivity() {
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position].id
val galleryID = galleries[position]
DownloadService.delete(this@MainActivity, galleryID)
CoroutineScope(Dispatchers.Default).launch {
DownloadWorker.getInstance(context).cancel(galleryID)
histories.remove(galleryID)
Cache(context).getCachedGallery(galleryID).deleteRecursively()
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
histories.remove(galleryID)
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
completeFlag.put(galleryID, false)
}
completeFlag.put(galleryID, false)
closeAllItems()
}
@@ -496,8 +480,7 @@ class MainActivity : AppCompatActivity() {
return@listener
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position]
intent.putExtra("galleryID", gallery.id)
intent.putExtra("galleryID", galleries[position])
//TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent)
@@ -507,7 +490,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView)
return@listener false
val galleryID = galleries[position].id
val galleryID = galleries[position]
GalleryDialog(
this@MainActivity,
@@ -762,7 +745,7 @@ class MainActivity : AppCompatActivity() {
if (!favoritesFile.exists()) {
favoritesFile.createNewFile()
favoritesFile.writeText(Json.encodeToString(Tags()))
favoritesFile.writeText("[]")
}
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
@@ -788,7 +771,7 @@ class MainActivity : AppCompatActivity() {
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
sortMode = MainActivity.SortMode.NEWEST
it.isChecked = true
runOnUiThread {
@@ -1023,7 +1006,7 @@ class MainActivity : AppCompatActivity() {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
else -> doSearch("$defaultQuery $query", sortMode == MainActivity.SortMode.POPULAR).also {
totalItems = it.size
}
}
@@ -1107,16 +1090,16 @@ class MainActivity : AppCompatActivity() {
for (chunk in chunks)
chunk.map { galleryID ->
async {
Cache(this@MainActivity).getGalleryBlock(galleryID)
Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
galleryID
}
}
}.forEach {
val galleryBlock = it.await()
it.await()?.also {
withContext(Dispatchers.Main) {
main_progressbar.hide()
withContext(Dispatchers.Main) {
main_progressbar.hide()
if (galleryBlock != null) {
galleries.add(galleryBlock)
galleries.add(it)
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
}
}

View File

@@ -18,10 +18,14 @@
package xyz.quaver.pupil.ui
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@@ -47,9 +51,10 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.timer
@@ -72,6 +77,22 @@ class ReaderActivity : AppCompatActivity() {
}
}
private lateinit var cache: Cache
var downloader: DownloadService? = null
private val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloader = (service as DownloadService.Binder).service
Log.i("PUPILD", "CON")
}
override fun onServiceDisconnected(name: ComponentName?) {
downloader = null
Log.i("PUPILD", "DIS")
}
}
private var deleteOnExit = true
private val timer = Timer()
private var autoTimer: Timer? = null
@@ -81,6 +102,7 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader)
title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
@@ -89,23 +111,18 @@ class ReaderActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_reader)
handleIntent(intent)
histories.add(galleryID)
cache = Cache.getInstance(this, galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
if (galleryID == 0) {
onBackPressed()
return
}
initView()
if (Preferences["cache_disable"]) {
reader_download_progressbar.visibility = View.GONE
CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@ReaderActivity).getReader(galleryID)
val reader = cache.getReader()
launch(Dispatchers.Main) initDownloader@{
if (reader == null) {
@@ -115,6 +132,7 @@ class ReaderActivity : AppCompatActivity() {
return@initDownloader
}
histories.add(galleryID)
(reader_recyclerview.adapter as ReaderAdapter).apply {
this.reader = reader
notifyDataSetChanged()
@@ -132,6 +150,8 @@ class ReaderActivity : AppCompatActivity() {
}
} else
initDownloader()
initView()
}
override fun onNewIntent(intent: Intent) {
@@ -187,9 +207,9 @@ class ReaderActivity : AppCompatActivity() {
R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) {
minValue=1
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
value=currentPage
minValue = 1
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
value = currentPage
}
val dialog = AlertDialog.Builder(this).apply {
setView(view)
@@ -224,8 +244,12 @@ class ReaderActivity : AppCompatActivity() {
timer.cancel()
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!Cache(this).isDownloading(galleryID))
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
if (deleteOnExit) {
downloader?.cancel(galleryID)
DownloadFolderManager.getInstance(this).deleteDownloadFolder(galleryID)
}
unbindService(conn)
}
override fun onBackPressed() {
@@ -261,16 +285,16 @@ class ReaderActivity : AppCompatActivity() {
}
private fun initDownloader() {
val worker = DownloadWorker.getInstance(this).apply {
cancel(galleryID)
queue.add(galleryID)
}
DownloadService.download(this, galleryID)
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
timer.schedule(1000, 1000) {
if (worker.progress.indexOfKey(galleryID) < 0) //loading
val downloader = downloader ?: return@schedule
if (downloader.progress.indexOfKey(galleryID) < 0) //loading
return@schedule
if (worker.progress[galleryID] == null) { //Gallery not found
if (downloader.progress[galleryID] == null) { //Gallery not found
timer.cancel()
Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
@@ -279,14 +303,13 @@ class ReaderActivity : AppCompatActivity() {
runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = worker.progress[galleryID]?.count { it.isInfinite() } ?: 0
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) {
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
val reader = cache.metadata.reader
if (reader != null) {
with (reader_recyclerview.adapter as ReaderAdapter) {
this.reader = reader
notifyDataSetChanged()
@@ -304,7 +327,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) { //Download finished
if (downloader.isCompleted(galleryID)) { //Download finished
reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false)
@@ -315,7 +338,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() {
with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID).apply {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = {
if (isScroll) {
isScroll = false
@@ -350,19 +373,18 @@ class ReaderActivity : AppCompatActivity() {
}
with(reader_fab_download) {
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
animateDownloadFAB(DownloadFolderManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
setOnClickListener {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else {
if (Cache(context).isDownloading(galleryID)) {
Cache(context).setDownloading(galleryID, false)
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
if (deleteOnExit) {
deleteOnExit = false
cache.moveToDownload()
animateDownloadFAB(true)
} else {
animateDownloadFAB(false)
}
}
}
@@ -371,10 +393,8 @@ class ReaderActivity : AppCompatActivity() {
with(reader_fab_retry) {
setImageResource(R.drawable.refresh)
setOnClickListener {
DownloadWorker.getInstance(context).let {
it.cancel(galleryID)
it.queue.add(galleryID)
}
downloader?.cancel(galleryID)
downloader?.download(galleryID)
}
}
@@ -450,8 +470,7 @@ class ReaderActivity : AppCompatActivity() {
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) // If download is finished, stop animating
if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download_cancel)

View File

@@ -27,15 +27,12 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.Pupil
import xyz.quaver.io.FileX
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
@@ -126,16 +123,14 @@ class SettingsActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this)
if (file?.canWrite() != true)
if (FileX(this, uri).canWrite())
Preferences["download_folder"] = uri.toString()
else
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
else
Preferences["dl_location"] = file.canonicalPath
}
}
}
@@ -146,11 +141,11 @@ class SettingsActivity : AppCompatActivity() {
if (!File(directory).canWrite())
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
else
Preferences["dl_location"] = File(directory).canonicalPath
Preferences["download_folder"] = File(directory).canonicalPath
}
}
else -> super.onActivityResult(requestCode, resultCode, data)

View File

@@ -18,21 +18,19 @@
package xyz.quaver.pupil.ui.dialog
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.item_dl_location.view.*
import androidx.core.net.toUri
import kotlinx.android.synthetic.main.item_download_folder.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R
@@ -44,7 +42,7 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_dl_location)
setTitle(R.string.settings_download_folder)
setView(build())
@@ -54,7 +52,7 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
}
private fun build() : View {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
@@ -62,13 +60,13 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
dir ?: return@forEachIndexed
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
location_type.text = context.getString(when (index) {
0 -> R.string.settings_dl_location_internal
else -> R.string.settings_dl_location_removable
0 -> R.string.settings_download_folder_internal
else -> R.string.settings_download_folder_removable
})
location_available.text = context.getString(
R.string.settings_dl_location_available,
R.string.settings_download_folder_available,
byteToString(dir.freeSpace)
)
setOnClickListener {
@@ -76,14 +74,14 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
pair.first.isChecked = false
}
button.performClick()
Preferences["dl_location"] = dir.canonicalPath
Preferences["download_folder"] = dir.toUri().toString()
}
buttons.add(button to dir)
})
}
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
location_type.text = context.getString(R.string.settings_dl_location_custom)
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
location_type.text = context.getString(R.string.settings_download_folder_custom)
setOnClickListener {
buttons.forEach { pair ->
pair.first.isChecked = false
@@ -91,17 +89,12 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), R.id.request_write_permission_and_saf.normalizeID())
else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, R.id.request_download_folder.normalizeID())
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, R.id.request_download_folder.normalizeID())
dismiss()
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()

View File

@@ -22,6 +22,7 @@ import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout.LayoutParams
@@ -36,15 +37,10 @@ import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
@@ -52,7 +48,7 @@ import xyz.quaver.pupil.histories
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
@@ -230,7 +226,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>()
val galleries = ArrayList<Int>()
val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag ->
@@ -240,19 +236,6 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
}
}
CoroutineScope(Dispatchers.Main).launch {
gallery.related.forEachIndexed { i, galleryID ->
async(Dispatchers.IO) {
Cache(context).getGalleryBlock(galleryID)
}.let {
val galleryBlock = it.await() ?: return@let
galleries.add(galleryBlock)
adapter.notifyItemInserted(i)
}
}
}
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related)
@@ -263,15 +246,15 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
ItemClickSupport.addTo(this).apply {
onItemClickListener = { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position].id)
putExtra("galleryID", galleries[position])
})
histories.add(galleries[position].id)
histories.add(galleries[position])
}
onItemLongClickListener = { _, position, _ ->
GalleryDialog(
context,
glide,
galleries[position].id
galleries[position]
).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
@@ -287,6 +270,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
}.let {
gallery_contents.addView(it)
}
CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID ->
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
galleries.add(galleryID)
withContext(Dispatchers.Main) {
adapter.notifyItemInserted(galleries.size-1)
}
}
}
}
}
}

View File

@@ -29,6 +29,7 @@ import androidx.preference.PreferenceFragmentCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.io.FileX
import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.ui.LockActivity
@@ -141,7 +142,7 @@ class SettingsFragment :
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"dl_location" -> {
"download_folder" -> {
DownloadLocationDialog(requireActivity()).show()
}
"default_query" -> {
@@ -208,9 +209,6 @@ class SettingsFragment :
"proxy" -> {
summary = context?.let { getProxyInfo().type.name }
}
"dl_location" -> {
summary = context?.let { getDownloadDirectory(it).canonicalPath }
}
}
}
}
@@ -275,8 +273,14 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
summary = getDownloadDirectory(requireContext()).canonicalPath
"download_folder" -> {
setSummaryProvider {
val uri: String = Preferences[it.key]
kotlin.runCatching {
FileX(context, uri).canonicalPath
}.getOrElse { "" }
}
onPreferenceClickListener = this@SettingsFragment
}

View File

@@ -25,8 +25,6 @@ import android.util.SparseArray
import androidx.preference.PreferenceManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

View File

@@ -20,50 +20,58 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import android.util.Log
import android.util.SparseArray
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGallery
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readBytes
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.io.util.*
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.formatDownloadFolder
import kotlin.io.deleteRecursively
import kotlin.io.writeText
@Serializable
data class Metadata(
var galleryBlock: GalleryBlock? = null,
var gallery: Gallery? = null,
var thumbnail: String? = null,
var reader: Reader? = null,
var imageList: MutableList<String?>? = null
) {
fun copy(): Metadata = Metadata(galleryBlock, gallery, thumbnail, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object {
private val mutex = Mutex()
private val instances = SparseArray<Cache>()
fun getInstance(context: Context, galleryID: Int) =
instances[galleryID] ?: synchronized(this) {
instances[galleryID] ?: runBlocking { mutex.withLock {
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
}
} }
fun delete(galleryID: Int) { runBlocking { mutex.withLock {
instances[galleryID]?.galleryFolder?.deleteRecursively()
instances.delete(galleryID)
} } }
}
init {
galleryFolder.mkdirs()
}
private val mutex = Mutex()
var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let {
Json.decodeFromString<Metadata>(it)
@@ -76,7 +84,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
val cacheFolder: FileX
get() = FileX(this, cacheDir, "imageCache/$galleryID")
val cachedGallery: FileX
val galleryFolder: FileX
get() = DownloadFolderManager.getInstance(this).getDownloadFolder(galleryID)
?: FileX(this, cacheDir, "imageCache/$galleryID")
@@ -87,16 +95,19 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
if (it.exists()) it else null
} }
@Synchronized
fun setMetadata(change: (Metadata) -> Unit) {
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun setMetadata(change: (Metadata) -> Unit) { mutex.withLock {
change.invoke(metadata)
val file = cachedGallery.getChild(".metadata")
val file = galleryFolder.getChild(".metadata")
CoroutineScope(Dispatchers.IO).launch {
file.writeText(Json.encodeToString(Metadata))
kotlin.runCatching {
file.createNewFile()
file.writeText(Json.encodeToString(metadata))
}
}
}
} }
suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
@@ -123,59 +134,47 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}
}
suspend fun getGallery(): Gallery? =
metadata.gallery
?: withContext(Dispatchers.IO) {
kotlin.runCatching {
getGallery(galleryID)
}.getOrNull()?.also {
launch { setMetadata { metadata ->
metadata.gallery = it
if (metadata.imageList == null)
metadata.imageList = MutableList(it.thumbnails.size) { null }
} }
}
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun getThumbnail(): String? =
metadata.thumbnail
?: withContext(Dispatchers.IO) {
getGalleryBlock()?.thumbnails?.firstOrNull()?.let { thumbnail ->
kotlin.runCatching {
val request = Request.Builder()
.url(thumbnail)
.build()
suspend fun getThumbnail(): ByteArray? =
findFile(".thumbnail")?.readBytes()
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(it)
.build()
val image = client.newCall(request).execute().body()?.use { it.bytes() }
kotlin.runCatching {
client.newCall(request).execute().body()?.use { it.bytes() }
}.getOrNull()?.also { kotlin.run {
galleryFolder.getChild(".thumbnail").writeBytes(it)
} }
} }
Base64.encodeToString(image, Base64.DEFAULT)
}.getOrNull()
}?.also {
launch { setMetadata { metadata -> metadata.thumbnail = it } }
}
}
suspend fun getReader(galleryID: Int): Reader? {
val mirrors = Preferences.get<String>("mirrors").split('>')
suspend fun getReader(): Reader? {
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).toSortedMap { o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
else
it
}
return metadata.reader
?: withContext(Dispatchers.IO) {
var reader: Reader? = null
for (source in sources) {
reader = try { withTimeoutOrNull(1000) {
reader = try {
source.value.invoke()
} } catch (e: Exception) { null }
} catch (e: Exception) {
null
}
if (reader != null)
break
if (reader != null)
break
}
reader?.also {
@@ -193,13 +192,11 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.imageList?.get(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext")
fun putImage(index: Int, fileName: String, data: ByteArray) = CoroutineScope(Dispatchers.IO).launch {
val file = FileX(this@Cache, cachedGallery, fileName).also {
it.createNewFile()
}
suspend fun putImage(index: Int, fileName: String, data: ByteArray) {
val file = galleryFolder.getChild(fileName)
file.createNewFile()
file.writeBytes(data)
setMetadata { metadata -> metadata.imageList!![index] = fileName }
}
@@ -208,11 +205,12 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
if (downloadFolder == null)
DownloadFolderManager.getInstance(this@Cache).addDownloadFolder(galleryID, this@Cache.formatDownloadFolder())
metadata.imageList?.forEach {
it ?: return@forEach
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder!!.getChild(it)
val source = cacheFolder.getChild(it)
Log.i("PUPIL", downloadFolder?.uri.toString())
val target = downloadFolder!!.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists())
return@forEach
@@ -223,6 +221,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}
}
Log.i("PUPIL", downloadFolder?.uri.toString())
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder!!.getChild(".metadata")

View File

@@ -24,13 +24,18 @@ import android.webkit.URLUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Call
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.pupil.client
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
class DownloadFolderManager private constructor(context: Context) : ContextWrapper(context) {
@@ -46,59 +51,70 @@ class DownloadFolderManager private constructor(context: Context) : ContextWrapp
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder = {
val uri: String = Preferences["download_directory"]
if (!URLUtil.isValidUrl(uri))
Preferences["download_directory"] = defaultDownloadFolder
val downloadFolder: FileX
get() = {
kotlin.runCatching {
FileX(this, Preferences.get<String>("download_folder"))
}.getOrElse {
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
defaultDownloadFolder
}
}.invoke()
private val downloadFolderMapMutex = Mutex()
private val downloadFolderMap: MutableMap<Int, String> = runBlocking { downloadFolderMapMutex.withLock {
kotlin.runCatching {
FileX(this, uri)
}.getOrElse {
Preferences["download_directory"] = defaultDownloadFolder
FileX(this, defaultDownloadFolder)
}
}.invoke()
private val downloadFolderMap: MutableMap<Int, String> =
kotlin.runCatching {
FileX(this@DownloadFolderManager, downloadFolder, ".download").readText()?.let {
downloadFolder.getChild(".download").readText()?.let {
Json.decodeFromString<MutableMap<Int, String>>(it)
}
}.getOrNull() ?: mutableMapOf()
private val downloadFolderMapMutex = Mutex()
} }
@Synchronized
fun getDownloadFolder(galleryID: Int): FileX? =
downloadFolderMap[galleryID]?.let { FileX(this, downloadFolder, it) }
fun isDownloading(galleryID: Int): Boolean {
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
@Synchronized
fun addDownloadFolder(galleryID: Int, name: String) {
if (downloadFolderMap.containsKey(galleryID))
return
if (FileX(this@DownloadFolderManager, downloadFolder, name).mkdir()) {
downloadFolderMap[galleryID] = name
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
FileX(this@DownloadFolderManager, downloadFolder, ".download").writeText(Json.encodeToString(downloadFolderMap))
} }
}
return downloadFolderMap.containsKey(galleryID)
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
}
@Synchronized
fun removeDownloadFolder(galleryID: Int) {
fun getDownloadFolder(galleryID: Int): FileX? = runBlocking { downloadFolderMapMutex.withLock {
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
} }
fun addDownloadFolder(galleryID: Int, name: String) { runBlocking { downloadFolderMapMutex.withLock {
if (downloadFolderMap.containsKey(galleryID))
return@withLock
val folder = downloadFolder.getChild(name)
if (!folder.exists())
folder.mkdirs()
downloadFolderMap[galleryID] = name
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
downloadFolder.getChild(".download").let {
it.createNewFile()
it.writeText(Json.encodeToString(downloadFolderMap))
}
} }
} } }
fun deleteDownloadFolder(galleryID: Int) { runBlocking { downloadFolderMapMutex.withLock {
if (!downloadFolderMap.containsKey(galleryID))
return
return@withLock
downloadFolderMap[galleryID]?.let {
if (FileX(this@DownloadFolderManager, downloadFolder, it).delete()) {
if (downloadFolder.getChild(it).delete()) {
downloadFolderMap.remove(galleryID)
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
FileX(this@DownloadFolderManager, downloadFolder, ".download").writeText(Json.encodeToString(downloadFolderMap))
downloadFolder.getChild(".download").let {
it.createNewFile()
it.writeText(Json.encodeToString(downloadFolderMap))
}
} }
}
}
}
} } }
}

View File

@@ -27,6 +27,7 @@ import java.io.FileOutputStream
import java.lang.reflect.Array
import java.net.URL
@Deprecated("Use downloader.Cache instead")
fun getCachedGallery(context: Context, galleryID: Int) =
File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists())
@@ -35,6 +36,7 @@ fun getCachedGallery(context: Context, galleryID: Int) =
File(context.cacheDir, "imageCache/$galleryID")
}
@Deprecated("Use downloader.Cache instead")
fun getDownloadDirectory(context: Context) =
Preferences.get<String>("dl_location").let {
if (it.isNotEmpty() && !it.startsWith("content"))
@@ -43,81 +45,6 @@ fun getDownloadDirectory(context: Context) =
context.getExternalFilesDir(null)!!
}
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
if (to.parentFile?.exists() == false)
to.parentFile!!.mkdirs()
if (!to.exists())
to.createNewFile()
FileOutputStream(to).use { out ->
with(openConnection()) {
val fileSize = contentLength.toLong()
getInputStream().use {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onDownloadProgress?.invoke(bytesCopied, fileSize)
bytes = it.read(buffer)
}
}
}
}
}
fun getExtSdCardPaths(context: Context) =
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
runCatching {
File(path).canonicalPath
}.getOrElse {
path
}
}
}
const val PRIMARY_VOLUME_NAME = "primary"
fun getVolumePath(context: Context, volumeID: String?): String? {
return runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
val getUUID = storageVolumeClass.getMethod("getUuid")
val getPath = storageVolumeClass.getMethod("getPath")
val isPrimary = storageVolumeClass.getMethod("isPrimary")
val result = getVolumeList.invoke(storageManager)!!
val length = Array.getLength(result)
for (i in 0 until length) {
val storageVolumeElement = Array.get(result, i)
val uuid = getUUID.invoke(storageVolumeElement) as? String
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
// primary volume?
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
return@runCatching getPath.invoke(storageVolumeElement) as? String
// other volumes?
if (volumeID == uuid) {
return@runCatching getPath.invoke(storageVolumeElement) as? String
}
}
return@runCatching null
}.getOrNull()
}
@Deprecated("Use FileX instead")
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

@@ -19,12 +19,23 @@
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.util.*
@@ -77,17 +88,47 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
}
val formatMap = mapOf<String, (Cache) -> (String)>(
"\$ID" to { runBlocking { it.getGalleryBlock()?.id.toString() } },
"\$TITLE" to { runBlocking { it.getGalleryBlock()?.title.toString() } },
"-id-" to { runBlocking { it.getGalleryBlock()?.id.toString() } },
"-title-" to { runBlocking { it.getGalleryBlock()?.title.toString() } },
// TODO
)
/**
* Formats download folder name with given Metadata
*/
fun Cache.formatDownloadFolder(): String {
return Preferences["download_folder_format", "\$ID"].apply {
formatMap.entries.forEach { (key, lambda) ->
this.replace(key, lambda.invoke(this@formatDownloadFolder))
fun Cache.formatDownloadFolder(): String =
Preferences["download_folder_format", "-id-"].let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}
fun Context.startForegroundServiceCompat(service: Intent) {
if (Build.VERSION.SDK_INT >= 26)
startForegroundService(service)
else
startService(service)
}
val Reader.requestBuilders: List<Request.Builder>
get() {
val galleryID = this.galleryInfo.id ?: 0
val lowQuality = Preferences["low_quality", true]
return when(code) {
Code.HITOMI -> {
this.galleryInfo.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
}
}
Code.HIYOBI -> {
createImgList(galleryID, this, lowQuality).map {
Request.Builder()
.url(it.path)
.header("User-Agent", user_agent)
.header("Cookie", cookie)
}
}
}
}

View File

@@ -96,18 +96,18 @@
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_dl_location">ダウンロード場所</string>
<string name="settings_dl_location_internal">内部ストレージ</string>
<string name="settings_dl_location_removable">外部SDカード</string>
<string name="settings_dl_location_available">%s 使用可能</string>
<string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_download_folder_internal">内部ストレージ</string>
<string name="settings_download_folder_removable">外部SDカード</string>
<string name="settings_download_folder_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_download_folder_custom">手動で設定</string>
<string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_proxy_title">プロクシ</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">プロクシタイプ</string>

View File

@@ -94,10 +94,10 @@
<string name="settings_backup_file_created">백업 파일을 생성하였습니다</string>
<string name="settings_restore_failed">복원에 실패했습니다</string>
<string name="settings_restore_success">%1$d개 항목을 복원했습니다</string>
<string name="settings_dl_location">다운로드 위치</string>
<string name="settings_dl_location_internal">내부 저장공간</string>
<string name="settings_dl_location_removable">외부 SD카드</string>
<string name="settings_dl_location_available">%s 사용 가능</string>
<string name="settings_download_folder">다운로드 위치</string>
<string name="settings_download_folder_internal">내부 저장공간</string>
<string name="settings_download_folder_removable">외부 SD카드</string>
<string name="settings_download_folder_available">%s 사용 가능</string>
<string name="update_download_completed">다운로드가 완료되었습니다</string>
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
<string name="settings_beta">베타 채널에서 업데이트</string>
@@ -106,8 +106,8 @@
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
<string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
<string name="settings_download_folder_custom">직접 설정</string>
<string name="settings_download_folder_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
<string name="settings_proxy_title">프록시</string>
<string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">프록시 타입</string>

View File

@@ -135,12 +135,12 @@
<string name="settings_clear_history">Clear history</string>
<string name="settings_clear_history_alert_message">Do you want to clear histories?</string>
<string name="settings_clear_history_summary">%1$d histories saved</string>
<string name="settings_dl_location">Download directory</string>
<string name="settings_dl_location_removable">Removable Storage</string>
<string name="settings_dl_location_internal">Internal Storage</string>
<string name="settings_dl_location_available">%s available</string>
<string name="settings_dl_location_custom">Custom Location</string>
<string name="settings_dl_location_not_writable">This folder is not writable. Please select another folder.</string>
<string name="settings_download_folder">Download directory</string>
<string name="settings_download_folder_removable">Removable Storage</string>
<string name="settings_download_folder_internal">Internal Storage</string>
<string name="settings_download_folder_available">%s available</string>
<string name="settings_download_folder_custom">Custom Location</string>
<string name="settings_download_folder_not_writable">This folder is not writable. Please select another folder.</string>
<string name="settings_cache_disable">Disable Cache</string>
<string name="settings_download_when_cache_disable_warning">Download is disabled when the cache is disabled</string>
<string name="settings_low_quality">Low quality images</string>

View File

@@ -44,8 +44,8 @@
app:title="@string/settings_clear_history"/>
<Preference
app:key="dl_location"
app:title="@string/settings_dl_location"/>
app:key="download_folder"
app:title="@string/settings_download_folder"/>
<SwitchPreferenceCompat
app:key="cache_disable"

View File

@@ -22,6 +22,7 @@ allprojects {
repositories {
google()
jcenter()
mavenLocal()
maven { url "https://jitpack.io" }
maven { url 'https://guardian.github.com/maven/repo-releases' }
}