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 "xyz.quaver:libpupil:1.8.16"
implementation "xyz.quaver:libpupil:1.9.0"
implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.0.7"

View File

@@ -51,9 +51,9 @@ import kotlin.reflect.KClass
typealias PupilInterceptor = (Interceptor.Chain) -> Response
lateinit var histories: SavedSet<Int>
lateinit var histories: SavedSet<String>
private set
lateinit var favorites: SavedSet<Int>
lateinit var favorites: SavedSet<String>
private set
lateinit var favoriteTags: SavedSet<Tag>
private set
@@ -108,8 +108,6 @@ class Pupil : Application() {
if (!FileX(this, it).canWrite())
throw Exception()
DownloadManager.getInstance(this).migrate()
}
} catch (e: Exception) {
Preferences.remove("download_folder")
@@ -120,8 +118,8 @@ class Pupil : Application() {
Preferences["reset_secure"] = true
}
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), "")
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), "")
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
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.ImageViewFactory
import kotlinx.coroutines.*
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding
import xyz.quaver.pupil.ui.ReaderActivity
@@ -50,9 +50,9 @@ import kotlin.math.roundToInt
class ReaderAdapter(
private val activity: ReaderActivity,
private val galleryID: Int
private val galleryID: String
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var reader: Reader? = null
var reader: GalleryInfo? = null
var isFullScreen = false
@@ -101,7 +101,7 @@ class ReaderAdapter(
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0
dimensionRatio =
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
"${reader!!.files[position].width}:${reader!!.files[position].height}"
}
} else {
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
@@ -158,7 +158,7 @@ class ReaderAdapter(
holder.bind(position)
}
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
override fun getItemCount() = reader?.files?.size ?: 0
override fun onViewRecycled(holder: ViewHolder) {
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
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
private val notificationManager by lazy {
@@ -66,15 +66,15 @@ class DownloadService : Service() {
.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)
.putExtra("galleryID", galleryID)
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
getPendingIntent(galleryID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
}
val action =
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
@@ -101,7 +101,7 @@ class DownloadService : Service() {
}
@SuppressLint("RestrictedApi")
private fun notify(galleryID: Int) {
private fun notify(galleryID: String) {
val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
@@ -114,16 +114,16 @@ class DownloadService : Service() {
.setOngoing(false)
.mActions.clear()
notificationManager.cancel(galleryID)
notificationManager.cancel(galleryID.hashCode())
} else
notification
.setProgress(max, progress, false)
.setContentText("$progress/$max")
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
notificationManager.cancel(galleryID)
notificationManager.cancel(galleryID.hashCode())
}
//endregion
@@ -194,10 +194,10 @@ class DownloadService : Service() {
* 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed
*/
val progress = ConcurrentHashMap<Int, MutableList<Float>>()
var priority = 0
val progress = ConcurrentHashMap<String, MutableList<Float>>()
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 {
@@ -266,7 +266,7 @@ class DownloadService : Service() {
startId?.let { stopSelf(it) }
}
fun cancel(galleryID: Int, startId: Int? = null) {
fun cancel(galleryID: String, startId: Int? = null) {
client.dispatcher().queuedCalls().filter {
(it.request().tag() as? Tag)?.galleryID == galleryID
}.forEach {
@@ -282,12 +282,12 @@ class DownloadService : Service() {
progress.remove(galleryID)
notification.remove(galleryID)
notificationManager.cancel(galleryID)
notificationManager.cancel(galleryID.hashCode())
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)
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
Cache.delete(this@DownloadService, galleryID)
@@ -295,7 +295,7 @@ class DownloadService : Service() {
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))
return@launch
@@ -316,7 +316,7 @@ class DownloadService : Service() {
histories.add(galleryID)
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
progress[galleryID] = MutableList(reader.files.size) { 0F }
cache.metadata.imageList?.let {
it.forEachIndexed { index, image ->
@@ -329,15 +329,15 @@ class DownloadService : Service() {
.getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID)
notificationManager.cancel(galleryID.hashCode())
startId?.let { stopSelf(it) }
return@launch
}
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
notification[galleryID]?.setContentTitle(reader.title?.ellipsize(30))
notify(galleryID)
val queued = mutableSetOf<Int>()
val queued = mutableSetOf<String>()
if (priority) {
client.dispatcher().queuedCalls().forEach {
@@ -372,7 +372,7 @@ class DownloadService : Service() {
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) {
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
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) {
putExtra(KEY_COMMAND, COMMAND_CANCEL)
galleryID?.let { putExtra(KEY_ID, it) }
}
}
fun delete(context: Context, galleryID: Int) {
fun delete(context: Context, galleryID: String) {
command(context) {
putExtra(KEY_COMMAND, COMMAND_DELETE)
putExtra(KEY_ID, galleryID)
@@ -399,11 +399,11 @@ class DownloadService : Service() {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
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)
}
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) }
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
COMMAND_CANCEL -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) cancel(it, startId) else cancel(startId = startId) }
COMMAND_DELETE -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) delete(it, startId) }
}
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.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.cardview.widget.CardView
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView
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.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding
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.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.view.MainView
import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.view.SwipePageTurnView
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences
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.restore
import java.util.regex.Pattern
@@ -69,20 +67,7 @@ class MainActivity :
BaseActivity(),
NavigationView.OnNavigationItemSelectedListener
{
enum class Mode {
SEARCH,
HISTORY,
DOWNLOAD,
FAVORITE
}
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Int>()
private val searchResults = mutableListOf<SearchResult>()
private var query = ""
set(value) {
@@ -94,13 +79,13 @@ class MainActivity :
}
private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
@Suppress("UNCHECKED_CAST")
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 loadingJob: Job? = null
private var currentPage = 0
private var currentPage = 1
private lateinit var binding: MainActivityBinding
@@ -136,35 +121,23 @@ class MainActivity :
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last()
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
else -> super.onBackPressed()
}
}
override fun onDestroy() {
super.onDestroy()
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) {
if (currentPage > 1) {
runOnUiThread {
currentPage--
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
}
@@ -175,10 +148,7 @@ class MainActivity :
runOnUiThread {
currentPage++
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
}
@@ -229,18 +199,14 @@ class MainActivity :
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
currentPage,
ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
runOnUiThread {
cancelFetch()
clearGalleries()
loadBlocks()
}
query()
}
}.show()
}
@@ -251,23 +217,18 @@ class MainActivity :
setOnClickListener {
runBlocking {
withTimeoutOrNull(100) {
galleryIDs?.await()
searchJob?.await()
}
}.let {
if (it?.isEmpty() == false) {
val galleryID = it.random()
if (it?.first?.isEmpty() == false) {
val random = it.first.random()
GalleryDialog(this@MainActivity, galleryID).apply {
GalleryDialog(this@MainActivity, random.id).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
currentPage = 0
query = it.toQuery()
currentPage = 1
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
query()
dismiss()
}
}.show()
@@ -288,19 +249,14 @@ class MainActivity :
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
val galleryID = editText.text.toString()
GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
currentPage = 0
query = it.toQuery()
currentPage = 1
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
query()
dismiss()
}
}.show()
@@ -309,8 +265,8 @@ class MainActivity :
}
}
with(binding.contents.view) {
setOnPageTurnListener(object: MainView.OnPageTurnListener {
with(binding.contents.swipePageTurnView) {
setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener {
override fun onPrev(page: Int) {
currentPage--
@@ -322,10 +278,7 @@ class MainActivity :
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
override fun onNext(page: Int) {
@@ -339,95 +292,70 @@ class MainActivity :
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
})
}
setupSearchBar()
setupRecyclerView()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
@SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() {
with(binding.contents.recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply {
adapter = SearchResultsAdapter(searchResults).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
currentPage = 0
query = it.toQuery()
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
query()
}
onDownloadClickedHandler = { position ->
val galleryID = galleries[position]
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID)
onDownloadClickedHandler = { id ->
if (DownloadManager.getInstance(context).isDownloading(id)) { //download in progress
DownloadService.cancel(this@MainActivity, id)
}
else {
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID)
DownloadManager.getInstance(context).addDownloadFolder(id)
DownloadService.download(this@MainActivity, id)
}
closeAllItems()
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position]
DownloadService.delete(this@MainActivity, galleryID)
onDeleteClickedHandler = { id ->
DownloadService.delete(this@MainActivity, id)
histories.remove(galleryID)
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
histories.remove(id)
closeAllItems()
}
}
ItemClickSupport.addTo(this).apply {
onItemClickListener = listener@{ _, position, v ->
if (v !is ProgressCard)
if (v !is ProgressCardView)
return@listener
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
startActivity(intent)
}
onItemLongClickListener = listener@{ _, position, v ->
if (v !is ProgressCard)
if (v !is ProgressCardView)
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 {
runOnUiThread {
query = it.toQuery()
currentPage = 0
query = it.toQuery()
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
query()
dismiss()
}
}.show()
@@ -458,7 +386,7 @@ class MainActivity :
with(binding.contents.searchview) {
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() {
(binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
(binding.contents.recyclerview.adapter as SearchResultsAdapter).closeAllItems()
}
override fun onMenuClosed() {
@@ -532,13 +460,8 @@ class MainActivity :
override fun onFocusCleared() {
suggestionJob?.cancel()
runOnUiThread {
cancelFetch()
clearGalleries()
currentPage = 0
fetchGalleries(query, sortMode)
loadBlocks()
}
currentPage = 1
query()
}
}
@@ -550,43 +473,26 @@ class MainActivity :
when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.main_menu_thin -> {
val thin = !item.isChecked
item.isChecked = thin
binding.contents.recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
this.thin = thin
Preferences["thin"] = thin
}
adapter = adapter // Force to redraw
}
// TODO
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
sortMode = Hitomi.SortMode.NEWEST
item.isChecked = true
runOnUiThread {
currentPage = 0
currentPage = 1
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
sortMode = Hitomi.SortMode.POPULAR
item.isChecked = true
runOnUiThread {
currentPage = 0
currentPage = 1
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
query()
}
}
}
@@ -597,46 +503,6 @@ class MainActivity :
binding.drawer.closeDrawers()
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 -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
@@ -659,161 +525,47 @@ class MainActivity :
}
private fun cancelFetch() {
galleryIDs?.cancel()
loadingJob?.cancel()
searchJob?.cancel()
}
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
galleries.clear()
searchResults.clear()
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) {
this ?: return@with
this.notifyDataSetChanged()
}
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
binding.contents.noresult.visibility = View.INVISIBLE
binding.contents.progressbar.show()
}
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() {
private fun query() {
val perPage = Preferences["per_page", "25"].toInt()
loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = try {
galleryIDs!!.await().also {
if (it.isEmpty())
throw Exception("No result")
cancelFetch()
clearGalleries()
CoroutineScope(Dispatchers.Main).launch {
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")
FirebaseCrashlytics.getInstance().recordException(e)
binding.contents.progressbar.hide()
binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
withContext(Dispatchers.Main) {
if (results.isEmpty()) {
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.delay
import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
@@ -65,7 +64,7 @@ import xyz.quaver.pupil.util.startCamera
class ReaderActivity : BaseActivity() {
private var galleryID = 0
private var galleryID = ""
private var currentPage = 0
private var isScroll = true
@@ -81,7 +80,7 @@ class ReaderActivity : BaseActivity() {
private val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloader = (service as DownloadService.Binder).service.also {
it.priority = 0
it.priority = ""
if (!it.progress.containsKey(galleryID))
DownloadService.download(this@ReaderActivity, galleryID, true)
@@ -130,7 +129,7 @@ class ReaderActivity : BaseActivity() {
cache = Cache.getInstance(this, galleryID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
if (galleryID == 0) {
if (galleryID.isEmpty()) {
onBackPressed()
return
}
@@ -151,14 +150,14 @@ class ReaderActivity : BaseActivity() {
if (uri != null && lastPathSegment != null) {
galleryID = when (uri.host) {
"hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
"hiyobi.me" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt()
else -> 0
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
"hiyobi.me" -> lastPathSegment
"e-hentai.org" -> uri.pathSegments[1]
else -> ""
}
}
} else {
galleryID = intent.getIntExtra("galleryID", 0)
galleryID = intent.getStringExtra("galleryID") ?: ""
}
}
@@ -184,7 +183,7 @@ class ReaderActivity : BaseActivity() {
with(binding.numberPicker) {
minValue = 1
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
maxValue = cache.metadata.reader?.files?.size ?: 0
value = currentPage
}
val dialog = AlertDialog.Builder(this).apply {
@@ -307,17 +306,19 @@ class ReaderActivity : BaseActivity() {
notifyDataSetChanged()
}
title = reader.galleryInfo.title
title = reader.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(
this@ReaderActivity,
R.drawable.hitomi
/*
when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
}
}*/
)
}
}

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import androidx.core.graphics.drawable.DrawableCompat
import xyz.quaver.pupil.R
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 {
LOADING,

View File

@@ -35,7 +35,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.view.NestedScrollingChild;
@@ -48,7 +47,7 @@ import androidx.core.widget.TextViewCompat;
import xyz.quaver.pupil.R;
@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_ANIM_DURATION = 500;
@@ -84,15 +83,15 @@ public class MainView extends ViewGroup implements NestedScrollingChild, NestedS
private OnPageTurnListener mOnPageTurnListener;
public MainView(@NonNull Context context) {
public SwipePageTurnView(@NonNull Context context) {
this(context, null);
}
public MainView(@NonNull Context context, AttributeSet attr) {
public SwipePageTurnView(@NonNull Context context, AttributeSet attr) {
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);
setWillNotDraw(false);

View File

@@ -32,9 +32,8 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.io.FileX
import xyz.quaver.io.util.*
import xyz.quaver.pupil.client
@@ -46,24 +45,24 @@ import java.util.concurrent.ConcurrentHashMap
@Serializable
data class Metadata(
var galleryBlock: GalleryBlock? = null,
var reader: Reader? = null,
var reader: GalleryInfo? = null,
var imageList: MutableList<String?>? = null
) {
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 {
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] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
instances[galleryID] ?: Cache(context, galleryID).also { instances[galleryID] = it }
}
@Synchronized
fun delete(context: Context, galleryID: Int) {
fun delete(context: Context, galleryID: String) {
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
instances.remove(galleryID)
}
@@ -111,8 +110,8 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
{ xyz.quaver.hitomi.getGalleryBlock(galleryID.toInt()) }
// { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
return metadata.galleryBlock
@@ -154,22 +153,17 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}.getOrNull()?.uri }
} } ?: 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 sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.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
}
"hitomi" to { xyz.quaver.hitomi.getGalleryInfo(galleryID.toInt()) },
//Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
)
return metadata.reader
?: withContext(Dispatchers.IO) {
var reader: Reader? = null
var reader: GalleryInfo? = null
for (source in sources) {
reader = try {
@@ -187,7 +181,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.reader = it
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 }
}
private val lock = ConcurrentHashMap<Int, Mutex>()
private val lock = ConcurrentHashMap<String, Mutex>()
@Suppress("BlockingMethodInNonBlockingContext")
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
val downloadFolder = downloadFolder ?: return@launch

View File

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

View File

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

View File

@@ -46,7 +46,6 @@ import okhttp3.Request
import okhttp3.Response
import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader
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)) {
onFailure?.invoke(IllegalArgumentException())
return
@@ -214,7 +213,7 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
override fun onResponse(call: Call, response: Response) {
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)
onSuccess?.invoke(it)
}
@@ -237,111 +236,4 @@ 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"
tools:context=".ui.MainActivity">
<xyz.quaver.pupil.ui.view.MainView
android:id="@+id/view"
<xyz.quaver.pupil.ui.view.SwipePageTurnView
android:id="@+id/swipe_page_turn_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -52,7 +52,7 @@
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView>
</xyz.quaver.pupil.ui.view.SwipePageTurnView>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"

View File

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