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 { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 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-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" //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.patternlockview:patternlockview:1.0.0'
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' //implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation "ru.noties.markwon:core:3.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' 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' 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:rules:1.3.0'
androidTestImplementation 'androidx.test:runner: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 { androidExtensions {

View File

@@ -21,6 +21,7 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontobfuscate -dontobfuscate
-dontoptimize
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule { -keep class * extends com.bumptech.glide.module.AppGlideModule {
@@ -47,3 +48,4 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment -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.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
@@ -121,4 +122,9 @@ class ExampleInstrumentedTest {
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null") 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:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme" tools:replace="android:theme"
android:requestLegacyExternalStorage="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<provider <provider

View File

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

View File

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

View File

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

View File

@@ -18,24 +18,37 @@
package xyz.quaver.pupil.services package xyz.quaver.pupil.services
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okio.* import okio.*
import xyz.quaver.pupil.PupilInterceptor import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache 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 private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
class DownloadService : Service() { class DownloadService : Service() {
data class Tag(val galleryID: Int, val index: Int) data class Tag(val galleryID: Int, val index: Int)
//region Notification //region Notification
@@ -50,6 +63,53 @@ class DownloadService : Service() {
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setOngoing(true) .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 //endregion
//region ProgressListener //region ProgressListener
@@ -107,6 +167,7 @@ class DownloadService : Service() {
} }
//endregion //endregion
//region Downloader
/** /**
* KEY * KEY
* primary galleryID * primary galleryID
@@ -120,11 +181,163 @@ class DownloadService : Service() {
*/ */
val progress = SparseArray<MutableList<Float>?>() val progress = SparseArray<MutableList<Float>?>()
override fun onCreate() { fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it.isInfinite() } == true
startForeground(R.id.downloader_notification_id, serviceNotification.build())
interceptors[Tag::class] = interceptor 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() { inner class Binder : android.os.Binder() {
val service = this@DownloadService val service = this@DownloadService
@@ -133,20 +346,13 @@ class DownloadService : Service() {
private val binder = Binder() private val binder = Binder()
override fun onBind(p0: Intent?) = binder override fun onBind(p0: Intent?) = binder
val cache = SparseArray<Cache>() override fun onCreate() {
fun load(galleryID: Int) { startForeground(R.id.downloader_notification_id, serviceNotification.build())
if (progress.indexOfKey(galleryID) < 0) interceptors[Tag::class] = interceptor
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)
}
} }
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 package xyz.quaver.pupil.types
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) { 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.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery 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.types.Tags
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -92,7 +91,7 @@ class MainActivity : AppCompatActivity() {
POPULAR POPULAR
} }
private val galleries = ArrayList<GalleryBlock>() private val galleries = ArrayList<Int>()
private var query = "" private var query = ""
set(value) { set(value) {
@@ -112,6 +111,8 @@ class MainActivity : AppCompatActivity() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var currentPage = 0 private var currentPage = 0
private lateinit var downloadFolderManager: DownloadFolderManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -146,15 +147,7 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
Intent(this, DownloadService::class.java).let { downloadFolderManager = DownloadFolderManager.getInstance(this)
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
startForegroundService(it)
else ->
startService(it)
}
}
checkUpdate(this) checkUpdate(this)
initView() initView()
@@ -336,7 +329,7 @@ class MainActivity : AppCompatActivity() {
with(main_fab_cancel) { with(main_fab_cancel) {
setImageResource(R.drawable.cancel) setImageResource(R.drawable.cancel)
setOnClickListener { setOnClickListener {
DownloadWorker.getInstance(context).stop() DownloadService.cancel(this@MainActivity)
} }
} }
@@ -447,19 +440,15 @@ class MainActivity : AppCompatActivity() {
} }
} }
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id val galleryID = galleries[position]
val worker = DownloadWorker.getInstance(context)
if (Preferences["cache_disable"]) if (Preferences["cache_disable"])
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else { else {
if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress if (downloadFolderManager.isDownloading(galleryID)) { //download in progress
Cache(context).setDownloading(galleryID, false) DownloadService.cancel(this@MainActivity, galleryID)
worker.cancel(galleryID)
} }
else { else {
Cache(context).setDownloading(galleryID, true) DownloadService.download(this@MainActivity, galleryID)
worker.queue.add(galleryID)
} }
} }
@@ -467,12 +456,8 @@ class MainActivity : AppCompatActivity() {
} }
onDeleteClickedHandler = { position -> 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)
Cache(context).getCachedGallery(galleryID).deleteRecursively()
histories.remove(galleryID) histories.remove(galleryID)
@@ -485,7 +470,6 @@ class MainActivity : AppCompatActivity() {
} }
completeFlag.put(galleryID, false) completeFlag.put(galleryID, false)
}
closeAllItems() closeAllItems()
} }
@@ -496,8 +480,7 @@ class MainActivity : AppCompatActivity() {
return@listener return@listener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position] intent.putExtra("galleryID", galleries[position])
intent.putExtra("galleryID", gallery.id)
//TODO: Maybe sprinkling some transitions will be nice :D //TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent) startActivity(intent)
@@ -507,7 +490,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView) if (v !is CardView)
return@listener false return@listener false
val galleryID = galleries[position].id val galleryID = galleries[position]
GalleryDialog( GalleryDialog(
this@MainActivity, this@MainActivity,
@@ -762,7 +745,7 @@ class MainActivity : AppCompatActivity() {
if (!favoritesFile.exists()) { if (!favoritesFile.exists()) {
favoritesFile.createNewFile() favoritesFile.createNewFile()
favoritesFile.writeText(Json.encodeToString(Tags())) favoritesFile.writeText("[]")
} }
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener { setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
@@ -788,7 +771,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
R.id.main_menu_sort_newest -> { R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST sortMode = MainActivity.SortMode.NEWEST
it.isChecked = true it.isChecked = true
runOnUiThread { runOnUiThread {
@@ -1023,7 +1006,7 @@ class MainActivity : AppCompatActivity() {
totalItems = it.size totalItems = it.size
} }
} }
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also { else -> doSearch("$defaultQuery $query", sortMode == MainActivity.SortMode.POPULAR).also {
totalItems = it.size totalItems = it.size
} }
} }
@@ -1107,16 +1090,16 @@ class MainActivity : AppCompatActivity() {
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {
Cache(this@MainActivity).getGalleryBlock(galleryID) Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
galleryID
}
} }
}.forEach { }.forEach {
val galleryBlock = it.await() it.await()?.also {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
main_progressbar.hide() main_progressbar.hide()
if (galleryBlock != null) { galleries.add(it)
galleries.add(galleryBlock)
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1) main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
} }
} }

