This commit is contained in:
tom5079
2020-12-27 13:39:57 +09:00
parent 3051d800bd
commit 8703fde9b1
9 changed files with 143 additions and 36 deletions

View File

@@ -44,10 +44,8 @@ android {
} }
buildTypes { buildTypes {
debug { debug {
minifyEnabled false minifyEnabled true
shrinkResources false shrinkResources true
multiDexEnabled true
debuggable true debuggable true
applicationIdSuffix ".debug" applicationIdSuffix ".debug"

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <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.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.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>

View File

@@ -151,12 +151,14 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
binding.artist.text = result.artists binding.artist.text = result.artists
with (binding.tagGroup) { CoroutineScope(Dispatchers.Main).launch {
tags.clear() with (binding.tagGroup) {
tags.addAll(result.tags.map { tags.clear()
Tag.parse(it) result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.map {
}) Tag.parse(it)
refresh() }?.let { tags.addAll(it) }
refresh()
}
} }
val extraType = listOf( val extraType = listOf(

View File

@@ -40,7 +40,6 @@ data class ItemInfo(
val title: String, val title: String,
val thumbnail: String, val thumbnail: String,
val artists: String, val artists: String,
val tags: List<String>,
val extra: Map<ExtraType, Deferred<String?>> = emptyMap() val extra: Map<ExtraType, Deferred<String?>> = emptyMap()
) { ) {
enum class ExtraType { enum class ExtraType {
@@ -48,8 +47,11 @@ data class ItemInfo(
CHARACTER, CHARACTER,
SERIES, SERIES,
TYPE, TYPE,
TAGS,
LANGUAGE, LANGUAGE,
PAGECOUNT PAGECOUNT,
PREVIEW,
RELATED_ITEM,
} }
@Serializable @Serializable
@@ -60,7 +62,6 @@ data class ItemInfo(
val title: String, val title: String,
val thumbnail: String, val thumbnail: String,
val artists: String, val artists: String,
val tags: List<String>,
val extra: Map<ExtraType, String?> = emptyMap() val extra: Map<ExtraType, String?> = emptyMap()
) )
@@ -74,7 +75,6 @@ data class ItemInfo(
value.title, value.title,
value.thumbnail, value.thumbnail,
value.artists, value.artists,
value.tags,
value.extra.mapValues { runBlocking { it.value.await() } } value.extra.mapValues { runBlocking { it.value.await() } }
) )
encoder.encodeSerializableValue(ItemInfoSurrogate.serializer(), surrogate) encoder.encodeSerializableValue(ItemInfoSurrogate.serializer(), surrogate)
@@ -88,7 +88,6 @@ data class ItemInfo(
surrogate.title, surrogate.title,
surrogate.thumbnail, surrogate.thumbnail,
surrogate.artists, surrogate.artists,
surrogate.tags,
surrogate.extra.mapValues { CoroutineScope(Dispatchers.Unconfined).async { it.value } } 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 search(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<ItemInfo>, Int>
abstract suspend fun suggestion(query: String) : List<Suggestion> abstract suspend fun suggestion(query: String) : List<Suggestion>
abstract suspend fun images(id: String) : List<String> 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> { open fun getHeadersForImage(id: String, url: String): Map<String, String> {
return emptyMap() return emptyMap()

View File

@@ -18,7 +18,6 @@
package xyz.quaver.pupil.sources package xyz.quaver.pupil.sources
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -93,6 +92,12 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
return Pair(channel, cache.size) 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> { override suspend fun images(id: String): List<String> {
val galleryID = id.toInt() 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> { override fun getHeadersForImage(id: String, url: String): Map<String, String> {
return mapOf( return mapOf(
"Referer" to getReferer(id.toInt()) "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) { override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: TagSuggestion) {
binding.leftIcon.setImageResource( binding.leftIcon.setImageResource(
when(item.n) { when(item.n) {
@@ -195,7 +217,6 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
galleryBlock.title, galleryBlock.title,
galleryBlock.thumbnails.first(), galleryBlock.thumbnails.first(),
galleryBlock.artists.joinToString { it.wordCapitalize() }, galleryBlock.artists.joinToString { it.wordCapitalize() },
galleryBlock.relatedTags,
mapOf( mapOf(
ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching { ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() } 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.LANGUAGE to CoroutineScope(Dispatchers.Unconfined).async { languageMap[galleryBlock.language] ?: galleryBlock.language },
ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching { ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
getGalleryInfo(galleryBlock.id).files.size.toString() getGalleryInfo(galleryBlock.id).files.size.toString()
}.getOrNull() } }.getOrNull() },
ExtraType.TAGS to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.relatedTags.joinToString() }
) )
) )
} }

View File

@@ -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) { override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: DefaultSearchSuggestion) {
val split = item.body.split(':', limit = 2) val split = item.body.split(':', limit = 2)
@@ -121,13 +125,13 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
galleryBlock.title, galleryBlock.title,
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg", "https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
galleryBlock.artists.joinToString { it.value.wordCapitalize() }, galleryBlock.artists.joinToString { it.value.wordCapitalize() },
galleryBlock.tags.map { it.value },
mapOf( mapOf(
ItemInfo.ExtraType.CHARACTER to async { galleryBlock.characters.joinToString { it.value.wordCapitalize() } }, ItemInfo.ExtraType.CHARACTER to async { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
ItemInfo.ExtraType.SERIES to async { galleryBlock.parodys.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.TYPE to async { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
ItemInfo.ExtraType.PAGECOUNT to async { getGalleryInfo(galleryBlock.id).files.size.toString() }, 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 } }
) )
) )
} }

View File

@@ -143,6 +143,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { 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) putExtra("android.content.extra.SHOW_ADVANCED", true)
} }

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -18,7 +18,13 @@
package xyz.quaver.pupil.util.downloader package xyz.quaver.pupil.util.downloader
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context 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 com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -26,10 +32,14 @@ import kotlinx.coroutines.launch
import okhttp3.* import okhttp3.*
import okio.* import okio.*
import xyz.quaver.pupil.PupilInterceptor import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.sources.sources import xyz.quaver.pupil.sources.sources
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.cleanCache import xyz.quaver.pupil.util.cleanCache
import xyz.quaver.pupil.util.normalizeID
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap 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 //region ProgressListener
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private val progressListener: ProgressListener = { (source, itemID, index), bytesRead, contentLength, done -> 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"] return progress["$source-$itemID"]
} }
fun isCompleted(source: String, itemID: String) = progress["$source-$itemID"]?.all { it == Float.POSITIVE_INFINITY } == true
fun cancel() { fun cancel() {
client.dispatcher().queuedCalls().filter { client.dispatcher().queuedCalls().filter {
it.request().tag() is Tag it.request().tag() is Tag
@@ -178,6 +263,7 @@ class Downloader private constructor(private val context: Context) {
if (isDownloading(source, itemID)) if (isDownloading(source, itemID))
return@launch return@launch
initNotification(source, itemID)
cleanCache(context) cleanCache(context)
val source = sources[source] ?: return@launch 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 if (cache.metadata.imageList?.get(i) == null) 0F else Float.POSITIVE_INFINITY
} }
with (Cache.getInstance(context, source.name, itemID).metadata) { if (cache.metadata.imageList == null)
if (imageList == null) cache.metadata.imageList = MutableList(it.size) { null }
imageList = MutableList(it.size) { null }
imageList!!.forEachIndexed { index, s ->
if (s != null)
progress["${source.name}-$itemID"]?.set(index, Float.POSITIVE_INFINITY)
}
}
onImageListLoadedCallback?.invoke(it) onImageListLoadedCallback?.invoke(it)
}.forEachIndexed { index, url -> }.forEachIndexed { index, url ->