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 ->