View File

@@ -18,10 +18,14 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -47,9 +51,10 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences 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.download.DownloadWorker import xyz.quaver.pupil.util.downloader.DownloadFolderManager
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.timer 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 val timer = Timer()
private var autoTimer: Timer? = null private var autoTimer: Timer? = null
@@ -81,6 +102,7 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader)
title = getString(R.string.reader_loading) title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
@@ -89,23 +111,18 @@ class ReaderActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE) WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_reader)
handleIntent(intent) handleIntent(intent)
cache = Cache.getInstance(this, galleryID)
histories.add(galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID) FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
if (galleryID == 0) { if (galleryID == 0) {
onBackPressed() onBackPressed()
return return
} }
initView()
if (Preferences["cache_disable"]) { if (Preferences["cache_disable"]) {
reader_download_progressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@ReaderActivity).getReader(galleryID) val reader = cache.getReader()
launch(Dispatchers.Main) initDownloader@{ launch(Dispatchers.Main) initDownloader@{
if (reader == null) { if (reader == null) {
@@ -115,6 +132,7 @@ class ReaderActivity : AppCompatActivity() {
return@initDownloader return@initDownloader
} }
histories.add(galleryID)
(reader_recyclerview.adapter as ReaderAdapter).apply { (reader_recyclerview.adapter as ReaderAdapter).apply {
this.reader = reader this.reader = reader
notifyDataSetChanged() notifyDataSetChanged()
@@ -132,6 +150,8 @@ class ReaderActivity : AppCompatActivity() {
} }
} else } else
initDownloader() initDownloader()
initView()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -187,9 +207,9 @@ class ReaderActivity : AppCompatActivity() {
R.id.reader_menu_page_indicator -> { R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue = 1
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0 maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
value=currentPage value = currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
setView(view) setView(view)
@@ -224,8 +244,12 @@ class ReaderActivity : AppCompatActivity() {
timer.cancel() timer.cancel()
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel() (reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!Cache(this).isDownloading(galleryID)) if (deleteOnExit) {
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID) downloader?.cancel(galleryID)
DownloadFolderManager.getInstance(this).deleteDownloadFolder(galleryID)
}
unbindService(conn)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -261,16 +285,16 @@ class ReaderActivity : AppCompatActivity() {
} }
private fun initDownloader() { private fun initDownloader() {
val worker = DownloadWorker.getInstance(this).apply { DownloadService.download(this, galleryID)
cancel(galleryID) bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
queue.add(galleryID)
}
timer.schedule(1000, 1000) { 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 return@schedule
if (worker.progress[galleryID] == null) { //Gallery not found if (downloader.progress[galleryID] == null) { //Gallery not found
timer.cancel() timer.cancel()
Snackbar Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE) .make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
@@ -279,14 +303,13 @@ class ReaderActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 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 reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID) val reader = cache.metadata.reader
if (reader != null) { if (reader != null) {
with (reader_recyclerview.adapter as ReaderAdapter) { with (reader_recyclerview.adapter as ReaderAdapter) {
this.reader = reader this.reader = reader
notifyDataSetChanged() 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 reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false) animateDownloadFAB(false)
@@ -315,7 +338,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID).apply { adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
isScroll = false isScroll = false
@@ -350,19 +373,18 @@ class ReaderActivity : AppCompatActivity() {
} }
with(reader_fab_download) { 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 { setOnClickListener {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
else { else {
if (Cache(context).isDownloading(galleryID)) { if (deleteOnExit) {
Cache(context).setDownloading(galleryID, false) deleteOnExit = false
cache.moveToDownload()
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true) animateDownloadFAB(true)
} else {
animateDownloadFAB(false)
} }
} }
} }
@@ -371,10 +393,8 @@ class ReaderActivity : AppCompatActivity() {
with(reader_fab_retry) { with(reader_fab_retry) {
setImageResource(R.drawable.refresh) setImageResource(R.drawable.refresh)
setOnClickListener { setOnClickListener {
DownloadWorker.getInstance(context).let { downloader?.cancel(galleryID)
it.cancel(galleryID) downloader?.download(galleryID)
it.queue.add(galleryID)
}
} }
} }
@@ -450,8 +470,7 @@ class ReaderActivity : AppCompatActivity() {
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() { icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) { override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context) if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) // If download is finished, stop animating
post { post {
setImageResource(R.drawable.ic_download) setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download_cancel) 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.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.* import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.io.util.toFile import xyz.quaver.io.FileX
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
@@ -126,16 +123,14 @@ class SettingsActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags) contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this) if (FileX(this, uri).canWrite())
Preferences["download_folder"] = uri.toString()
if (file?.canWrite() != true) else
Snackbar.make( Snackbar.make(
settings, settings,
R.string.settings_dl_location_not_writable, R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).show()
else
Preferences["dl_location"] = file.canonicalPath
} }
} }
} }
@@ -146,11 +141,11 @@ class SettingsActivity : AppCompatActivity() {
if (!File(directory).canWrite()) if (!File(directory).canWrite())
Snackbar.make( Snackbar.make(
settings, settings,
R.string.settings_dl_location_not_writable, R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).show()
else else
Preferences["dl_location"] = File(directory).canonicalPath Preferences["download_folder"] = File(directory).canonicalPath
} }
} }
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)

