diff --git a/app/build.gradle b/app/build.gradle
index 637864d7..15b3b2b5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -44,10 +44,8 @@ android {
}
buildTypes {
debug {
- minifyEnabled false
- shrinkResources false
-
- multiDexEnabled true
+ minifyEnabled true
+ shrinkResources true
debuggable true
applicationIdSuffix ".debug"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8d3b3cfa..1ddb7c2b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
index 7daa1976..eedcc9d2 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
@@ -151,12 +151,14 @@ class SearchResultsAdapter(private val results: List) : RecyclerSwipeA
binding.artist.text = result.artists
- with (binding.tagGroup) {
- tags.clear()
- tags.addAll(result.tags.map {
- Tag.parse(it)
- })
- refresh()
+ CoroutineScope(Dispatchers.Main).launch {
+ with (binding.tagGroup) {
+ tags.clear()
+ result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.map {
+ Tag.parse(it)
+ }?.let { tags.addAll(it) }
+ refresh()
+ }
}
val extraType = listOf(
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
index 938dfc5b..09ad6432 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
@@ -40,7 +40,6 @@ data class ItemInfo(
val title: String,
val thumbnail: String,
val artists: String,
- val tags: List,
val extra: Map> = emptyMap()
) {
enum class ExtraType {
@@ -48,8 +47,11 @@ data class ItemInfo(
CHARACTER,
SERIES,
TYPE,
+ TAGS,
LANGUAGE,
- PAGECOUNT
+ PAGECOUNT,
+ PREVIEW,
+ RELATED_ITEM,
}
@Serializable
@@ -60,7 +62,6 @@ data class ItemInfo(
val title: String,
val thumbnail: String,
val artists: String,
- val tags: List,
val extra: Map = emptyMap()
)
@@ -74,7 +75,6 @@ data class ItemInfo(
value.title,
value.thumbnail,
value.artists,
- value.tags,
value.extra.mapValues { runBlocking { it.value.await() } }
)
encoder.encodeSerializableValue(ItemInfoSurrogate.serializer(), surrogate)
@@ -88,7 +88,6 @@ data class ItemInfo(
surrogate.title,
surrogate.thumbnail,
surrogate.artists,
- surrogate.tags,
surrogate.extra.mapValues { CoroutineScope(Dispatchers.Unconfined).async { it.value } }
)
}
@@ -119,7 +118,7 @@ abstract class Source, Suggestion: SearchSu
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair, Int>
abstract suspend fun suggestion(query: String) : List
abstract suspend fun images(id: String) : List
- /* abstract suspend */ fun info(id: String)/* : ItemInfo */{}
+ abstract suspend fun info(id: String) : ItemInfo
open fun getHeadersForImage(id: String, url: String): Map {
return emptyMap()
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
index 09b31e92..40c089cc 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
@@ -18,7 +18,6 @@
package xyz.quaver.pupil.sources
-import android.util.Log
import android.view.LayoutInflater
import android.widget.TextView
import kotlinx.coroutines.*
@@ -93,6 +92,12 @@ class Hitomi : Source() {
return Pair(channel, cache.size)
}
+ override suspend fun suggestion(query: String) : List {
+ return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
+ TagSuggestion(it)
+ }
+ }
+
override suspend fun images(id: String): List {
val galleryID = id.toInt()
@@ -103,18 +108,35 @@ class Hitomi : Source() {
}
}
+ override suspend fun info(id: String): ItemInfo = coroutineScope {
+ getGallery(id.toInt()).let {
+ ItemInfo(
+ name,
+ id,
+ it.title,
+ it.cover,
+ it.artists.joinToString { it.wordCapitalize() },
+ mapOf(
+ ExtraType.TYPE to async { it.type.wordCapitalize() },
+ ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
+ ExtraType.LANGUAGE to async { languageMap[it.language] ?: it.language },
+ ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
+ ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
+ ExtraType.TAGS to async { it.tags.joinToString() },
+ ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
+ ExtraType.RELATED_ITEM to async { it.related.joinToString() },
+ ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
+ )
+ )
+ }
+ }
+
override fun getHeadersForImage(id: String, url: String): Map {
return mapOf(
"Referer" to getReferer(id.toInt())
)
}
- override suspend fun suggestion(query: String) : List {
- return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
- TagSuggestion(it)
- }
- }
-
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: TagSuggestion) {
binding.leftIcon.setImageResource(
when(item.n) {
@@ -195,7 +217,6 @@ class Hitomi : Source() {
galleryBlock.title,
galleryBlock.thumbnails.first(),
galleryBlock.artists.joinToString { it.wordCapitalize() },
- galleryBlock.relatedTags,
mapOf(
ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() }
@@ -205,7 +226,8 @@ class Hitomi : Source() {
ExtraType.LANGUAGE to CoroutineScope(Dispatchers.Unconfined).async { languageMap[galleryBlock.language] ?: galleryBlock.language },
ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
getGalleryInfo(galleryBlock.id).files.size.toString()
- }.getOrNull() }
+ }.getOrNull() },
+ ExtraType.TAGS to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.relatedTags.joinToString() }
)
)
}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
index 3c20f260..e1c084ba 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
@@ -78,6 +78,10 @@ class Hiyobi : Source() {
}
}
+ override suspend fun info(id: String): ItemInfo {
+ return transform(name, getGalleryBlock(id))
+ }
+
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: DefaultSearchSuggestion) {
val split = item.body.split(':', limit = 2)
@@ -121,13 +125,13 @@ class Hiyobi : Source() {
galleryBlock.title,
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
galleryBlock.artists.joinToString { it.value.wordCapitalize() },
- galleryBlock.tags.map { it.value },
mapOf(
ItemInfo.ExtraType.CHARACTER to async { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
ItemInfo.ExtraType.SERIES to async { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
ItemInfo.ExtraType.TYPE to async { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
ItemInfo.ExtraType.PAGECOUNT to async { getGalleryInfo(galleryBlock.id).files.size.toString() },
- ItemInfo.ExtraType.GROUP to async { galleryBlock.groups.joinToString { it.value.wordCapitalize() } }
+ ItemInfo.ExtraType.GROUP to async { galleryBlock.groups.joinToString { it.value.wordCapitalize() } },
+ ItemInfo.ExtraType.TAGS to async { galleryBlock.tags.joinToString() { it.value } }
)
)
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
index cc370f13..c3ffa33a 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
@@ -143,6 +143,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
index e8364b46..2ece9e06 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
+import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
index 864f12f0..e3d001ff 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
@@ -18,7 +18,13 @@
package xyz.quaver.pupil.util.downloader
+import android.annotation.SuppressLint
+import android.app.PendingIntent
import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.TaskStackBuilder
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -26,10 +32,14 @@ import kotlinx.coroutines.launch
import okhttp3.*
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.services.DownloadService
import xyz.quaver.pupil.sources.sources
+import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.cleanCache
+import xyz.quaver.pupil.util.normalizeID
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
@@ -51,6 +61,79 @@ class Downloader private constructor(private val context: Context) {
}
}
+ //region Notification
+ private val notificationManager by lazy {
+ NotificationManagerCompat.from(context)
+ }
+
+ private val serviceNotification by lazy {
+ NotificationCompat.Builder(context, "downloader")
+ .setContentTitle(context.getString(R.string.downloader_running))
+ .setProgress(0, 0, false)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setOngoing(true)
+ }
+
+ private val notification = ConcurrentHashMap()
+
+ private fun initNotification(source: String, itemID: String) {
+ val key = "$source-$itemID"
+
+ val intent = Intent(context, ReaderActivity::class.java)
+ .putExtra("source", source)
+ .putExtra("itemID", itemID)
+
+ val pendingIntent = TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(intent)
+ getPendingIntent(itemID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+ val action =
+ NotificationCompat.Action.Builder(0, context.getText(android.R.string.cancel),
+ PendingIntent.getService(
+ context,
+ R.id.notification_download_cancel_action.normalizeID(),
+ Intent(context, DownloadService::class.java)
+ .putExtra(DownloadService.KEY_COMMAND, DownloadService.COMMAND_CANCEL)
+ .putExtra(DownloadService.KEY_ID, itemID),
+ PendingIntent.FLAG_UPDATE_CURRENT),
+ ).build()
+
+ notification[key] = NotificationCompat.Builder(context, "download").apply {
+ setContentTitle(context.getString(R.string.reader_loading))
+ setContentText(context.getString(R.string.reader_notification_text))
+ setSmallIcon(R.drawable.ic_notification)
+ setContentIntent(pendingIntent)
+ addAction(action)
+ setProgress(0, 0, true)
+ setOngoing(true)
+ }
+
+ notify(source, itemID)
+ }
+
+ @SuppressLint("RestrictedApi")
+ private fun notify(source: String, itemID: String) {
+ val key = "$source-$itemID"
+ val max = progress[key]?.size ?: 0
+ val progress = progress[key]?.count { it == Float.POSITIVE_INFINITY } ?: 0
+
+ val notification = notification[key] ?: return
+
+ if (isCompleted(source, itemID)) {
+ notification
+ .setContentText(context.getString(R.string.reader_notification_complete))
+ .setProgress(0, 0, false)
+ .setOngoing(false)
+ .mActions.clear()
+
+ notificationManager.cancel(key.hashCode())
+ } else
+ notification
+ .setProgress(max, progress, false)
+ .setContentText("$progress/$max")
+ }
+ //endregion
+
//region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener: ProgressListener = { (source, itemID, index), bytesRead, contentLength, done ->
@@ -134,6 +217,8 @@ class Downloader private constructor(private val context: Context) {
return progress["$source-$itemID"]
}
+ fun isCompleted(source: String, itemID: String) = progress["$source-$itemID"]?.all { it == Float.POSITIVE_INFINITY } == true
+
fun cancel() {
client.dispatcher().queuedCalls().filter {
it.request().tag() is Tag
@@ -178,6 +263,7 @@ class Downloader private constructor(private val context: Context) {
if (isDownloading(source, itemID))
return@launch
+ initNotification(source, itemID)
cleanCache(context)
val source = sources[source] ?: return@launch
@@ -188,15 +274,8 @@ class Downloader private constructor(private val context: Context) {
if (cache.metadata.imageList?.get(i) == null) 0F else Float.POSITIVE_INFINITY
}
- with (Cache.getInstance(context, source.name, itemID).metadata) {
- if (imageList == null)
- imageList = MutableList(it.size) { null }
-
- imageList!!.forEachIndexed { index, s ->
- if (s != null)
- progress["${source.name}-$itemID"]?.set(index, Float.POSITIVE_INFINITY)
- }
- }
+ if (cache.metadata.imageList == null)
+ cache.metadata.imageList = MutableList(it.size) { null }
onImageListLoadedCallback?.invoke(it)
}.forEachIndexed { index, url ->