WIP
This commit is contained in:
@@ -44,10 +44,8 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
|
||||
multiDexEnabled true
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
@@ -151,12 +151,14 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : 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(
|
||||
|
||||
@@ -40,7 +40,6 @@ data class ItemInfo(
|
||||
val title: String,
|
||||
val thumbnail: String,
|
||||
val artists: String,
|
||||
val tags: List<String>,
|
||||
val extra: Map<ExtraType, Deferred<String?>> = 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<String>,
|
||||
val extra: Map<ExtraType, String?> = 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<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
|
||||
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<ItemInfo>, Int>
|
||||
abstract suspend fun suggestion(query: String) : List<Suggestion>
|
||||
abstract suspend fun images(id: String) : List<String>
|
||||
/* abstract suspend */ fun info(id: String)/* : ItemInfo */{}
|
||||
abstract suspend fun info(id: String) : ItemInfo
|
||||
|
||||
open fun getHeadersForImage(id: String, url: String): Map<String, String> {
|
||||
return emptyMap()
|
||||
|
||||
@@ -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<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
||||
return Pair(channel, cache.size)
|
||||
}
|
||||
|
||||
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
||||
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
||||
TagSuggestion(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun images(id: String): List<String> {
|
||||
val galleryID = id.toInt()
|
||||
|
||||
@@ -103,18 +108,35 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
||||
}
|
||||
}
|
||||
|
||||
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<String, String> {
|
||||
return mapOf(
|
||||
"Referer" to getReferer(id.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun suggestion(query: String) : List<Hitomi.TagSuggestion> {
|
||||
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<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
||||
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<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
||||
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() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,10 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
||||
}
|
||||
}
|
||||
|
||||
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<DefaultSortMode, DefaultSearchSuggestion>() {
|
||||
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 } }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, NotificationCompat.Builder?>()
|
||||
|
||||
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 ->
|
||||
|
||||
Reference in New Issue
Block a user