View File

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

View File

@@ -22,6 +22,7 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout.LayoutParams 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_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.* import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.* import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Gallery import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery import xyz.quaver.hitomi.getGallery
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter 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.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport 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 import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) { 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) { private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>() val galleries = ArrayList<Int>()
val adapter = GalleryBlockAdapter(glide, galleries).apply { val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag -> 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 { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related) gallery_details.setText(R.string.gallery_related)
@@ -263,15 +246,15 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
ItemClickSupport.addTo(this).apply { ItemClickSupport.addTo(this).apply {
onItemClickListener = { _, position, _ -> onItemClickListener = { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply { 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, _ -> onItemLongClickListener = { _, position, _ ->
GalleryDialog( GalleryDialog(
context, context,
glide, glide,
galleries[position].id galleries[position]
).apply { ).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) } this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
@@ -287,6 +270,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
}.let { }.let {
gallery_contents.addView(it) 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.io.FileX
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
@@ -141,7 +142,7 @@ class SettingsFragment :
setNegativeButton(android.R.string.no) { _, _ -> } setNegativeButton(android.R.string.no) { _, _ -> }
}.show() }.show()
} }
"dl_location" -> { "download_folder" -> {
DownloadLocationDialog(requireActivity()).show() DownloadLocationDialog(requireActivity()).show()
} }
"default_query" -> { "default_query" -> {
@@ -208,9 +209,6 @@ class SettingsFragment :
"proxy" -> { "proxy" -> {
summary = context?.let { getProxyInfo().type.name } summary = context?.let { getProxyInfo().type.name }
} }
"dl_location" -> {
summary = context?.let { getDownloadDirectory(it).canonicalPath }
}
} }
} }
} }
@@ -275,8 +273,14 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dl_location" -> { "download_folder" -> {
summary = getDownloadDirectory(requireContext()).canonicalPath setSummaryProvider {
val uri: String = Preferences[it.key]
kotlin.runCatching {
FileX(context, uri).canonicalPath
}.getOrElse { "" }
}
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }

