Rebase source onto dev

This commit is contained in:
tom5079
2020-11-27 14:55:58 +09:00
parent 74ed9e9e42
commit aa6cc80172
21 changed files with 493 additions and 909 deletions

View File

@@ -123,7 +123,7 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0" implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.8.16" implementation "xyz.quaver:libpupil:1.9.0"
implementation "xyz.quaver:documentfilex:0.4-alpha02" implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.0.7" implementation "xyz.quaver:floatingsearchview:1.0.7"

View File

@@ -51,9 +51,9 @@ import kotlin.reflect.KClass
typealias PupilInterceptor = (Interceptor.Chain) -> Response typealias PupilInterceptor = (Interceptor.Chain) -> Response
lateinit var histories: SavedSet<Int> lateinit var histories: SavedSet<String>
private set private set
lateinit var favorites: SavedSet<Int> lateinit var favorites: SavedSet<String>
private set private set
lateinit var favoriteTags: SavedSet<Tag> lateinit var favoriteTags: SavedSet<Tag>
private set private set
@@ -108,8 +108,6 @@ class Pupil : Application() {
if (!FileX(this, it).canWrite()) if (!FileX(this, it).canWrite())
throw Exception() throw Exception()
DownloadManager.getInstance(this).migrate()
} }
} catch (e: Exception) { } catch (e: Exception) {
Preferences.remove("download_folder") Preferences.remove("download_folder")
@@ -120,8 +118,8 @@ class Pupil : Application() {
Preferences["reset_secure"] = true Preferences["reset_secure"] = true
} }
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0) histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), "")
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0) favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), "")
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse("")) favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "") searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")

View File

@@ -1,323 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.github.piasy.biv.loader.ImageLoader
import kotlinx.coroutines.*
import xyz.quaver.hitomi.getGallery
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.GalleryblockItemBinding
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.wordCapitalize
import java.io.File
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
var updateAll = true
var thin: Boolean = Preferences["thin"]
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
private var galleryID: Int = 0
init {
CoroutineScope(Dispatchers.Main).launch {
while (updateAll) {
updateProgress(itemView.context)
delay(1000)
}
}
}
private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
with(binding.galleryblockCard) {
val imageList = Cache.getInstance(context, galleryID).metadata.imageList
if (imageList == null) {
max = 0
return@with
}
progress = imageList.count { it != null }
max = imageList.size
this@GalleryViewHolder.binding.galleryblockId.setOnClickListener {
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("gallery_id", galleryID.toString())
)
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
type = if (!imageList.contains(null)) {
val downloadManager = DownloadManager.getInstance(context)
if (downloadManager.getDownloadFolder(galleryID) == null)
ProgressCard.Type.CACHE
else
ProgressCard.Type.DOWNLOAD
} else
ProgressCard.Type.LOADING
}
}
fun bind(galleryID: Int) {
this.galleryID = galleryID
updateProgress(itemView.context)
val cache = Cache.getInstance(itemView.context, galleryID)
val galleryBlock = runBlocking {
cache.getGalleryBlock()
} ?: return
val resources = itemView.context.resources
val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
val artists = galleryBlock.artists
val series = galleryBlock.series
binding.galleryblockThumbnail.apply {
setOnClickListener {
itemView.performClick()
}
setOnLongClickListener {
itemView.performLongClick()
}
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
setImageLoaderCallback(object: ImageLoader.Callback {
override fun onFail(error: Exception?) {
Cache.getInstance(context, galleryID).let { cache ->
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
}
}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
}
}
binding.galleryblockTitle.text = galleryBlock.title
with(binding.galleryblockArtist) {
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch {
val gallery = runCatching {
getGallery(galleryID)
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
}
with(binding.galleryblockSeries) {
text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(binding.galleryblockLanguage) {
text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(binding.galleryblockTagGroup) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
}
}
tags.clear()
CoroutineScope(Dispatchers.IO).launch {
tags.addAll(
galleryBlock.relatedTags.sortedBy {
val tag = Tag.parse(it)
if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
launch(Dispatchers.Main) {
refresh()
}
}
}
binding.galleryblockId.text = galleryBlock.id.toString()
binding.galleryblockPagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getReader(galleryBlock.id).galleryInfo.files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(binding.galleryblockFavorite) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
}
else -> {
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
}
})
start()
})
}
}
}
}
// Make some views invisible to make it thinner
if (thin) {
binding.galleryblockTagGroup.visibility = View.GONE
}
}
}
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((Int) -> Unit)? = null
var onDeleteClickedHandler: ((Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) {
val galleryID = galleries[position]
holder.bind(galleryID)
holder.binding.galleryblockCard.binding.download.setOnClickListener {
onDownloadClickedHandler?.invoke(position)
}
holder.binding.galleryblockCard.binding.delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position)
}
mItemManger.bindView(holder.binding.root, position)
holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
holder.binding.galleryblockCard.binding.download.text =
if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID))
holder.binding.root.context.getString(android.R.string.cancel)
else
holder.binding.root.context.getString(R.string.main_download)
}
override fun onClose(layout: SwipeLayout?) {}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onOpen(layout: SwipeLayout?) {}
override fun onStartClose(layout: SwipeLayout?) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
})
}
}
override fun getItemCount() = galleries.size
override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout
}

View File

@@ -40,7 +40,7 @@ import com.github.piasy.biv.view.BigImageView
import com.github.piasy.biv.view.ImageShownCallback import com.github.piasy.biv.view.ImageShownCallback
import com.github.piasy.biv.view.ImageViewFactory import com.github.piasy.biv.view.ImageViewFactory
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding import xyz.quaver.pupil.databinding.ReaderItemBinding
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
@@ -50,9 +50,9 @@ import kotlin.math.roundToInt
class ReaderAdapter( class ReaderAdapter(
private val activity: ReaderActivity, private val activity: ReaderActivity,
private val galleryID: Int private val galleryID: String
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var reader: Reader? = null var reader: GalleryInfo? = null
var isFullScreen = false var isFullScreen = false
@@ -101,7 +101,7 @@ class ReaderAdapter(
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> { binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0 height = 0
dimensionRatio = dimensionRatio =
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}" "${reader!!.files[position].width}:${reader!!.files[position].height}"
} }
} else { } else {
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
@@ -158,7 +158,7 @@ class ReaderAdapter(
holder.bind(position) holder.bind(position)
} }
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0 override fun getItemCount() = reader?.files?.size ?: 0
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
holder.clear() holder.clear()

