Use ArrayDeque in reader
This commit is contained in:
@@ -63,8 +63,8 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.5.0-alpha04'
|
||||
implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.fragment_details.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -73,7 +72,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
)
|
||||
}
|
||||
manga.url.toUri().toFileOrNull()?.let { f ->
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleScope.launch {
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
f.length()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
private val convertLock = Mutex()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main.immediate + job
|
||||
get() = job + Dispatchers.Main.immediate
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun loadFile(url: String, force: Boolean): File {
|
||||
@@ -88,7 +88,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineContext.cancel()
|
||||
job.cancelChildren()
|
||||
tasks.clear()
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,14 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.*
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -53,7 +53,7 @@ import org.koitharu.kotatsu.utils.ext.*
|
||||
class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener,
|
||||
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
|
||||
ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
View.OnApplyWindowInsetsListener, ActivityResultCallback<Boolean> {
|
||||
ActivityResultCallback<Boolean>, OnApplyWindowInsetsListener {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::ReaderPresenter)
|
||||
private val settings by inject<AppSettings>()
|
||||
@@ -93,7 +93,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size)
|
||||
}
|
||||
|
||||
rootLayout.setOnApplyWindowInsetsListener(this)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootLayout, this)
|
||||
|
||||
settings.subscribe(this)
|
||||
loadSettings()
|
||||
@@ -105,10 +105,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
|
||||
presenter.init(state.manga)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
GlobalScope.launch {
|
||||
safe {
|
||||
MangaShortcut(state.manga).addAppShortcut(applicationContext)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main + IgnoreErrors) {
|
||||
MangaShortcut(state.manga).addAppShortcut(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +189,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
R.id.action_pages_thumbs -> {
|
||||
if (reader?.hasItems == true) {
|
||||
val pages = reader?.getPages()
|
||||
if (pages != null) {
|
||||
if (!pages.isNullOrEmpty()) {
|
||||
PagesThumbnailsSheet.show(
|
||||
supportFragmentManager, pages,
|
||||
state.chapter?.name ?: title?.toString().orEmpty()
|
||||
@@ -363,7 +361,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) {
|
||||
override fun onPageChanged(chapter: MangaChapter, page: Int) {
|
||||
title = chapter.name
|
||||
state.manga.chapters?.run {
|
||||
supportActionBar?.subtitle =
|
||||
@@ -395,18 +393,21 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
appbar_top.updatePadding(
|
||||
top = insets.systemWindowInsetTop,
|
||||
right = insets.systemWindowInsetRight,
|
||||
left = insets.systemWindowInsetLeft
|
||||
top = systemBars.top,
|
||||
right = systemBars.right,
|
||||
left = systemBars.left
|
||||
)
|
||||
appbar_bottom.updatePadding(
|
||||
bottom = insets.systemWindowInsetBottom,
|
||||
right = insets.systemWindowInsetRight,
|
||||
left = insets.systemWindowInsetLeft
|
||||
bottom = systemBars.bottom,
|
||||
right = systemBars.right,
|
||||
left = systemBars.left
|
||||
)
|
||||
return insets.consumeSystemWindowInsets()
|
||||
return WindowInsetsCompat.Builder(insets)
|
||||
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.ui.base.BaseMvpView
|
||||
|
||||
interface ReaderListener : BaseMvpView {
|
||||
|
||||
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
|
||||
fun onPageChanged(chapter: MangaChapter, page: Int)
|
||||
|
||||
fun saveState(chapterId: Long, page: Int, scroll: Int)
|
||||
}
|
||||
@@ -1,45 +1,52 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.base.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderListener
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.utils.ext.associateByLong
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayoutId),
|
||||
OnBoundsScrollListener {
|
||||
|
||||
protected lateinit var manga: Manga
|
||||
protected lateinit var loader: PageLoader
|
||||
private set
|
||||
private lateinit var pages: GroupedList<Long, MangaPage>
|
||||
private lateinit var chapters: LongSparseArray<MangaChapter>
|
||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
PageLoader()
|
||||
}
|
||||
protected val pages = ArrayDeque<ReaderPage>()
|
||||
protected var adapter: BaseReaderAdapter? = null
|
||||
private set
|
||||
|
||||
val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
val hasItems: Boolean
|
||||
get() = itemsCount != 0
|
||||
|
||||
val currentPage: MangaPage?
|
||||
get() = pages.getOrNull(getCurrentItem())
|
||||
get() = pages.getOrNull(getCurrentItem())?.toMangaPage()
|
||||
|
||||
protected val readerListener: ReaderListener?
|
||||
get() = activity as? ReaderListener
|
||||
private var readerListener: ReaderListener? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
pages = GroupedList()
|
||||
manga = requireArguments().getParcelable<ReaderState>(ARG_STATE)!!.manga
|
||||
loader = PageLoader()
|
||||
manga = requireNotNull(requireArguments().getParcelable<ReaderState>(ARG_STATE)).manga
|
||||
chapters = requireNotNull(manga.chapters).associateByLong { it.id }
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -50,7 +57,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!!
|
||||
loadChapter(state.chapterId) {
|
||||
pages.clear()
|
||||
pages.addLast(state.chapterId, it)
|
||||
it.mapIndexedTo(pages) { i, p ->
|
||||
ReaderPage.from(p, i, state.chapterId)
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
setCurrentItem(state.page, false)
|
||||
if (state.scroll != 0) {
|
||||
@@ -59,24 +68,37 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
readerListener = activity as? ReaderListener
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
readerListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
val page = pages.getOrNull(getCurrentItem()) ?: return
|
||||
outState.putParcelable(
|
||||
ARG_STATE, ReaderState(
|
||||
manga = manga,
|
||||
chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return,
|
||||
page = pages.getRelativeIndex(getCurrentItem()),
|
||||
chapterId = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = getCurrentPageScroll()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrolledToStart() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val chapterId = getFirstPage()?.chapterId ?: return
|
||||
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
|
||||
val prevChapterId = manga.chapters!!.getOrNull(index - 1)?.id ?: return
|
||||
loadChapter(prevChapterId) {
|
||||
pages.addFirst(prevChapterId, it)
|
||||
pages.addAll(0, it.mapIndexed { i, p ->
|
||||
ReaderPage.from(p, i, prevChapterId)
|
||||
})
|
||||
adapter?.notifyItemsPrepended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimEnd()
|
||||
@@ -85,11 +107,13 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
|
||||
val chapterId = getLastPage()?.chapterId ?: return
|
||||
val index = manga.chapters?.indexOfLast { it.id == chapterId } ?: return
|
||||
val nextChapterId = manga.chapters!!.getOrNull(index + 1)?.id ?: return
|
||||
loadChapter(nextChapterId) {
|
||||
pages.addLast(nextChapterId, it)
|
||||
pages.addAll(it.mapIndexed { i, p ->
|
||||
ReaderPage.from(p, i, nextChapterId)
|
||||
})
|
||||
adapter?.notifyItemsAppended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimStart()
|
||||
@@ -107,8 +131,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun getPages() = pages.findGroupByIndex(getCurrentItem())?.let {
|
||||
pages.getGroup(it)
|
||||
fun getPages(): List<MangaPage>? {
|
||||
val chapterId = (pages.getOrNull(getCurrentItem()) ?: return null).chapterId
|
||||
// TODO optimize
|
||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -117,11 +143,11 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
}
|
||||
|
||||
private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) {
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleScope.launch {
|
||||
readerListener?.onLoadingStateChanged(isLoading = true)
|
||||
try {
|
||||
val pages = withContext(Dispatchers.IO) {
|
||||
val chapter = manga.chapters?.find { it.id == chapterId }
|
||||
val pages = withContext(Dispatchers.Default) {
|
||||
val chapter = chapters.get(chapterId)
|
||||
?: throw RuntimeException("Chapter $chapterId not found")
|
||||
val repo = manga.source.repository
|
||||
repo.getPages(chapter)
|
||||
@@ -137,45 +163,39 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
}
|
||||
|
||||
private fun trimStart() {
|
||||
var removed = 0
|
||||
/*var removed = 0
|
||||
while (pages.groupCount > 3 && pages.size > 8) {
|
||||
removed += pages.removeFirst().size
|
||||
}
|
||||
if (removed != 0) {
|
||||
adapter?.notifyItemsRemovedStart(removed)
|
||||
Log.i(TAG, "Removed $removed pages from start")
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
private fun trimEnd() {
|
||||
var removed = 0
|
||||
/*var removed = 0
|
||||
while (pages.groupCount > 3 && pages.size > 8) {
|
||||
removed += pages.removeLast().size
|
||||
}
|
||||
if (removed != 0) {
|
||||
adapter?.notifyItemsRemovedEnd(removed)
|
||||
Log.i(TAG, "Removed $removed pages from end")
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
protected fun notifyPageChanged(page: Int) {
|
||||
val chapters = manga.chapters ?: return
|
||||
val chapterId = pages.findGroupByIndex(page) ?: return
|
||||
val chapter = chapters.find { it.id == chapterId } ?: return
|
||||
protected fun notifyPageChanged(position: Int) {
|
||||
val page = pages.getOrNull(position) ?: return
|
||||
val chapter = chapters.get(page.chapterId) ?: return
|
||||
readerListener?.onPageChanged(
|
||||
chapter = chapter,
|
||||
page = page - pages.getGroupOffset(chapterId),
|
||||
total = pages.getGroup(chapterId)?.size ?: return
|
||||
page = page.index
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveState() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val page = pages.getRelativeIndex(getCurrentItem())
|
||||
if (page != -1) {
|
||||
readerListener?.saveState(chapterId, page, getCurrentPageScroll())
|
||||
}
|
||||
Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)")
|
||||
private fun saveState() {
|
||||
val page = pages.getOrNull(getCurrentItem()) ?: return
|
||||
readerListener?.saveState(page.chapterId, page.index, getCurrentPageScroll())
|
||||
}
|
||||
|
||||
open fun switchPageBy(delta: Int) {
|
||||
@@ -183,13 +203,15 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
}
|
||||
|
||||
fun updateState(chapterId: Long = 0, pageId: Long = 0) {
|
||||
val currentChapterId = pages.findGroupByIndex(getCurrentItem())
|
||||
val currentChapterId = pages.getOrNull(getCurrentItem())?.chapterId ?: 0L
|
||||
if (chapterId != 0L && chapterId != currentChapterId) {
|
||||
pages.clear()
|
||||
adapter?.notifyDataSetChanged()
|
||||
loadChapter(chapterId) {
|
||||
pages.clear()
|
||||
pages.addLast(chapterId, it)
|
||||
it.mapIndexedTo(pages) { i, p ->
|
||||
ReaderPage.from(p, i, chapterId)
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
setCurrentItem(
|
||||
if (pageId == 0L) {
|
||||
@@ -200,19 +222,27 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setCurrentItem(
|
||||
if (pageId == 0L) {
|
||||
0
|
||||
} else {
|
||||
val chapterPages = pages.getGroup(currentChapterId ?: return) ?: return
|
||||
chapterPages.indexOfFirst { it.id == pageId }
|
||||
.coerceAtLeast(0) + pages.getGroupOffset(currentChapterId)
|
||||
}, false
|
||||
)
|
||||
var index = 0
|
||||
if (pageId != 0L) {
|
||||
index = pages.indexOfFirst {
|
||||
it.chapterId == currentChapterId && it.id == pageId
|
||||
}
|
||||
if (index == -1) { // try to find chapter at least
|
||||
index = pages.indexOfFirst {
|
||||
it.chapterId == currentChapterId
|
||||
}
|
||||
}
|
||||
if (index == -1) {
|
||||
index = 0
|
||||
}
|
||||
}
|
||||
setCurrentItem(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
abstract val itemsCount: Int
|
||||
protected open fun getLastPage() = pages.lastOrNull()
|
||||
|
||||
protected open fun getFirstPage() = pages.firstOrNull()
|
||||
|
||||
protected abstract fun getCurrentItem(): Int
|
||||
|
||||
@@ -222,12 +252,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
|
||||
|
||||
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
|
||||
|
||||
protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter
|
||||
protected abstract fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter
|
||||
|
||||
protected companion object {
|
||||
|
||||
const val ARG_STATE = "state"
|
||||
private const val TAG = "AbstractReader"
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,17 @@ package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
|
||||
|
||||
abstract class BaseReaderAdapter(protected val pages: GroupedList<Long, MangaPage>) :
|
||||
RecyclerView.Adapter<BaseViewHolder<MangaPage, Unit>>() {
|
||||
abstract class BaseReaderAdapter(protected val pages: List<ReaderPage>) :
|
||||
RecyclerView.Adapter<BaseViewHolder<ReaderPage, Unit>>() {
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<MangaPage, Unit>, position: Int) {
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit>, position: Int) {
|
||||
val item = pages[position]
|
||||
holder.bind(item, Unit)
|
||||
}
|
||||
@@ -36,18 +35,18 @@ abstract class BaseReaderAdapter(protected val pages: GroupedList<Long, MangaPag
|
||||
notifyItemRangeRemoved(pages.size - count, count)
|
||||
}
|
||||
|
||||
open override fun getItemId(position: Int) = pages[position].id
|
||||
override fun getItemId(position: Int) = pages[position].id
|
||||
|
||||
final override fun getItemCount() = pages.size
|
||||
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BaseViewHolder<MangaPage, Unit> {
|
||||
): BaseViewHolder<ReaderPage, Unit> {
|
||||
return onCreateViewHolder(parent).also(this::onViewHolderCreated)
|
||||
}
|
||||
|
||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<MangaPage, Unit>) = Unit
|
||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<ReaderPage, Unit>) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<MangaPage, Unit>
|
||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<ReaderPage, Unit>
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import java.util.*
|
||||
|
||||
class GroupedList<K, T> {
|
||||
|
||||
private val data = LinkedList<Pair<K, List<T>>>()
|
||||
|
||||
private var intSize: Int = -1
|
||||
private var lruGroup: List<T>? = null
|
||||
private var lruGroupKey: K? = null
|
||||
private var lruGroupFirstIndex = -1
|
||||
|
||||
val size: Int
|
||||
get() {
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
}
|
||||
return intSize
|
||||
}
|
||||
|
||||
val groupCount: Int
|
||||
get() = data.size
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = size == 0
|
||||
|
||||
val isNotEmpty: Boolean
|
||||
get() = size != 0
|
||||
|
||||
operator fun get(index: Int): T {
|
||||
if (index >= lruGroupFirstIndex) {
|
||||
val relIndex = index - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return it[relIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || index < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (index < firstIndex + entry.second.size && index >= firstIndex) {
|
||||
lruGroup = entry.second
|
||||
lruGroupKey = entry.first
|
||||
lruGroupFirstIndex = firstIndex
|
||||
return entry.second[index - firstIndex]
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (index < lastIndex && index >= lastIndex - entry.second.size) {
|
||||
lruGroup = entry.second
|
||||
lruGroupKey = entry.first
|
||||
lruGroupFirstIndex = lastIndex - entry.second.size
|
||||
return entry.second[index - lruGroupFirstIndex]
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int) = try {
|
||||
get(index)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun getLastKey() = data.peekLast()?.first
|
||||
|
||||
fun getFirstKey() = data.peekFirst()?.first
|
||||
|
||||
fun getGroup(key: K): List<T>? {
|
||||
if (key == lruGroupKey && lruGroup != null) {
|
||||
return lruGroup
|
||||
} else {
|
||||
for(entry in data) {
|
||||
if (entry.first == key) {
|
||||
return entry.second
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getRelativeIndex(absIndex: Int): Int {
|
||||
if (absIndex >= lruGroupFirstIndex) {
|
||||
val relIndex = absIndex - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return relIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || absIndex < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
|
||||
return absIndex - firstIndex
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
|
||||
return absIndex - lruGroupFirstIndex
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun findGroupByIndex(absIndex: Int): K? {
|
||||
if (absIndex >= lruGroupFirstIndex && lruGroupKey != null) {
|
||||
val relIndex = absIndex - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return lruGroupKey
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || absIndex < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
|
||||
return entry.first
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
|
||||
return entry.first
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getGroupOffset(key: K): Int {
|
||||
if (lruGroupKey == key && lruGroupFirstIndex >= 0) {
|
||||
return lruGroupFirstIndex
|
||||
}
|
||||
var offset = 0
|
||||
for (entry in data) {
|
||||
if (entry.first == key) {
|
||||
return offset
|
||||
}
|
||||
offset += entry.second.size
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun indexOf(item: T): Int {
|
||||
var offset = 0
|
||||
for ((_, list) in data) {
|
||||
for ((i, x) in list.withIndex()) {
|
||||
if (x == item) {
|
||||
return i + offset
|
||||
}
|
||||
}
|
||||
offset += list.size
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun addLast(key: K, items: List<T>) {
|
||||
data.addLast(key to items.toList())
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize += items.size
|
||||
}
|
||||
}
|
||||
|
||||
fun addFirst(key: K, items: List<T>) {
|
||||
data.addFirst(key to items.toList())
|
||||
if (lruGroupFirstIndex >= 0) {
|
||||
lruGroupFirstIndex += items.size
|
||||
}
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize += items.size
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLast(): List<T> {
|
||||
val item = data.removeLast()
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize -= item.second.size
|
||||
}
|
||||
return item.second
|
||||
}
|
||||
|
||||
fun removeFirst(): List<T> {
|
||||
val item = data.removeFirst()
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize -= item.second.size
|
||||
}
|
||||
if (lruGroupFirstIndex >= 0) {
|
||||
lruGroupFirstIndex -= item.second.size
|
||||
}
|
||||
return item.second
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
data.clear()
|
||||
intSize = 0
|
||||
lruGroupFirstIndex = -1
|
||||
lruGroup = null
|
||||
lruGroupKey = null
|
||||
}
|
||||
|
||||
private fun computeSize() {
|
||||
intSize = data.sumBy { it.second.size }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
|
||||
@Parcelize
|
||||
data class ReaderPage(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val preview: String?,
|
||||
val chapterId: Long,
|
||||
val index: Int,
|
||||
val source: MangaSource
|
||||
) : Parcelable {
|
||||
|
||||
fun toMangaPage() = MangaPage(
|
||||
id = id,
|
||||
url = url,
|
||||
preview = preview,
|
||||
source = source
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(page: MangaPage, index: Int, chapterId: Long) = ReaderPage(
|
||||
id = page.id,
|
||||
url = page.url,
|
||||
preview = page.preview,
|
||||
chapterId = chapterId,
|
||||
index = index,
|
||||
source = page.source
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
package org.koitharu.kotatsu.ui.reader.reversed
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.ui.reader.standard.PageHolder
|
||||
|
||||
class ReversedPagesAdapter(
|
||||
pages: GroupedList<Long, MangaPage>,
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<MangaPage, Unit>, position: Int) {
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit>, position: Int) {
|
||||
super.onBindViewHolder(holder, reversed(position))
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): MangaPage {
|
||||
override fun getItem(position: Int): ReaderPage {
|
||||
return super.getItem(reversed(position))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_reader_standard.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.ui.reader.standard.PageAnimTransformer
|
||||
import org.koitharu.kotatsu.ui.reader.standard.PagerPaginationListener
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
@@ -53,13 +52,10 @@ class ReversedReaderFragment : AbstractReader(R.layout.fragment_reader_standard)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter {
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return ReversedPagesAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
override fun getCurrentItem() = reversed(pager.currentItem)
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
@@ -82,6 +78,10 @@ class ReversedReaderFragment : AbstractReader(R.layout.fragment_reader_standard)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLastPage() = pages.firstOrNull()
|
||||
|
||||
override fun getFirstPage() = pages.lastOrNull()
|
||||
|
||||
private fun reversed(position: Int) = (itemsCount - position - 1).coerceAtLeast(0)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -7,14 +7,14 @@ import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.android.synthetic.main.item_page.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
||||
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page),
|
||||
BaseViewHolder<ReaderPage, Unit>(parent, R.layout.item_page),
|
||||
PageHolderDelegate.Callback, View.OnClickListener {
|
||||
|
||||
private val delegate = PageHolderDelegate(loader, this)
|
||||
@@ -24,8 +24,8 @@ class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
||||
button_retry.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(data: MangaPage, extra: Unit) {
|
||||
delegate.onBind(data)
|
||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
@@ -57,7 +57,7 @@ class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_retry -> delegate.retry(boundData ?: return)
|
||||
R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_reader_standard.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
@@ -49,13 +48,10 @@ class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter {
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return PagesAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
override fun getCurrentItem() = pager.currentItem
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.koitharu.kotatsu.ui.reader.standard
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
|
||||
class PagesAdapter(
|
||||
pages: GroupedList<Long, MangaPage>,
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
|
||||
class WebtoonAdapter(
|
||||
pages: GroupedList<Long, MangaPage>,
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.android.synthetic.main.item_page_webtoon.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
|
||||
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon),
|
||||
BaseViewHolder<ReaderPage, Unit>(parent, R.layout.item_page_webtoon),
|
||||
PageHolderDelegate.Callback, View.OnClickListener {
|
||||
|
||||
private val delegate = PageHolderDelegate(loader, this)
|
||||
@@ -27,8 +27,8 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
button_retry.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(data: MangaPage, extra: Unit) {
|
||||
delegate.onBind(data)
|
||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
@@ -65,7 +65,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_retry -> delegate.retry(boundData ?: return)
|
||||
R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import kotlinx.android.synthetic.main.fragment_reader_webtoon.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
@@ -29,7 +28,7 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
|
||||
recyclerView.doOnCurrentItemChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter {
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return WebtoonAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
@@ -38,9 +37,6 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
override fun getCurrentItem(): Int {
|
||||
return recyclerView.findCenterViewPosition()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.collection.LongSparseArray
|
||||
|
||||
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
|
||||
clear()
|
||||
@@ -56,4 +58,12 @@ fun LongArray.toArraySet(): Set<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(HashMap<K, V>(size))
|
||||
fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(ArrayMap(size))
|
||||
|
||||
inline fun <T> Collection<T>.associateByLong(selector: (T) -> Long): LongSparseArray<T> {
|
||||
val result = LongSparseArray<T>(size)
|
||||
for (item in this) {
|
||||
result.put(selector(item), item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user