View File

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

View File

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

View File

@@ -24,13 +24,18 @@ import android.webkit.URLUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Call
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText import xyz.quaver.io.util.readText
import xyz.quaver.pupil.client
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
class DownloadFolderManager private constructor(context: Context) : ContextWrapper(context) { 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 defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder = { val downloadFolder: FileX
val uri: String = Preferences["download_directory"] get() = {
if (!URLUtil.isValidUrl(uri))
Preferences["download_directory"] = defaultDownloadFolder
kotlin.runCatching { kotlin.runCatching {
FileX(this, uri) FileX(this, Preferences.get<String>("download_folder"))
}.getOrElse { }.getOrElse {
Preferences["download_directory"] = defaultDownloadFolder Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
FileX(this, defaultDownloadFolder) defaultDownloadFolder
} }
}.invoke() }.invoke()
private val downloadFolderMap: MutableMap<Int, String> = private val downloadFolderMapMutex = Mutex()
private val downloadFolderMap: MutableMap<Int, String> = runBlocking { downloadFolderMapMutex.withLock {
kotlin.runCatching { kotlin.runCatching {
FileX(this@DownloadFolderManager, downloadFolder, ".download").readText()?.let { downloadFolder.getChild(".download").readText()?.let {
Json.decodeFromString<MutableMap<Int, String>>(it) Json.decodeFromString<MutableMap<Int, String>>(it)
} }
}.getOrNull() ?: mutableMapOf() }.getOrNull() ?: mutableMapOf()
private val downloadFolderMapMutex = Mutex() } }
@Synchronized fun isDownloading(galleryID: Int): Boolean {
fun getDownloadFolder(galleryID: Int): FileX? = val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
downloadFolderMap[galleryID]?.let { FileX(this, downloadFolder, it) }
@Synchronized return downloadFolderMap.containsKey(galleryID)
fun addDownloadFolder(galleryID: Int, name: String) { && client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
}
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)) if (downloadFolderMap.containsKey(galleryID))
return return@withLock
val folder = downloadFolder.getChild(name)
if (!folder.exists())
folder.mkdirs()
if (FileX(this@DownloadFolderManager, downloadFolder, name).mkdir()) {
downloadFolderMap[galleryID] = name downloadFolderMap[galleryID] = name
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock { 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))
}
} } } }
} } } }
}
@Synchronized fun deleteDownloadFolder(galleryID: Int) { runBlocking { downloadFolderMapMutex.withLock {
fun removeDownloadFolder(galleryID: Int) {
if (!downloadFolderMap.containsKey(galleryID)) if (!downloadFolderMap.containsKey(galleryID))
return return@withLock
downloadFolderMap[galleryID]?.let { downloadFolderMap[galleryID]?.let {
if (FileX(this@DownloadFolderManager, downloadFolder, it).delete()) { if (downloadFolder.getChild(it).delete()) {
downloadFolderMap.remove(galleryID) downloadFolderMap.remove(galleryID)
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock { 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.lang.reflect.Array
import java.net.URL import java.net.URL
@Deprecated("Use downloader.Cache instead")
fun getCachedGallery(context: Context, galleryID: Int) = fun getCachedGallery(context: Context, galleryID: Int) =
File(getDownloadDirectory(context), galleryID.toString()).let { File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists()) if (it.exists())
@@ -35,6 +36,7 @@ fun getCachedGallery(context: Context, galleryID: Int) =
File(context.cacheDir, "imageCache/$galleryID") File(context.cacheDir, "imageCache/$galleryID")
} }
@Deprecated("Use downloader.Cache instead")
fun getDownloadDirectory(context: Context) = fun getDownloadDirectory(context: Context) =
Preferences.get<String>("dl_location").let { Preferences.get<String>("dl_location").let {
if (it.isNotEmpty() && !it.startsWith("content")) if (it.isNotEmpty() && !it.startsWith("content"))
@@ -43,81 +45,6 @@ fun getDownloadDirectory(context: Context) =
context.getExternalFilesDir(null)!! context.getExternalFilesDir(null)!!
} }
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) { @Deprecated("Use FileX instead")
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()
}
fun File.isParentOf(another: File) = fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath) another.absolutePath.startsWith(this.absolutePath)

View File

@@ -19,12 +19,23 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint 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.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient 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.Cache
import xyz.quaver.pupil.util.downloader.Metadata import xyz.quaver.pupil.util.downloader.Metadata
import java.util.* import java.util.*
@@ -77,17 +88,47 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
} }
val formatMap = mapOf<String, (Cache) -> (String)>( val formatMap = mapOf<String, (Cache) -> (String)>(
"\$ID" to { runBlocking { it.getGalleryBlock()?.id.toString() } }, "-id-" to { runBlocking { it.getGalleryBlock()?.id.toString() } },
"\$TITLE" to { runBlocking { it.getGalleryBlock()?.title.toString() } }, "-title-" to { runBlocking { it.getGalleryBlock()?.title.toString() } },
// TODO // TODO
) )
/** /**
* Formats download folder name with given Metadata * Formats download folder name with given Metadata
*/ */
fun Cache.formatDownloadFolder(): String { fun Cache.formatDownloadFolder(): String =
return Preferences["download_folder_format", "\$ID"].apply { Preferences["download_folder_format", "-id-"].let {
formatMap.entries.forEach { (key, lambda) -> formatMap.entries.fold(it) { str, (k, v) ->
this.replace(key, lambda.invoke(this@formatDownloadFolder)) 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_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_failed">復元に失敗しました</string> <string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string> <string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_dl_location">ダウンロード場所</string> <string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_dl_location_internal">内部ストレージ</string> <string name="settings_download_folder_internal">内部ストレージ</string>
<string name="settings_dl_location_removable">外部SDカード</string> <string name="settings_download_folder_removable">外部SDカード</string>
<string name="settings_dl_location_available">%s 使用可能</string> <string name="settings_download_folder_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string> <string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string> <string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string> <string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string> <string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string> <string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string> <string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string> <string name="settings_download_folder_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string> <string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_proxy_title">プロクシ</string> <string name="settings_proxy_title">プロクシ</string>
<string name="proxy_dialog_username_hint">ID</string> <string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">プロクシタイプ</string> <string name="proxy_dialog_type">プロクシタイプ</string>

View File

@@ -94,10 +94,10 @@
<string name="settings_backup_file_created">백업 파일을 생성하였습니다</string> <string name="settings_backup_file_created">백업 파일을 생성하였습니다</string>
<string name="settings_restore_failed">복원에 실패했습니다</string> <string name="settings_restore_failed">복원에 실패했습니다</string>
<string name="settings_restore_success">%1$d개 항목을 복원했습니다</string> <string name="settings_restore_success">%1$d개 항목을 복원했습니다</string>
<string name="settings_dl_location">다운로드 위치</string> <string name="settings_download_folder">다운로드 위치</string>
<string name="settings_dl_location_internal">내부 저장공간</string> <string name="settings_download_folder_internal">내부 저장공간</string>
<string name="settings_dl_location_removable">외부 SD카드</string> <string name="settings_download_folder_removable">외부 SD카드</string>
<string name="settings_dl_location_available">%s 사용 가능</string> <string name="settings_download_folder_available">%s 사용 가능</string>
<string name="update_download_completed">다운로드가 완료되었습니다</string> <string name="update_download_completed">다운로드가 완료되었습니다</string>
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string> <string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
<string name="settings_beta">베타 채널에서 업데이트</string> <string name="settings_beta">베타 채널에서 업데이트</string>
@@ -106,8 +106,8 @@
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string> <string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string> <string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
<string name="settings_mirror_title">미러 설정</string> <string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string> <string name="settings_download_folder_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string> <string name="settings_download_folder_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
<string name="settings_proxy_title">프록시</string> <string name="settings_proxy_title">프록시</string>
<string name="proxy_dialog_username_hint">ID</string> <string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">프록시 타입</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">Clear history</string>
<string name="settings_clear_history_alert_message">Do you want to clear histories?</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_clear_history_summary">%1$d histories saved</string>
<string name="settings_dl_location">Download directory</string> <string name="settings_download_folder">Download directory</string>
<string name="settings_dl_location_removable">Removable Storage</string> <string name="settings_download_folder_removable">Removable Storage</string>
<string name="settings_dl_location_internal">Internal Storage</string> <string name="settings_download_folder_internal">Internal Storage</string>
<string name="settings_dl_location_available">%s available</string> <string name="settings_download_folder_available">%s available</string>
<string name="settings_dl_location_custom">Custom Location</string> <string name="settings_download_folder_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_not_writable">This folder is not writable. Please select another folder.</string>
<string name="settings_cache_disable">Disable Cache</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_download_when_cache_disable_warning">Download is disabled when the cache is disabled</string>
<string name="settings_low_quality">Low quality images</string> <string name="settings_low_quality">Low quality images</string>

View File

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

View File

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