View File

@@ -0,0 +1,151 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.SearchResultItemBinding
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.wordCapitalize
class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((String) -> Unit)? = null
var onDeleteClickedHandler: ((String) -> Unit)? = null
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
var itemID: String = ""
var update = true
init {
CoroutineScope(Dispatchers.Main).launch {
while (update) {
updateProgress()
delay(1000)
}
}
binding.root.binding.download.setOnClickListener {
onDownloadClickedHandler?.invoke(itemID)
}
binding.root.binding.delete.setOnClickListener {
onDeleteClickedHandler?.invoke(itemID)
}
binding.idView.setOnClickListener {
(itemView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("item_id", itemID)
)
Toast.makeText(itemView.context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
binding.root.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
binding.root.binding.download.text =
if (DownloadManager.getInstance(itemView.context).isDownloading(itemID))
itemView.context.getString(android.R.string.cancel)
else
itemView.context.getString(R.string.main_download)
}
override fun onOpen(layout: SwipeLayout?) {}
override fun onStartClose(layout: SwipeLayout?) {}
override fun onClose(layout: SwipeLayout?) {}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
})
}
fun updateProgress() = CoroutineScope(Dispatchers.Main).launch {
with (itemView as ProgressCardView) {
val imageList = Cache.getInstance(context, itemID).metadata.imageList
if (imageList == null) {
max = 0
return@with
}
progress = imageList.count { it != null }
max = imageList.size
type = if (!imageList.contains(null)) {
val downloadManager = DownloadManager.getInstance(context)
if (downloadManager.getDownloadFolder(itemID) == null)
ProgressCardView.Type.CACHE
else
ProgressCardView.Type.DOWNLOAD
} else
ProgressCardView.Type.LOADING
}
}
fun bind(result: SearchResult) {
itemID = result.id
binding.thumbnail.ssiv?.recycle()
binding.thumbnail.showImage(Uri.parse(result.thumbnail))
updateProgress()
binding.title.text = result.title
binding.idView.text = result.id
binding.artist.text = result.artists.joinToString { it.wordCapitalize() }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
mItemManger.bindView(holder.itemView, position)
holder.bind(results[position])
}
override fun onViewDetachedFromWindow(holder: ViewHolder) {
holder.update = false
}
override fun getItemCount(): Int = results.size
override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout
}

View File

@@ -51,7 +51,7 @@ import kotlin.math.log10
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, val startId: Int? = null) data class Tag(val galleryID: String, val index: Int, val startId: Int? = null)
//region Notification //region Notification
private val notificationManager by lazy { private val notificationManager by lazy {
@@ -66,15 +66,15 @@ class DownloadService : Service() {
.setOngoing(true) .setOngoing(true)
} }
private val notification = ConcurrentHashMap<Int, NotificationCompat.Builder?>() private val notification = ConcurrentHashMap<String, NotificationCompat.Builder?>()
private fun initNotification(galleryID: Int) { private fun initNotification(galleryID: String) {
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
.putExtra("galleryID", galleryID) .putExtra("galleryID", galleryID)
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT) getPendingIntent(galleryID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
} }
val action = val action =
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel), NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
@@ -101,7 +101,7 @@ class DownloadService : Service() {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun notify(galleryID: Int) { private fun notify(galleryID: String) {
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0 val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
@@ -114,16 +114,16 @@ class DownloadService : Service() {
.setOngoing(false) .setOngoing(false)
.mActions.clear() .mActions.clear()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID.hashCode())
} else } else
notification notification
.setProgress(max, progress, false) .setProgress(max, progress, false)
.setContentText("$progress/$max") .setContentText("$progress/$max")
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority) if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
notification.let { notificationManager.notify(galleryID, it.build()) } notification.let { notificationManager.notify(galleryID.hashCode(), it.build()) }
else else
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID.hashCode())
} }
//endregion //endregion
@@ -194,10 +194,10 @@ class DownloadService : Service() {
* 0 <= value < 100 -> Download in progress * 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed * Float.POSITIVE_INFINITY -> Download completed
*/ */
val progress = ConcurrentHashMap<Int, MutableList<Float>>() val progress = ConcurrentHashMap<String, MutableList<Float>>()
var priority = 0 var priority = ""
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true fun isCompleted(galleryID: String) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
private val callback = object: Callback { private val callback = object: Callback {
@@ -266,7 +266,7 @@ class DownloadService : Service() {
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }
} }
fun cancel(galleryID: Int, startId: Int? = null) { fun cancel(galleryID: String, startId: Int? = null) {
client.dispatcher().queuedCalls().filter { client.dispatcher().queuedCalls().filter {
(it.request().tag() as? Tag)?.galleryID == galleryID (it.request().tag() as? Tag)?.galleryID == galleryID
}.forEach { }.forEach {
@@ -282,12 +282,12 @@ class DownloadService : Service() {
progress.remove(galleryID) progress.remove(galleryID)
notification.remove(galleryID) notification.remove(galleryID)
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID.hashCode())
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }
} }
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch { fun delete(galleryID: String, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
cancel(galleryID) cancel(galleryID)
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID) DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
Cache.delete(this@DownloadService, galleryID) Cache.delete(this@DownloadService, galleryID)
@@ -295,7 +295,7 @@ class DownloadService : Service() {
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }
} }
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch { fun download(galleryID: String, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID)) if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
return@launch return@launch
@@ -316,7 +316,7 @@ class DownloadService : Service() {
histories.add(galleryID) histories.add(galleryID)
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F } progress[galleryID] = MutableList(reader.files.size) { 0F }
cache.metadata.imageList?.let { cache.metadata.imageList?.let {
it.forEachIndexed { index, image -> it.forEachIndexed { index, image ->
@@ -329,15 +329,15 @@ class DownloadService : Service() {
.getDownloadFolder(galleryID) != null ) .getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload() Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID.hashCode())
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }
return@launch return@launch
} }
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30)) notification[galleryID]?.setContentTitle(reader.title?.ellipsize(30))
notify(galleryID) notify(galleryID)
val queued = mutableSetOf<Int>() val queued = mutableSetOf<String>()
if (priority) { if (priority) {
client.dispatcher().queuedCalls().forEach { client.dispatcher().queuedCalls().forEach {
@@ -372,7 +372,7 @@ class DownloadService : Service() {
ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras)) ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
} }
fun download(context: Context, galleryID: Int, priority: Boolean = false) { fun download(context: Context, galleryID: String, priority: Boolean = false) {
command(context) { command(context) {
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD) putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
putExtra(KEY_PRIORITY, priority) putExtra(KEY_PRIORITY, priority)
@@ -380,14 +380,14 @@ class DownloadService : Service() {
} }
} }
fun cancel(context: Context, galleryID: Int? = null) { fun cancel(context: Context, galleryID: String? = null) {
command(context) { command(context) {
putExtra(KEY_COMMAND, COMMAND_CANCEL) putExtra(KEY_COMMAND, COMMAND_CANCEL)
galleryID?.let { putExtra(KEY_ID, it) } galleryID?.let { putExtra(KEY_ID, it) }
} }
} }
fun delete(context: Context, galleryID: Int) { fun delete(context: Context, galleryID: String) {
command(context) { command(context) {
putExtra(KEY_COMMAND, COMMAND_DELETE) putExtra(KEY_COMMAND, COMMAND_DELETE)
putExtra(KEY_ID, galleryID) putExtra(KEY_ID, galleryID)
@@ -399,11 +399,11 @@ class DownloadService : Service() {
startForeground(R.id.downloader_notification_id, serviceNotification.build()) startForeground(R.id.downloader_notification_id, serviceNotification.build())
when (intent?.getStringExtra(KEY_COMMAND)) { when (intent?.getStringExtra(KEY_COMMAND)) {
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) COMMAND_DOWNLOAD -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty())
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId) download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
} }
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) } COMMAND_CANCEL -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) cancel(it, startId) else cancel(startId = startId) }
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) } COMMAND_DELETE -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) delete(it, startId) }
} }
return START_NOT_STICKY return START_NOT_STICKY

View File

@@ -0,0 +1,37 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.sources
import com.google.android.gms.vision.L
import kotlin.reflect.KClass
interface SearchResult {
val id: String
val title: String
val thumbnail: String
val artists: List<String>
}
// Might be better to use channel on Query_Result
interface Source<Query_SortMode: Enum<*>, Query_Result: SearchResult> {
val querySortModeClass: KClass<Query_SortMode>
val queryResultClass: KClass<Query_Result>
suspend fun query(query: String, range: IntRange, sortMode: Query_SortMode? = null) : Pair<List<Query_Result>, Int>
}

View File

@@ -0,0 +1,73 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.sources.hitomi
import kotlinx.coroutines.yield
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.pupil.sources.Source
import kotlin.math.min
import kotlin.math.max
class Hitomi : Source<Hitomi.SortMode, Hitomi.SearchResult> {
override val querySortModeClass = SortMode::class
override val queryResultClass = SearchResult::class
enum class SortMode {
NEWEST,
POPULAR
}
data class SearchResult(
override val id: String,
override val title: String,
override val thumbnail: String,
override val artists: List<String>,
) : xyz.quaver.pupil.sources.SearchResult
var cachedQuery: String? = null
val cache = mutableListOf<Int>()
override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<List<SearchResult>, Int> {
if (cachedQuery != query) {
cachedQuery = null
cache.clear()
yield()
doSearch(query, sortMode == SortMode.POPULAR).let {
yield()
cache.addAll(it)
}
cachedQuery = query
}
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
return Pair(cache.slice(sanitizedRange).map {
getGalleryBlock(it).let { gallery ->
SearchResult(
gallery.id.toString(),
gallery.title,
gallery.thumbnails.first(),
gallery.artists
)
}
}, cache.size)
}
}

View File

@@ -31,35 +31,33 @@ import android.view.animation.DecelerateInterpolator
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.* import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.view.MainView import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.view.ProgressCard import xyz.quaver.pupil.ui.view.SwipePageTurnView
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.checkUpdate import xyz.quaver.pupil.util.checkUpdate
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import java.util.regex.Pattern import java.util.regex.Pattern
@@ -69,20 +67,7 @@ class MainActivity :
BaseActivity(), BaseActivity(),
NavigationView.OnNavigationItemSelectedListener NavigationView.OnNavigationItemSelectedListener
{ {
private val searchResults = mutableListOf<SearchResult>()
enum class Mode {
SEARCH,
HISTORY,
DOWNLOAD,
FAVORITE
}
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Int>()
private var query = "" private var query = ""
set(value) { set(value) {
@@ -94,13 +79,13 @@ class MainActivity :
} }
private var queryStack = mutableListOf<String>() private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH @Suppress("UNCHECKED_CAST")
private var sortMode = SortMode.NEWEST private var source: Source<Enum<*>, SearchResult> = Hitomi() as Source<Enum<*>, SearchResult>
private var sortMode = Hitomi.SortMode.NEWEST
private var galleryIDs: Deferred<List<Int>>? = null private var searchJob: Deferred<Pair<List<SearchResult>, Int>>? = null
private var totalItems = 0 private var totalItems = 0
private var loadingJob: Job? = null private var currentPage = 1
private var currentPage = 0
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
@@ -136,35 +121,23 @@ class MainActivity :
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last() query = queryStack.last()
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
} }
} }
override fun onDestroy() {
super.onDestroy()
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) { if (currentPage > 1) {
runOnUiThread { runOnUiThread {
currentPage-- currentPage--
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
} }
@@ -175,10 +148,7 @@ class MainActivity :
runOnUiThread { runOnUiThread {
currentPage++ currentPage++
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
} }
@@ -229,18 +199,14 @@ class MainActivity :
setTitle(R.string.main_jump_title) setTitle(R.string.main_jump_title)
setMessage(getString( setMessage(getString(
R.string.main_jump_message, R.string.main_jump_message,
currentPage+1, currentPage,
ceil(totalItems / perPage.toDouble()).roundToInt() ceil(totalItems / perPage.toDouble()).roundToInt()
)) ))
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
runOnUiThread { query()
cancelFetch()
clearGalleries()
loadBlocks()
}
} }
}.show() }.show()
} }
@@ -251,23 +217,18 @@ class MainActivity :
setOnClickListener { setOnClickListener {
runBlocking { runBlocking {
withTimeoutOrNull(100) { withTimeoutOrNull(100) {
galleryIDs?.await() searchJob?.await()
} }
}.let { }.let {
if (it?.isEmpty() == false) { if (it?.first?.isEmpty() == false) {
val galleryID = it.random() val random = it.first.random()
GalleryDialog(this@MainActivity, galleryID).apply { GalleryDialog(this@MainActivity, random.id).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { query = it.toQuery()
query = it.toQuery() currentPage = 1
currentPage = 0
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
dismiss() dismiss()
} }
}.show() }.show()
@@ -288,19 +249,14 @@ class MainActivity :
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton val galleryID = editText.text.toString()
GalleryDialog(this@MainActivity, galleryID).apply { GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { query = it.toQuery()
query = it.toQuery() currentPage = 1
currentPage = 0
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
dismiss() dismiss()
} }
}.show() }.show()
@@ -309,8 +265,8 @@ class MainActivity :
} }
} }
with(binding.contents.view) { with(binding.contents.swipePageTurnView) {
setOnPageTurnListener(object: MainView.OnPageTurnListener { setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener {
override fun onPrev(page: Int) { override fun onPrev(page: Int) {
currentPage-- currentPage--
@@ -322,10 +278,7 @@ class MainActivity :
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.translationY(0F) .translationY(0F)
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
override fun onNext(page: Int) { override fun onNext(page: Int) {
@@ -339,95 +292,70 @@ class MainActivity :
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.translationY(0F) .translationY(0F)
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
}) })
} }
setupSearchBar() setupSearchBar()
setupRecyclerView() setupRecyclerView()
fetchGalleries(query, sortMode) query()
loadBlocks()
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(binding.contents.recyclerview) { with(binding.contents.recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply { adapter = SearchResultsAdapter(searchResults).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { query = it.toQuery()
query = it.toQuery() currentPage = 0
currentPage = 0
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
} }
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { id ->
val galleryID = galleries[position] if (DownloadManager.getInstance(context).isDownloading(id)) { //download in progress
DownloadService.cancel(this@MainActivity, id)
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID)
} }
else { else {
DownloadManager.getInstance(context).addDownloadFolder(galleryID) DownloadManager.getInstance(context).addDownloadFolder(id)
DownloadService.download(this@MainActivity, galleryID) DownloadService.download(this@MainActivity, id)
} }
closeAllItems() closeAllItems()
} }
onDeleteClickedHandler = { position -> onDeleteClickedHandler = { id ->
val galleryID = galleries[position] DownloadService.delete(this@MainActivity, id)
DownloadService.delete(this@MainActivity, galleryID)
histories.remove(galleryID) histories.remove(id)
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
closeAllItems() closeAllItems()
} }
} }
ItemClickSupport.addTo(this).apply { ItemClickSupport.addTo(this).apply {
onItemClickListener = listener@{ _, position, v -> onItemClickListener = listener@{ _, position, v ->
if (v !is ProgressCard) if (v !is ProgressCardView)
return@listener return@listener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
intent.putExtra("galleryID", galleries[position]) intent.putExtra("galleryID", searchResults[position].id)
//TODO: Maybe sprinkling some transitions will be nice :D //TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent) startActivity(intent)
} }
onItemLongClickListener = listener@{ _, position, v -> onItemLongClickListener = listener@{ _, position, v ->
if (v !is ProgressCard) if (v !is ProgressCardView)
return@listener false return@listener false
val galleryID = galleries.getOrNull(position) ?: return@listener true val result = searchResults.getOrNull(position) ?: return@listener true
GalleryDialog(this@MainActivity, galleryID).apply { GalleryDialog(this@MainActivity, result.id).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { query = it.toQuery()
query = it.toQuery() currentPage = 0
currentPage = 0
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
dismiss() dismiss()
} }
}.show() }.show()
@@ -458,7 +386,7 @@ class MainActivity :
with(binding.contents.searchview) { with(binding.contents.searchview) {
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (binding.contents.recyclerview.adapter as SearchResultsAdapter).closeAllItems()
} }
override fun onMenuClosed() { override fun onMenuClosed() {
@@ -532,13 +460,8 @@ class MainActivity :
override fun onFocusCleared() { override fun onFocusCleared() {
suggestionJob?.cancel() suggestionJob?.cancel()
runOnUiThread { currentPage = 1
cancelFetch() query()
clearGalleries()
currentPage = 0
fetchGalleries(query, sortMode)
loadBlocks()
}
} }
} }
@@ -550,43 +473,26 @@ class MainActivity :
when(item?.itemId) { when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.main_menu_thin -> { R.id.main_menu_thin -> {
val thin = !item.isChecked // TODO
item.isChecked = thin
binding.contents.recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
this.thin = thin
Preferences["thin"] = thin
}
adapter = adapter // Force to redraw
}
} }
R.id.main_menu_sort_newest -> { R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST sortMode = Hitomi.SortMode.NEWEST
item.isChecked = true item.isChecked = true
runOnUiThread { runOnUiThread {
currentPage = 0 currentPage = 1
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
} }
R.id.main_menu_sort_popular -> { R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR sortMode = Hitomi.SortMode.POPULAR
item.isChecked = true item.isChecked = true
runOnUiThread { runOnUiThread {
currentPage = 0 currentPage = 1
cancelFetch() query()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
} }
} }
} }
@@ -597,46 +503,6 @@ class MainActivity :
binding.drawer.closeDrawers() binding.drawer.closeDrawers()
when(item.itemId) { when(item.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.SEARCH
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.HISTORY
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.FAVORITE
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
} }
@@ -659,161 +525,47 @@ class MainActivity :
} }
private fun cancelFetch() { private fun cancelFetch() {
galleryIDs?.cancel() searchJob?.cancel()
loadingJob?.cancel()
} }
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch { private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
galleries.clear() searchResults.clear()
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) { binding.contents.recyclerview.adapter?.notifyDataSetChanged()
this ?: return@with
this.notifyDataSetChanged()
}
binding.contents.noresult.visibility = View.INVISIBLE binding.contents.noresult.visibility = View.INVISIBLE
binding.contents.progressbar.show() binding.contents.progressbar.show()
} }
private fun query() {
private fun fetchGalleries(query: String, sortMode: SortMode) {
val defaultQuery: String = Preferences["default_query"]
if (query.isNotBlank())
searchHistory.add(query)
if (query != queryStack.lastOrNull()) {
queryStack.remove(query)
queryStack.add(query)
}
if (query.isNotEmpty() && mode != Mode.SEARCH) {
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
setAction(android.R.string.ok) {
cancelFetch()
clearGalleries()
currentPage = 0
mode = Mode.SEARCH
queryStack.clear()
fetchGalleries(query, sortMode)
loadBlocks()
}
}.show()
}
galleryIDs = null
if (galleryIDs?.isActive == true)
return
galleryIDs = CoroutineScope(Dispatchers.IO).async {
when(mode) {
Mode.SEARCH -> {
when {
query.isEmpty() and defaultQuery.isEmpty() -> {
when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all")
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
}
}
Mode.HISTORY -> {
when {
query.isEmpty() -> {
histories.reversed().also {
totalItems = it.size
}
}
else -> {
val result = doSearch(query).sorted()
histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.DOWNLOAD -> {
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
when {
query.isEmpty() -> downloads.reversed().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
Mode.FAVORITE -> {
when {
query.isEmpty() -> favorites.reversed().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
}
}
}.toList()
}
}
private fun loadBlocks() {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
loadingJob = CoroutineScope(Dispatchers.IO).launch { cancelFetch()
val galleryIDs = try { clearGalleries()
galleryIDs!!.await().also {
if (it.isEmpty()) CoroutineScope(Dispatchers.Main).launch {
throw Exception("No result") searchJob = async(Dispatchers.IO) {
source.query(
query + Preferences["default_query", ""],
(currentPage - 1) * perPage until currentPage * perPage,
sortMode
)
}.also {
val results: List<SearchResult>
it.await().let { r ->
results = r.first
totalItems = r.second
} }
} catch (e: Exception) {
if (e.message != "No result") binding.contents.progressbar.hide()
FirebaseCrashlytics.getInstance().recordException(e) binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
withContext(Dispatchers.Main) { if (results.isEmpty()) {
binding.contents.noresult.visibility = View.VISIBLE binding.contents.noresult.visibility = View.VISIBLE
binding.contents.progressbar.hide() } else {
searchResults.addAll(results)
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
} }
return@launch
}
launch(Dispatchers.Main) {
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
}
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
for (chunk in chunks)
chunk.map { galleryID ->
async {
Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
galleryID
}
}
}.forEach {
it.await()?.also {
withContext(Dispatchers.Main) {
binding.contents.progressbar.hide()
galleries.add(it)
binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
}
}
}
} }
} }
} }

View File

@@ -49,7 +49,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
@@ -65,7 +64,7 @@ import xyz.quaver.pupil.util.startCamera
class ReaderActivity : BaseActivity() { class ReaderActivity : BaseActivity() {
private var galleryID = 0 private var galleryID = ""
private var currentPage = 0 private var currentPage = 0
private var isScroll = true private var isScroll = true
@@ -81,7 +80,7 @@ class ReaderActivity : BaseActivity() {
private val conn = object: ServiceConnection { private val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloader = (service as DownloadService.Binder).service.also { downloader = (service as DownloadService.Binder).service.also {
it.priority = 0 it.priority = ""
if (!it.progress.containsKey(galleryID)) if (!it.progress.containsKey(galleryID))
DownloadService.download(this@ReaderActivity, galleryID, true) DownloadService.download(this@ReaderActivity, galleryID, true)
@@ -130,7 +129,7 @@ class ReaderActivity : BaseActivity() {
cache = Cache.getInstance(this, galleryID) cache = Cache.getInstance(this, galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID) FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
if (galleryID == 0) { if (galleryID.isEmpty()) {
onBackPressed() onBackPressed()
return return
} }
@@ -151,14 +150,14 @@ class ReaderActivity : BaseActivity() {
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
galleryID = when (uri.host) { galleryID = when (uri.host) {
"hitomi.la" -> "hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0 Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
"hiyobi.me" -> lastPathSegment.toInt() "hiyobi.me" -> lastPathSegment
"e-hentai.org" -> uri.pathSegments[1].toInt() "e-hentai.org" -> uri.pathSegments[1]
else -> 0 else -> ""
} }
} }
} else { } else {
galleryID = intent.getIntExtra("galleryID", 0) galleryID = intent.getStringExtra("galleryID") ?: ""
} }
} }
@@ -184,7 +183,7 @@ class ReaderActivity : BaseActivity() {
with(binding.numberPicker) { with(binding.numberPicker) {
minValue = 1 minValue = 1
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0 maxValue = cache.metadata.reader?.files?.size ?: 0
value = currentPage value = currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -307,17 +306,19 @@ class ReaderActivity : BaseActivity() {
notifyDataSetChanged() notifyDataSetChanged()
} }
title = reader.galleryInfo.title title = reader.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = menu?.findItem(R.id.reader_menu_page_indicator)?.title =
"$currentPage/${reader.galleryInfo.files.size}" "$currentPage/${reader.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable( menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(
this@ReaderActivity, this@ReaderActivity,
R.drawable.hitomi
/*
when (reader.code) { when (reader.code) {
Code.HITOMI -> R.drawable.hitomi Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent else -> android.R.color.transparent
} }*/
) )
} }
} }

View File

@@ -55,7 +55,7 @@ class DownloadFolderNameDialogFragment : DialogFragment() {
} }
private fun initView() { private fun initView() {
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) } val galleryID = Cache.instances.let { if (it.size == 0) "1199708" else it.keys.elementAt((0 until it.size).random()) }
val galleryBlock = runBlocking { val galleryBlock = runBlocking {
Cache.getInstance(requireContext(), galleryID).getGalleryBlock() Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
} }

View File

@@ -39,7 +39,6 @@ import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.migrate
import java.io.File import java.io.File
class DownloadLocationDialogFragment : DialogFragment() { class DownloadLocationDialogFragment : DialogFragment() {
@@ -181,8 +180,6 @@ class DownloadLocationDialogFragment : DialogFragment() {
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ -> setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: "" Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
DownloadManager.getInstance(requireContext()).migrate()
} }
isCancelable = false isCancelable = false

View File

@@ -38,10 +38,12 @@ import kotlinx.coroutines.withContext
import xyz.quaver.hitomi.Gallery import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.getGallery import xyz.quaver.hitomi.getGallery
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.databinding.* import xyz.quaver.pupil.databinding.*
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.sources.SearchResult
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.ui.view.TagChip import xyz.quaver.pupil.ui.view.TagChip
@@ -51,7 +53,7 @@ import xyz.quaver.pupil.util.wordCapitalize
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) { class GalleryDialog(context: Context, private val galleryID: String) : AlertDialog(context) {
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
@@ -80,7 +82,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val gallery = getGallery(galleryID) val gallery = getGallery(galleryID.toInt())
launch (Dispatchers.Main) { launch (Dispatchers.Main) {
binding.progressbar.visibility = View.GONE binding.progressbar.visibility = View.GONE
@@ -203,9 +205,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
private fun addRelated(gallery: Gallery) { private fun addRelated(gallery: Gallery) {
val galleries = ArrayList<Int>() val galleries = mutableListOf<SearchResult>()
val adapter = GalleryBlockAdapter(galleries).apply { val adapter = SearchResultsAdapter(galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler -> this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -223,11 +225,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
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]) putExtra("galleryID", galleries[position].id)
}) })
} }
onItemLongClickListener = { _, position, _ -> onItemLongClickListener = { _, position, _ ->
GalleryDialog(context, galleries[position]).apply { GalleryDialog(context, galleries[position].id).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) } this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
} }
@@ -240,13 +242,20 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID -> gallery.related.forEach { galleryID ->
Cache.getInstance(context, galleryID).getGalleryBlock()?.let { Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let {
galleries.add(galleryID) galleries.add(
Hitomi.SearchResult(
it.id.toString(),
it.title,
it.thumbnails.first(),
it.artists
)
)
} }
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
}
} }
} }
} }

View File

@@ -11,7 +11,7 @@ import androidx.core.graphics.drawable.DrawableCompat
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ProgressCardViewBinding import xyz.quaver.pupil.databinding.ProgressCardViewBinding
class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) { class ProgressCardView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
enum class Type { enum class Type {
LOADING, LOADING,

View File

@@ -35,7 +35,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.view.NestedScrollingChild; import androidx.core.view.NestedScrollingChild;
@@ -48,7 +47,7 @@ import androidx.core.widget.TextViewCompat;
import xyz.quaver.pupil.R; import xyz.quaver.pupil.R;
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent { public class SwipePageTurnView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent {
private static final int PAGE_TURN_LAYOUT_SIZE = 48; private static final int PAGE_TURN_LAYOUT_SIZE = 48;
private static final int PAGE_TURN_ANIM_DURATION = 500; private static final int PAGE_TURN_ANIM_DURATION = 500;
@@ -84,15 +83,15 @@ public class MainView extends ViewGroup implements NestedScrollingChild, NestedS
private OnPageTurnListener mOnPageTurnListener; private OnPageTurnListener mOnPageTurnListener;
public MainView(@NonNull Context context) { public SwipePageTurnView(@NonNull Context context) {
this(context, null); this(context, null);
} }
public MainView(@NonNull Context context, AttributeSet attr) { public SwipePageTurnView(@NonNull Context context, AttributeSet attr) {
this(context, attr, 0); this(context, attr, 0);
} }
public MainView(@NonNull Context context, AttributeSet attr, int defStyle) { public SwipePageTurnView(@NonNull Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle); super(context, attr, defStyle);
setWillNotDraw(false); setWillNotDraw(false);

View File

@@ -32,9 +32,8 @@ 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.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.* import xyz.quaver.io.util.*
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
@@ -46,24 +45,24 @@ import java.util.concurrent.ConcurrentHashMap
@Serializable @Serializable
data class Metadata( data class Metadata(
var galleryBlock: GalleryBlock? = null, var galleryBlock: GalleryBlock? = null,
var reader: Reader? = null, var reader: GalleryInfo? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
fun copy(): Metadata = Metadata(galleryBlock, 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: String) : ContextWrapper(context) {
companion object { companion object {
val instances = ConcurrentHashMap<Int, Cache>() val instances = ConcurrentHashMap<String, Cache>()
fun getInstance(context: Context, galleryID: Int) = fun getInstance(context: Context, galleryID: String) =
instances[galleryID] ?: synchronized(this) { instances[galleryID] ?: synchronized(this) {
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) } instances[galleryID] ?: Cache(context, galleryID).also { instances[galleryID] = it }
} }
@Synchronized @Synchronized
fun delete(context: Context, galleryID: Int) { fun delete(context: Context, galleryID: String) {
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively() File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
instances.remove(galleryID) instances.remove(galleryID)
} }
@@ -111,8 +110,8 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
suspend fun getGalleryBlock(): GalleryBlock? { suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf( val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) }, { xyz.quaver.hitomi.getGalleryBlock(galleryID.toInt()) }
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) } // { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
) )
return metadata.galleryBlock return metadata.galleryBlock
@@ -154,22 +153,17 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}.getOrNull()?.uri } }.getOrNull()?.uri }
} } ?: Uri.EMPTY } } ?: Uri.EMPTY
suspend fun getReader(): Reader? { suspend fun getReader(): GalleryInfo? {
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') } val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
val sources = mapOf( val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) }, "hitomi" to { xyz.quaver.hitomi.getGalleryInfo(galleryID.toInt()) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) } //Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).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: GalleryInfo? = null
for (source in sources) { for (source in sources) {
reader = try { reader = try {
@@ -187,7 +181,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.reader = it metadata.reader = it
if (metadata.imageList == null) if (metadata.imageList == null)
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null } metadata.imageList = MutableList(reader.files.size) { null }
} }
} }
} }
@@ -206,7 +200,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
setMetadata { metadata -> metadata.imageList!![index] = fileName } setMetadata { metadata -> metadata.imageList!![index] = fileName }
} }
private val lock = ConcurrentHashMap<Int, Mutex>() private val lock = ConcurrentHashMap<String, Mutex>()
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch { fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
val downloadFolder = downloadFolder ?: return@launch val downloadFolder = downloadFolder ?: return@launch

View File

@@ -57,8 +57,8 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
}.invoke() }.invoke()
private var prevDownloadFolder: FileX? = null private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap<Int, String>? = null private var downloadFolderMapInstance: MutableMap<String, String>? = null
val downloadFolderMap: MutableMap<Int, String> val downloadFolderMap: MutableMap<String, String>
@Synchronized @Synchronized
get() { get() {
if (prevDownloadFolder != downloadFolder) { if (prevDownloadFolder != downloadFolder) {
@@ -68,14 +68,14 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val data = if (file.exists()) val data = if (file.exists())
kotlin.runCatching { kotlin.runCatching {
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) } file.readText()?.let { Json.decodeFromString<MutableMap<String, String>>(it) }
}.onFailure { file.delete() }.getOrNull() }.onFailure { file.delete() }.getOrNull()
else else
null null
data ?: { data ?: {
file.createNewFile() file.createNewFile()
mutableMapOf<Int, String>() mutableMapOf<String, String>()
}.invoke() }.invoke()
}.invoke() }.invoke()
} }
@@ -85,7 +85,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
@Synchronized @Synchronized
fun isDownloading(galleryID: Int): Boolean { fun isDownloading(galleryID: String): Boolean {
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
return downloadFolderMap.containsKey(galleryID) return downloadFolderMap.containsKey(galleryID)
@@ -93,11 +93,11 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
} }
@Synchronized @Synchronized
fun getDownloadFolder(galleryID: Int): FileX? = fun getDownloadFolder(galleryID: String): FileX? =
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) } downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
@Synchronized @Synchronized
fun addDownloadFolder(galleryID: Int) { fun addDownloadFolder(galleryID: String) {
val name = runBlocking { val name = runBlocking {
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock() Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
}?.formatDownloadFolder() ?: return }?.formatDownloadFolder() ?: return
@@ -116,7 +116,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
} }
@Synchronized @Synchronized
fun deleteDownloadFolder(galleryID: Int) { fun deleteDownloadFolder(galleryID: String) {
downloadFolderMap[galleryID]?.let { downloadFolderMap[galleryID]?.let {
kotlin.runCatching { kotlin.runCatching {
downloadFolder.getChild(it).deleteRecursively() downloadFolder.getChild(it).deleteRecursively()

View File

@@ -26,9 +26,8 @@ import androidx.core.content.ContextCompat
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
@@ -103,11 +102,17 @@ fun GalleryBlock.formatDownloadFolderTest(format: String): String =
} }
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127) }.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
val Reader.requestBuilders: List<Request.Builder> val GalleryInfo.requestBuilders: List<Request.Builder>
get() { get() {
val galleryID = this.galleryInfo.id ?: 0 val galleryID = this.id ?: 0
val lowQuality = Preferences["low_quality", true] val lowQuality = Preferences["low_quality", true]
return this.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
}
/*
return when(code) { return when(code) {
Code.HITOMI -> { Code.HITOMI -> {
this.galleryInfo.files.map { this.galleryInfo.files.map {
@@ -122,9 +127,8 @@ val Reader.requestBuilders: List<Request.Builder>
.url(it.path) .url(it.path)
} }
} }
} }*/
} }
fun String.ellipsize(n: Int): String = fun String.ellipsize(n: Int): String =
if (this.length > n) if (this.length > n)
this.slice(0 until n) + "" this.slice(0 until n) + ""

View File

@@ -46,7 +46,6 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGalleryBlock import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader import xyz.quaver.hitomi.getReader
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
@@ -196,7 +195,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) { fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<String>) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -214,7 +213,7 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let { Json.decodeFromString<List<String>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
favorites.addAll(it) favorites.addAll(it)
onSuccess?.invoke(it) onSuccess?.invoke(it)
} }
@@ -238,110 +237,3 @@ private val receiver = object: BroadcastReceiver() {
} }
} }
} }
@SuppressLint("RestrictedApi")
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
val notificationManager = NotificationManagerCompat.from(this)
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
).build()
val notification = NotificationCompat.Builder(this, "import")
.setContentTitle(getText(R.string.import_old_galleries_notification))
.setProgress(0, 0, true)
.addAction(action)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
DownloadService.cancel(this)
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
val images = listOf(
"jpg",
"png",
"gif",
"webp"
)
val downloadFolders = downloadFolder.listFiles { folder ->
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
}?.map {
if (it !is FileX)
FileX(this@migrate, it)
else
it
}
if (downloadFolders.isNullOrEmpty()) return@launch
downloadFolders.forEachIndexed { index, folder ->
notification
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
.setProgress(index, downloadFolders.size, false)
notificationManager.notify(R.id.notification_id_import, notification.build())
val metadata = kotlin.runCatching {
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it) }
}.getOrNull()
val galleryID = metadata?.get("reader")?.get("galleryInfo")?.get("id")?.content?.toIntOrNull()
?: folder.name.toIntOrNull() ?: return@forEachIndexed
val galleryBlock: GalleryBlock? = kotlin.runCatching {
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
}.getOrNull() ?: kotlin.runCatching {
getGalleryBlock(galleryID)
}.getOrNull() ?: kotlin.runCatching {
xyz.quaver.hiyobi.getGalleryBlock(galleryID)
}.getOrNull()
val reader: Reader? = kotlin.runCatching {
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
}.getOrNull() ?: kotlin.runCatching {
getReader(galleryID)
}.getOrNull() ?: kotlin.runCatching {
xyz.quaver.hiyobi.getReader(galleryID)
}.getOrNull()
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
val file = folder.getChild(".thumbnail").also {
if (it.exists())
it.delete()
it.createNewFile()
}
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
}
val list: MutableList<String?> =
MutableList(reader!!.galleryInfo.files.size) { null }
folder.list { _, name ->
name?.substringAfterLast('.') in images
}?.sorted()?.take(list.size)?.forEachIndexed { i, name ->
list[i] = name
}
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
Json.encodeToString(Metadata(galleryBlock, reader, list))
)
Cache.delete(this@migrate, galleryID)
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
}
notification
.setContentText(getText(R.string.import_old_galleries_notification_done))
.setProgress(0, 0, false)
.setOngoing(false)
.mActions.clear()
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
unregisterReceiver(receiver)
}
}
}

View File

@@ -24,8 +24,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.MainActivity"> tools:context=".ui.MainActivity">
<xyz.quaver.pupil.ui.view.MainView <xyz.quaver.pupil.ui.view.SwipePageTurnView
android:id="@+id/view" android:id="@+id/swipe_page_turn_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -52,7 +52,7 @@
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller> </com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView> </xyz.quaver.pupil.ui.view.SwipePageTurnView>
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"

View File

@@ -17,11 +17,10 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>. ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<xyz.quaver.pupil.ui.view.ProgressCard <xyz.quaver.pupil.ui.view.ProgressCardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/galleryblock_card"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="true" android:clipChildren="true"
@@ -36,12 +35,13 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.github.piasy.biv.view.BigImageView <com.github.piasy.biv.view.BigImageView
android:id="@+id/galleryblock_thumbnail" android:id="@+id/thumbnail"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description" android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:clickable="false" android:clickable="false"
android:duplicateParentState="true"
app:layout_constraintHeight_default="spread" app:layout_constraintHeight_default="spread"
app:layout_constraintHeight_min="200dp" app:layout_constraintHeight_min="200dp"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@@ -50,61 +50,61 @@
<TextView <TextView
style="@style/TextAppearance.AppCompat.Headline" style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title" android:id="@+id/title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
style="@style/TextAppearance.AppCompat.Medium" style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist" android:id="@+id/artist"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title" /> app:layout_constraintTop_toBottomOf="@id/title" />
<TextView <TextView
android:id="@+id/galleryblock_series" android:id="@+id/series"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist" app:layout_constraintTop_toBottomOf="@id/artist"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/thumbnail"
app:layout_constraintRight_toRightOf="parent"/> app:layout_constraintRight_toRightOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_type" android:id="@+id/type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series" app:layout_constraintTop_toBottomOf="@id/series"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" /> app:layout_constraintLeft_toRightOf="@id/thumbnail" />
<TextView <TextView
android:id="@+id/galleryblock_language" android:id="@+id/language"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type" app:layout_constraintTop_toBottomOf="@id/type"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" /> app:layout_constraintLeft_toRightOf="@id/thumbnail" />
<xyz.quaver.pupil.ui.view.TagChipGroup <xyz.quaver.pupil.ui.view.TagChipGroup
android:id="@+id/galleryblock_tag_group" android:id="@+id/tag_group"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:chipSpacing="4dp" app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language" app:layout_constraintTop_toBottomOf="@id/language"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/thumbnail"
app:layout_constraintRight_toRightOf="parent"/> app:layout_constraintRight_toRightOf="parent"/>
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
@@ -112,7 +112,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:constraint_referenced_ids="galleryblock_thumbnail, galleryblock_tag_group"/> app:constraint_referenced_ids="thumbnail, tag_group"/>
<View <View
android:id="@+id/divider" android:id="@+id/divider"
@@ -123,7 +123,7 @@
android:layout_margin="8dp"/> android:layout_margin="8dp"/>
<TextView <TextView
android:id="@+id/galleryblock_id" android:id="@+id/id_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@@ -132,7 +132,7 @@
app:layout_constraintLeft_toLeftOf="parent"/> app:layout_constraintLeft_toLeftOf="parent"/>
<TextView <TextView
android:id="@+id/galleryblock_pagecount" android:id="@+id/pagecount"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@@ -143,7 +143,7 @@
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
<ImageView <ImageView
android:id="@+id/galleryblock_favorite" android:id="@+id/favorite"
android:contentDescription="@string/app_name" android:contentDescription="@string/app_name"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
@@ -157,4 +157,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</xyz.quaver.pupil.ui.view.ProgressCard> </xyz.quaver.pupil.ui.view.ProgressCardView>