Refactor reader
This commit is contained in:
@@ -32,12 +32,14 @@ abstract class BaseFragment<B : ViewBinding> : Fragment() {
|
||||
|
||||
open fun getTitle(): CharSequence? = null
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
getTitle()?.let {
|
||||
activity?.title = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun bindingOrNull() = viewBinding
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@Deprecated("")
|
||||
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
|
||||
RecyclerView.ViewHolder(binding.root), KoinComponent {
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.DownloadService
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
|
||||
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
|
||||
@@ -84,9 +85,9 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
)
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
view.context,
|
||||
viewModel.manga.value ?: return,
|
||||
item.id
|
||||
ReaderState(item.id, 0, 0)
|
||||
), options.toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,12 +209,14 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
||||
|
||||
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga) =
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, manga)
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long) =
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import kotlin.math.roundToInt
|
||||
@@ -138,7 +139,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
manga ?: return,
|
||||
viewModel.readingHistory.value
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -157,7 +158,10 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return@showPopupMenu false,
|
||||
viewModel.manga.value ?: return@showPopupMenu false
|
||||
viewModel.manga.value ?: return@showPopupMenu false,
|
||||
viewModel.chapters.value?.firstOrNull()?.let { c ->
|
||||
ReaderState(c.chapter.id, 0, 0)
|
||||
}
|
||||
)
|
||||
)
|
||||
true
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
@@ -34,28 +35,28 @@ class DetailsViewModel(
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
historyRepository.observeOne(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
private val favourite = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
private val newChapters = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { mangaId ->
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
val manga = mangaData.filterNotNull()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val favouriteCategories = favourite
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val newChaptersCount = newChapters
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val readingHistory = history
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
|
||||
@@ -76,7 +77,7 @@ class DetailsViewModel(
|
||||
}
|
||||
)
|
||||
}
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
|
||||
@@ -30,7 +30,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
adapterState = savedInstanceState?.getParcelable(KEY_ADAPTER_STATE) ?: adapterState
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
@@ -41,7 +40,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val adapter = FavouritesPagerAdapter(this, this)
|
||||
adapterState?.let(adapter::restoreState)
|
||||
binding.pager.adapter = adapter
|
||||
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
||||
|
||||
@@ -49,6 +47,13 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
(savedInstanceState?.getParcelable(KEY_ADAPTER_STATE) ?: adapterState)?.let {
|
||||
(binding.pager.adapter as FavouritesPagerAdapter).restoreState(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapterState = (binding.pager.adapter as? FavouritesPagerAdapter)?.saveState()
|
||||
super.onDestroyView()
|
||||
@@ -56,6 +61,8 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
adapterState = (bindingOrNull()?.pager?.adapter as? FavouritesPagerAdapter)?.saveState()
|
||||
?: adapterState
|
||||
outState.putParcelable(KEY_ADAPTER_STATE, adapterState)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
|
||||
@@ -14,7 +15,7 @@ class FavouritesCategoriesViewModel(
|
||||
private var reorderJob: Job? = null
|
||||
|
||||
val categories = repository.observeCategories()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
fun createCategory(name: String) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
@@ -25,7 +26,7 @@ class MangaCategoriesViewModel(
|
||||
isChecked = it.id in checked
|
||||
)
|
||||
}
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.list
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -15,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class FavouritesListViewModel(
|
||||
@@ -41,11 +41,9 @@ class FavouritesListViewModel(
|
||||
}
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.catch {
|
||||
emit(listOf(it.toErrorState(canRetry = false)))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.history.ui
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -18,6 +17,7 @@ import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.util.*
|
||||
@@ -51,11 +51,9 @@ class HistoryListViewModel(
|
||||
}
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.catch {
|
||||
it.toErrorState(canRetry = false)
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ abstract class MangaListViewModel(
|
||||
.filter { it == AppSettings.KEY_GRID_SIZE }
|
||||
.map { settings.gridSize / 100f }
|
||||
.onStart { emit(settings.gridSize / 100f) }
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.IO)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
protected fun createListModeFlow() = settings.observe()
|
||||
.filter { it == AppSettings.KEY_LIST_MODE }
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -52,7 +53,7 @@ class LocalListViewModel(
|
||||
}
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.AppSection
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
@@ -26,7 +27,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
import org.koitharu.kotatsu.search.ui.SearchHelper
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
@@ -159,7 +159,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onOpenReader(state: ReaderState) {
|
||||
private fun onOpenReader(manga: Manga) {
|
||||
val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ActivityOptions.makeClipRevealAnimation(
|
||||
binding.fab, 0, 0, binding.fab.measuredWidth, binding.fab.measuredHeight
|
||||
@@ -169,7 +169,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
binding.fab, 0, 0, binding.fab.measuredWidth, binding.fab.measuredHeight
|
||||
)
|
||||
}
|
||||
startActivity(ReaderActivity.newIntent(this, state), options?.toBundle())
|
||||
startActivity(ReaderActivity.newIntent(this, manga, null), options?.toBundle())
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
|
||||
@@ -3,16 +3,13 @@ package org.koitharu.kotatsu.main.ui
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
|
||||
class MainViewModel(
|
||||
@@ -20,7 +17,7 @@ class MainViewModel(
|
||||
settings: AppSettings
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onOpenReader = SingleLiveEvent<ReaderState>()
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
var defaultSection by settings::defaultSection
|
||||
|
||||
val remoteSources = settings.observe()
|
||||
@@ -28,18 +25,14 @@ class MainViewModel(
|
||||
.onStart { emit("") }
|
||||
.map { MangaProviderFactory.getSources(settings, includeHidden = false) }
|
||||
.distinctUntilChanged()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
fun openLastReader() {
|
||||
launchLoadingJob {
|
||||
val manga = historyRepository.getList(0, 1).firstOrNull()
|
||||
?: throw EmptyHistoryException()
|
||||
val history = historyRepository.getOne(manga) ?: throw EmptyHistoryException()
|
||||
val state = ReaderState(
|
||||
manga.source.repository.getDetails(manga),
|
||||
history.chapterId, history.page, history.scroll
|
||||
)
|
||||
onOpenReader.call(state)
|
||||
onOpenReader.call(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
|
||||
@Deprecated("TODO not object")
|
||||
object AppProtectHelper : KoinComponent {
|
||||
|
||||
val settings by inject<AppSettings>()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.koitharu.kotatsu.reader
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class ReaderControlDelegate(
|
||||
private val scope: LifecycleCoroutineScope,
|
||||
private val settings: AppSettings,
|
||||
private val listener: OnInteractionListener
|
||||
) {
|
||||
|
||||
private var isTapSwitchEnabled: Boolean = true
|
||||
private var isVolumeKeysSwitchEnabled: Boolean = false
|
||||
|
||||
init {
|
||||
settings.observe()
|
||||
.filter { it == AppSettings.KEY_READER_SWITCHERS }
|
||||
.map { settings.readerPageSwitch }
|
||||
.onStart { emit(settings.readerPageSwitch) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.onEach {
|
||||
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
|
||||
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun onGridTouch(area: Int) {
|
||||
when (area) {
|
||||
GridTouchHelper.AREA_CENTER -> {
|
||||
listener.toggleUiVisibility()
|
||||
}
|
||||
GridTouchHelper.AREA_TOP,
|
||||
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(-1)
|
||||
}
|
||||
GridTouchHelper.AREA_BOTTOM,
|
||||
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
|
||||
listener.switchPageBy(-1)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
|
||||
listener.switchPageBy(1)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
listener.switchPageBy(1)
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_PAGE_UP,
|
||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
listener.switchPageBy(-1)
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
listener.toggleUiVisibility()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
return (isVolumeKeysSwitchEnabled &&
|
||||
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP))
|
||||
}
|
||||
|
||||
interface OnInteractionListener {
|
||||
|
||||
fun switchPageBy(delta: Int)
|
||||
|
||||
fun toggleUiVisibility()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.reader
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
val readerModule
|
||||
@@ -12,5 +14,7 @@ val readerModule
|
||||
single { MangaDataRepository(get()) }
|
||||
single { PagesCache(get()) }
|
||||
|
||||
viewModel { ReaderViewModel(get(), get()) }
|
||||
viewModel { (intent: MangaIntent, state: ReaderState?) ->
|
||||
ReaderViewModel(intent, state, get(), get(), get())
|
||||
}
|
||||
}
|
||||
@@ -9,26 +9,21 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
class PageLoader(
|
||||
scope: CoroutineScope,
|
||||
private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache
|
||||
) : CoroutineScope by scope {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
private val tasks = ArrayMap<String, Deferred<File>>()
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
private val cache by inject<PagesCache>()
|
||||
private val convertLock = Mutex()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main.immediate
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun loadFile(url: String, force: Boolean): File {
|
||||
if (!force) {
|
||||
@@ -74,7 +69,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
|
||||
suspend fun convertInPlace(file: File) {
|
||||
convertLock.withLock(file) {
|
||||
withContext(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
file.outputStream().use { out ->
|
||||
@@ -86,9 +81,4 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
job.cancelChildren()
|
||||
tasks.clear()
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.reader.ui
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
@@ -20,53 +18,54 @@ import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseFullscreenActivity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
||||
import org.koitharu.kotatsu.reader.ui.reversed.ReversedReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ReaderControlDelegate
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.wetoon.WebtoonReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import org.koitharu.kotatsu.reader.ui.wetoon.WebtoonReaderFragment
|
||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||
import org.koitharu.kotatsu.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.anim.Motion
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
|
||||
import org.koitharu.kotatsu.utils.ext.hideAnimated
|
||||
import org.koitharu.kotatsu.utils.ext.hitTest
|
||||
import org.koitharu.kotatsu.utils.ext.showAnimated
|
||||
|
||||
class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
ChaptersDialog.OnChapterChangeListener,
|
||||
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
|
||||
ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
ActivityResultCallback<Boolean>, OnApplyWindowInsetsListener {
|
||||
ActivityResultCallback<Boolean>, OnApplyWindowInsetsListener,
|
||||
ReaderControlDelegate.OnInteractionListener {
|
||||
|
||||
private val viewModel by viewModel<ReaderViewModel>()
|
||||
private val settings by inject<AppSettings>()
|
||||
|
||||
lateinit var state: ReaderState
|
||||
private set
|
||||
private val viewModel by viewModel<ReaderViewModel> {
|
||||
parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra<ReaderState>(EXTRA_STATE))
|
||||
}
|
||||
|
||||
private lateinit var touchHelper: GridTouchHelper
|
||||
private lateinit var orientationHelper: ScreenOrientationHelper
|
||||
private var isTapSwitchEnabled = true
|
||||
private var isVolumeKeysSwitchEnabled = false
|
||||
private lateinit var controlDelegate: ReaderControlDelegate
|
||||
|
||||
private val reader
|
||||
get() = supportFragmentManager.findFragmentById(R.id.container) as? AbstractReader<*>
|
||||
get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -74,63 +73,43 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
touchHelper = GridTouchHelper(this, this)
|
||||
orientationHelper = ScreenOrientationHelper(this)
|
||||
controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this)
|
||||
binding.toolbarBottom.inflateMenu(R.menu.opt_reader_bottom)
|
||||
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
||||
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
state = savedInstanceState?.getParcelable<ReaderState>(EXTRA_STATE)
|
||||
?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE)
|
||||
?: let {
|
||||
Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show()
|
||||
finishAfterTransition()
|
||||
return
|
||||
}
|
||||
|
||||
title = state.chapter?.name ?: state.manga.title
|
||||
state.manga.chapters?.run {
|
||||
supportActionBar?.subtitle =
|
||||
getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size)
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout, this)
|
||||
|
||||
settings.subscribe(this)
|
||||
loadSwitchSettings()
|
||||
orientationHelper.observeAutoOrientation()
|
||||
.onEach {
|
||||
binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
|
||||
}.launchIn(lifecycleScope)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.init(state.manga)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
GlobalScope.launch(Dispatchers.Main + IgnoreErrors) {
|
||||
MangaShortcut(state.manga).addAppShortcut(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.reader.observe(this) { (manga, mode) -> onInitReader(manga, mode) }
|
||||
viewModel.readerMode.observe(this, this::onInitReader)
|
||||
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
||||
viewModel.uiState.observe(this, this::onUiStateChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.content.observe(this) {
|
||||
onLoadingStateChanged(viewModel.isLoading.value == true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInitReader(manga: Manga, mode: ReaderMode) {
|
||||
private fun onInitReader(mode: ReaderMode) {
|
||||
val currentReader = reader
|
||||
when (mode) {
|
||||
ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, WebtoonReaderFragment.newInstance(state))
|
||||
replace(R.id.container, WebtoonReaderFragment())
|
||||
}
|
||||
}
|
||||
ReaderMode.REVERSED -> if (currentReader !is ReversedReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, ReversedReaderFragment.newInstance(state))
|
||||
replace(R.id.container, ReversedReaderFragment())
|
||||
}
|
||||
}
|
||||
ReaderMode.STANDARD -> if (currentReader !is PagerReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, PagerReaderFragment.newInstance(state))
|
||||
replace(R.id.container, PagerReaderFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,9 +125,9 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroy()
|
||||
override fun onPause() {
|
||||
viewModel.saveCurrentState(reader?.getCurrentState())
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
@@ -156,11 +135,6 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_reader_mode -> {
|
||||
@@ -182,30 +156,28 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
R.id.action_chapters -> {
|
||||
ChaptersDialog.show(
|
||||
supportFragmentManager,
|
||||
state.manga.chapters.orEmpty(),
|
||||
state.chapterId
|
||||
viewModel.manga?.chapters.orEmpty(),
|
||||
viewModel.getCurrentState()?.chapterId ?: 0L
|
||||
)
|
||||
}
|
||||
R.id.action_screen_rotate -> {
|
||||
orientationHelper.toggleOrientation()
|
||||
}
|
||||
R.id.action_pages_thumbs -> {
|
||||
if (reader?.hasItems == true) {
|
||||
val pages = reader?.getPages()
|
||||
if (!pages.isNullOrEmpty()) {
|
||||
PagesThumbnailsSheet.show(
|
||||
supportFragmentManager, pages,
|
||||
state.chapter?.name ?: title?.toString().orEmpty()
|
||||
)
|
||||
} else {
|
||||
showWaitWhileLoading()
|
||||
}
|
||||
val pages = viewModel.getCurrentChapterPages()
|
||||
if (!pages.isNullOrEmpty()) {
|
||||
PagesThumbnailsSheet.show(
|
||||
supportFragmentManager,
|
||||
pages,
|
||||
title?.toString().orEmpty(),
|
||||
reader?.getCurrentState()?.page ?: -1
|
||||
)
|
||||
} else {
|
||||
showWaitWhileLoading()
|
||||
}
|
||||
}
|
||||
R.id.action_save_page -> {
|
||||
if (reader?.hasItems == true) {
|
||||
if (!viewModel.content.value?.pages.isNullOrEmpty()) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
@@ -229,30 +201,22 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
|
||||
override fun onActivityResult(result: Boolean) {
|
||||
if (result) {
|
||||
viewModel.savePage(
|
||||
resolver = contentResolver,
|
||||
page = reader?.currentPage ?: return
|
||||
)
|
||||
viewModel.saveCurrentPage(contentResolver)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveState(chapterId: Long, page: Int, scroll: Int) {
|
||||
state = state.copy(chapterId = chapterId, page = page, scroll = scroll)
|
||||
ReaderViewModel.saveState(state)
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasPages = reader?.hasItems == true
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
|
||||
binding.layoutLoading.isVisible = isLoading && !hasPages
|
||||
binding.progressBarBottom.isVisible = isLoading && hasPages
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
private fun onError(e: Throwable) {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(e.message)
|
||||
.setPositiveButton(R.string.close, null)
|
||||
if (reader?.hasItems != true) {
|
||||
if (viewModel.content.value?.pages.isNullOrEmpty()) {
|
||||
dialog.setOnDismissListener {
|
||||
finish()
|
||||
}
|
||||
@@ -261,19 +225,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
|
||||
override fun onGridTouch(area: Int) {
|
||||
when (area) {
|
||||
GridTouchHelper.AREA_CENTER -> {
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
}
|
||||
GridTouchHelper.AREA_TOP,
|
||||
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
|
||||
reader?.switchPageBy(-1)
|
||||
}
|
||||
GridTouchHelper.AREA_BOTTOM,
|
||||
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
|
||||
reader?.switchPageBy(1)
|
||||
}
|
||||
}
|
||||
controlDelegate.onGridTouch(area)
|
||||
}
|
||||
|
||||
override fun onProcessTouch(rawX: Int, rawY: Int): Boolean {
|
||||
@@ -292,61 +244,32 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
|
||||
reader?.switchPageBy(-1)
|
||||
true
|
||||
} else {
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
|
||||
reader?.switchPageBy(1)
|
||||
true
|
||||
} else {
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
reader?.switchPageBy(1)
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_PAGE_UP,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
reader?.switchPageBy(-1)
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
true
|
||||
}
|
||||
else -> super.onKeyDown(keyCode, event)
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
return controlDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
return (isVolumeKeysSwitchEnabled &&
|
||||
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP))
|
||||
|| super.onKeyUp(keyCode, event)
|
||||
return controlDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
override fun onChapterChanged(chapter: MangaChapter) {
|
||||
state = state.copy(
|
||||
chapterId = chapter.id,
|
||||
page = 0,
|
||||
scroll = 0
|
||||
)
|
||||
reader?.updateState(chapterId = chapter.id)
|
||||
viewModel.switchChapter(chapter.id)
|
||||
}
|
||||
|
||||
override fun onPageSelected(page: MangaPage) {
|
||||
reader?.updateState(pageId = page.id)
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
val pages = viewModel.content.value?.pages ?: return@launch
|
||||
val index = pages.indexOfFirst { it.id == page.id }
|
||||
if (index != -1) {
|
||||
withContext(Dispatchers.Main) {
|
||||
reader?.switchPageTo(index, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReaderModeChanged(mode: ReaderMode) {
|
||||
//TODO save state
|
||||
viewModel.setMode(state.manga, mode)
|
||||
viewModel.switchMode(mode)
|
||||
}
|
||||
|
||||
private fun onPageSaved(uri: Uri?) {
|
||||
@@ -363,22 +286,6 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageChanged(chapter: MangaChapter, page: Int) {
|
||||
title = chapter.name
|
||||
state.manga.chapters?.run {
|
||||
supportActionBar?.subtitle =
|
||||
getString(R.string.chapter_d_of_d, chapter.number, size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_READER_SWITCHERS -> loadSwitchSettings()
|
||||
AppSettings.KEY_READER_ANIMATION,
|
||||
AppSettings.KEY_ZOOM_MODE -> reader?.recreateAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showWaitWhileLoading() {
|
||||
Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
|
||||
setGravity(Gravity.CENTER, 0, 0)
|
||||
@@ -416,10 +323,20 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun loadSwitchSettings() {
|
||||
settings.readerPageSwitch.let {
|
||||
isTapSwitchEnabled = it.contains(AppSettings.PAGE_SWITCH_TAPS)
|
||||
isVolumeKeysSwitchEnabled = it.contains(AppSettings.PAGE_SWITCH_VOLUME_KEYS)
|
||||
override fun switchPageBy(delta: Int) {
|
||||
reader?.switchPageBy(delta)
|
||||
}
|
||||
|
||||
override fun toggleUiVisibility() {
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
}
|
||||
|
||||
private fun onUiStateChanged(uiState: ReaderUiState) {
|
||||
title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_)
|
||||
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
|
||||
getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,32 +344,16 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
|
||||
private const val EXTRA_STATE = "state"
|
||||
|
||||
fun newIntent(context: Context, state: ReaderState) =
|
||||
Intent(context, ReaderActivity::class.java)
|
||||
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, manga)
|
||||
.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, chapterId: Long = -1) = newIntent(
|
||||
context, ReaderState(
|
||||
manga = manga,
|
||||
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
|
||||
?: -1 else chapterId,
|
||||
page = 0,
|
||||
scroll = 0
|
||||
)
|
||||
)
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, history: MangaHistory?) =
|
||||
if (history == null) {
|
||||
newIntent(context, manga)
|
||||
} else {
|
||||
newIntent(
|
||||
context, ReaderState(
|
||||
manga = manga,
|
||||
chapterId = history.chapterId,
|
||||
page = history.page,
|
||||
scroll = history.scroll
|
||||
)
|
||||
)
|
||||
}
|
||||
fun newIntent(context: Context, mangaId: Long, state: ReaderState?): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -31,6 +32,7 @@ class ReaderConfigDialog : AlertDialogFragment<DialogReaderConfigBinding>(),
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(R.string.read_mode)
|
||||
.setPositiveButton(R.string.done, null)
|
||||
.setCancelable(true)
|
||||
}
|
||||
|
||||
@@ -40,19 +42,19 @@ class ReaderConfigDialog : AlertDialogFragment<DialogReaderConfigBinding>(),
|
||||
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
|
||||
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
|
||||
|
||||
binding.buttonOk.setOnClickListener(this)
|
||||
binding.buttonStandard.setOnClickListener(this)
|
||||
binding.buttonReversed.setOnClickListener(this)
|
||||
binding.buttonWebtoon.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
((parentFragment as? Callback)
|
||||
?: (activity as? Callback))?.onReaderModeChanged(mode)
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_ok -> {
|
||||
((parentFragment as? Callback)
|
||||
?: (activity as? Callback))?.onReaderModeChanged(mode)
|
||||
dismiss()
|
||||
}
|
||||
R.id.button_standard -> mode = ReaderMode.STANDARD
|
||||
R.id.button_webtoon -> mode = ReaderMode.WEBTOON
|
||||
R.id.button_reversed -> mode = ReaderMode.REVERSED
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
|
||||
data class ReaderContent(
|
||||
val pages: List<ReaderPage>,
|
||||
val state: ReaderState?
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
|
||||
interface ReaderListener {
|
||||
|
||||
fun onPageChanged(chapter: MangaChapter, page: Int)
|
||||
|
||||
fun saveState(chapterId: Long, page: Int, scroll: Int)
|
||||
|
||||
fun onLoadingStateChanged(isLoading: Boolean)
|
||||
|
||||
fun onError(error: Throwable)
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
|
||||
@Parcelize
|
||||
data class ReaderState(
|
||||
val manga: Manga,
|
||||
val chapterId: Long,
|
||||
val page: Int,
|
||||
val scroll: Int
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val chapter: MangaChapter? by lazy {
|
||||
manga.chapters?.find { it.id == chapterId }
|
||||
companion object {
|
||||
|
||||
fun from(history: MangaHistory) = ReaderState(
|
||||
chapterId = history.chapterId,
|
||||
page = history.page,
|
||||
scroll = history.scroll
|
||||
)
|
||||
|
||||
fun initial(manga: Manga) = ReaderState(
|
||||
chapterId = manga.chapters?.firstOrNull()?.id ?: error("Cannot find first chapter"),
|
||||
page = 0,
|
||||
scroll = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,96 +2,198 @@ package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.util.LongSparseArray
|
||||
import android.webkit.URLUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.domain.MangaUtils
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||
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.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class ReaderViewModel(
|
||||
intent: MangaIntent,
|
||||
state: ReaderState?,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings
|
||||
) : BaseViewModel() {
|
||||
|
||||
val reader = MutableLiveData<Pair<Manga, ReaderMode>>()
|
||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||
private var loadingJob: Job? = null
|
||||
private val currentState = MutableStateFlow(state)
|
||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||
private val chapters = LongSparseArray<MangaChapter>()
|
||||
|
||||
fun init(manga: Manga) {
|
||||
launchLoadingJob {
|
||||
val mode = withContext(Dispatchers.Default) {
|
||||
val repo = manga.source.repository
|
||||
val chapter =
|
||||
(manga.chapters ?: throw RuntimeException("Chapters is null")).random()
|
||||
var mode = dataRepository.getReaderMode(manga.id)
|
||||
if (mode == null) {
|
||||
val pages = repo.getPages(chapter)
|
||||
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
|
||||
mode = getReaderMode(isWebtoon)
|
||||
if (isWebtoon != null) {
|
||||
dataRepository.savePreferences(
|
||||
manga = manga,
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
}
|
||||
mode
|
||||
val readerMode = MutableLiveData<ReaderMode>()
|
||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||
val uiState = combine(
|
||||
mangaData,
|
||||
currentState
|
||||
) { manga, state ->
|
||||
val chapter = state?.chapterId?.let(chapters::get)
|
||||
ReaderUiState(
|
||||
mangaName = manga?.title,
|
||||
chapterName = chapter?.name,
|
||||
chapterNumber = chapter?.number ?: 0,
|
||||
chaptersTotal = chapters.size()
|
||||
)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null))
|
||||
val manga: Manga?
|
||||
get() = mangaData.value
|
||||
|
||||
val readerAnimation = settings.observe()
|
||||
.filter { it == AppSettings.KEY_READER_ANIMATION }
|
||||
.map { settings.readerAnimation }
|
||||
.onStart { emit(settings.readerAnimation) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val onZoomChanged = settings.observe()
|
||||
.filter { it == AppSettings.KEY_ZOOM_MODE }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.asLiveEvent(viewModelScope.coroutineContext)
|
||||
|
||||
init {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = dataRepository.resolveIntent(intent)
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
val repo = manga.source.repository
|
||||
manga = repo.getDetails(manga)
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
reader.value = manga to mode
|
||||
mangaData.value = manga
|
||||
// determine mode
|
||||
val mode =
|
||||
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
|
||||
val pages = repo.getPages(it)
|
||||
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
|
||||
val newMode = getReaderMode(isWebtoon)
|
||||
if (isWebtoon != null) {
|
||||
dataRepository.savePreferences(manga, newMode)
|
||||
}
|
||||
newMode
|
||||
} ?: error("There are no chapters in this manga")
|
||||
// obtain state
|
||||
if (state == null) {
|
||||
currentState.value = historyRepository.getOne(manga)?.let {
|
||||
ReaderState.from(it)
|
||||
} ?: ReaderState.initial(manga)
|
||||
}
|
||||
readerMode.postValue(mode)
|
||||
|
||||
val pages = loadChapter(checkNotNull(manga.chapters?.firstOrNull()).id)
|
||||
content.postValue(ReaderContent(pages, currentState.value))
|
||||
}
|
||||
}
|
||||
|
||||
fun setMode(manga: Manga, mode: ReaderMode) {
|
||||
fun switchMode(newMode: ReaderMode) {
|
||||
launchJob {
|
||||
val manga = checkNotNull(mangaData.value)
|
||||
dataRepository.savePreferences(
|
||||
manga = manga,
|
||||
mode = mode
|
||||
mode = newMode
|
||||
)
|
||||
reader.value = manga to mode
|
||||
readerMode.value = newMode
|
||||
}
|
||||
}
|
||||
|
||||
fun savePage(resolver: ContentResolver, page: MangaPage) {
|
||||
launchJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val repo = page.source.repository
|
||||
val url = repo.getPageFullUrl(page)
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
|
||||
val fileName =
|
||||
URLUtil.guessFileName(
|
||||
url,
|
||||
response.contentDisposition,
|
||||
response.mimeType
|
||||
)
|
||||
MediaStoreCompat.insertImage(resolver, fileName) {
|
||||
response.body!!.byteStream().copyTo(it)
|
||||
}
|
||||
fun saveCurrentState(state: ReaderState? = null) {
|
||||
saveState(
|
||||
mangaData.value ?: return,
|
||||
state ?: currentState.value ?: return
|
||||
)
|
||||
}
|
||||
|
||||
fun getCurrentState() = currentState.value
|
||||
|
||||
fun getCurrentChapterPages(): List<MangaPage>? {
|
||||
val chapterId = currentState.value?.chapterId ?: return null
|
||||
val pages = content.value?.pages ?: return null
|
||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
||||
}
|
||||
|
||||
fun saveCurrentPage(resolver: ContentResolver) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
val page =
|
||||
content.value?.pages?.randomOrNull()?.toMangaPage() ?: return@launchJob //TODO
|
||||
val repo = page.source.repository
|
||||
val url = repo.getPageFullUrl(page)
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
|
||||
val fileName =
|
||||
URLUtil.guessFileName(
|
||||
url,
|
||||
response.contentDisposition,
|
||||
response.mimeType
|
||||
)
|
||||
MediaStoreCompat.insertImage(resolver, fileName) {
|
||||
checkNotNull(response.body).byteStream().copyTo(it)
|
||||
}
|
||||
onPageSaved.postCall(uri)
|
||||
} catch (e: CancellationException) {
|
||||
} catch (e: Exception) {
|
||||
onPageSaved.postCall(null)
|
||||
}
|
||||
onPageSaved.postCall(uri)
|
||||
} catch (e: CancellationException) {
|
||||
} catch (e: Exception) {
|
||||
onPageSaved.postCall(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun switchChapter(id: Long) {
|
||||
val prevJob = loadingJob
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
content.postValue(ReaderContent(emptyList(), null))
|
||||
val newPages = loadChapter(id)
|
||||
content.postValue(ReaderContent(newPages, ReaderState(id, 0, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onCurrentPageChanged(position: Int) {
|
||||
val pages = content.value?.pages ?: return
|
||||
pages.getOrNull(position)?.let {
|
||||
val currentValue = currentState.value
|
||||
if (currentValue != null && currentValue.chapterId != it.chapterId) {
|
||||
currentState.value = currentValue.copy(chapterId = it.chapterId)
|
||||
}
|
||||
}
|
||||
when {
|
||||
loadingJob?.isActive == true -> return
|
||||
pages.isEmpty() -> return
|
||||
position <= BOUNDS_PAGE_OFFSET -> {
|
||||
val chapterId = pages.first().chapterId
|
||||
loadPrevNextChapter(chapterId, -1)
|
||||
}
|
||||
position >= pages.size - BOUNDS_PAGE_OFFSET -> {
|
||||
val chapterId = pages.last().chapterId
|
||||
loadPrevNextChapter(chapterId, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,12 +204,56 @@ class ReaderViewModel(
|
||||
else -> ReaderMode.STANDARD
|
||||
}
|
||||
|
||||
companion object : KoinComponent {
|
||||
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
|
||||
val manga = checkNotNull(mangaData.value) { "Manga is null" }
|
||||
val chapter = checkNotNull(chapters.get(chapterId)) { "Chapter $chapterId not found" }
|
||||
val repo = manga.source.repository
|
||||
return repo.getPages(chapter).mapIndexed { index, page ->
|
||||
ReaderPage.from(page, index, chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveState(state: ReaderState) {
|
||||
private fun loadPrevNextChapter(currentId: Long, delta: Int) {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
val chapters = mangaData.value?.chapters ?: return@launchLoadingJob
|
||||
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
|
||||
val index =
|
||||
if (delta < 0) chapters.indexOfLast(predicate) else chapters.indexOfFirst(predicate)
|
||||
if (index == -1) return@launchLoadingJob
|
||||
val newChapter = chapters.getOrNull(index + delta) ?: return@launchLoadingJob
|
||||
val newPages = loadChapter(newChapter.id)
|
||||
var currentPages = content.value?.pages ?: return@launchLoadingJob
|
||||
// trim pages
|
||||
if (currentPages.size > PAGES_TRIM_THRESHOLD) {
|
||||
val firstChapterId = currentPages.first().chapterId
|
||||
val lastChapterId = currentPages.last().chapterId
|
||||
if (firstChapterId != lastChapterId) {
|
||||
currentPages = when (delta) {
|
||||
1 -> currentPages.dropWhile { it.chapterId == firstChapterId }
|
||||
-1 -> currentPages.dropLastWhile { it.chapterId == lastChapterId }
|
||||
else -> currentPages
|
||||
}
|
||||
}
|
||||
}
|
||||
val pages = when (delta) {
|
||||
0 -> newPages
|
||||
-1 -> newPages + currentPages
|
||||
1 -> currentPages + newPages
|
||||
else -> error("Invalid delta $delta")
|
||||
}
|
||||
content.postValue(ReaderContent(pages, null))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object : KoinComponent {
|
||||
|
||||
const val BOUNDS_PAGE_OFFSET = 2
|
||||
const val PAGES_TRIM_THRESHOLD = 120
|
||||
|
||||
fun saveState(manga: Manga, state: ReaderState) {
|
||||
processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) {
|
||||
get<HistoryRepository>().addOrUpdate(
|
||||
manga = state.manga,
|
||||
manga = manga,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page,
|
||||
scroll = state.scroll
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
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.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderListener
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.utils.ext.associateByLong
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
abstract class AbstractReader<B : ViewBinding> : BaseFragment<B>(), OnBoundsScrollListener {
|
||||
|
||||
protected lateinit var manga: Manga
|
||||
private set
|
||||
private lateinit var chapters: LongSparseArray<MangaChapter>
|
||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
PageLoader()
|
||||
}
|
||||
protected val pages = ArrayDeque<ReaderPage>()
|
||||
protected var readerAdapter: BaseReaderAdapter? = null
|
||||
private set
|
||||
|
||||
val itemsCount: Int
|
||||
get() = readerAdapter?.itemCount ?: 0
|
||||
|
||||
val hasItems: Boolean
|
||||
get() = itemsCount != 0
|
||||
|
||||
val currentPage: MangaPage?
|
||||
get() = pages.getOrNull(getCurrentItem())?.toMangaPage()
|
||||
|
||||
private var readerListener: ReaderListener? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
manga = requireNotNull(requireArguments().getParcelable<ReaderState>(ARG_STATE)).manga
|
||||
chapters = requireNotNull(manga.chapters).associateByLong { it.id }
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
readerAdapter = onCreateAdapter(pages)
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
val state = savedInstanceState?.getParcelable<ReaderState>(ARG_STATE)
|
||||
?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!!
|
||||
loadChapter(state.chapterId) {
|
||||
pages.clear()
|
||||
it.mapIndexedTo(pages) { i, p ->
|
||||
ReaderPage.from(p, i, state.chapterId)
|
||||
}
|
||||
readerAdapter?.notifyDataSetChanged()
|
||||
setCurrentItem(state.page, false)
|
||||
if (state.scroll != 0) {
|
||||
restorePageScroll(state.page, state.scroll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = getCurrentPageScroll()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrolledToStart() {
|
||||
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.addAll(0, it.mapIndexed { i, p ->
|
||||
ReaderPage.from(p, i, prevChapterId)
|
||||
})
|
||||
readerAdapter?.notifyItemsPrepended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
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.addAll(it.mapIndexed { i, p ->
|
||||
ReaderPage.from(p, i, nextChapterId)
|
||||
})
|
||||
readerAdapter?.notifyItemsAppended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
readerAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loader.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun recreateAdapter() {
|
||||
readerAdapter = onCreateAdapter(pages)
|
||||
}
|
||||
|
||||
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() {
|
||||
saveState()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) {
|
||||
viewLifecycleScope.launch {
|
||||
readerListener?.onLoadingStateChanged(isLoading = true)
|
||||
try {
|
||||
val pages = withContext(Dispatchers.Default) {
|
||||
val chapter = chapters.get(chapterId)
|
||||
?: throw RuntimeException("Chapter $chapterId not found")
|
||||
val repo = manga.source.repository
|
||||
repo.getPages(chapter)
|
||||
}
|
||||
callback(pages)
|
||||
} catch (_: CancellationException) {
|
||||
} catch (e: Throwable) {
|
||||
readerListener?.onError(e)
|
||||
} finally {
|
||||
readerListener?.onLoadingStateChanged(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimStart() {
|
||||
/*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
|
||||
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(position: Int) {
|
||||
val page = pages.getOrNull(position) ?: return
|
||||
val chapter = chapters.get(page.chapterId) ?: return
|
||||
readerListener?.onPageChanged(
|
||||
chapter = chapter,
|
||||
page = page.index
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveState() {
|
||||
val page = pages.getOrNull(getCurrentItem()) ?: return
|
||||
readerListener?.saveState(page.chapterId, page.index, getCurrentPageScroll())
|
||||
}
|
||||
|
||||
open fun switchPageBy(delta: Int) {
|
||||
setCurrentItem(getCurrentItem() + delta, true)
|
||||
}
|
||||
|
||||
fun updateState(chapterId: Long = 0, pageId: Long = 0) {
|
||||
val currentChapterId = pages.getOrNull(getCurrentItem())?.chapterId ?: 0L
|
||||
if (chapterId != 0L && chapterId != currentChapterId) {
|
||||
pages.clear()
|
||||
readerAdapter?.notifyDataSetChanged()
|
||||
loadChapter(chapterId) {
|
||||
pages.clear()
|
||||
it.mapIndexedTo(pages) { i, p ->
|
||||
ReaderPage.from(p, i, chapterId)
|
||||
}
|
||||
readerAdapter?.notifyDataSetChanged()
|
||||
setCurrentItem(
|
||||
if (pageId == 0L) {
|
||||
0
|
||||
} else {
|
||||
it.indexOfFirst { x -> x.id == pageId }.coerceAtLeast(0)
|
||||
}, false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getLastPage() = pages.lastOrNull()
|
||||
|
||||
protected open fun getFirstPage() = pages.firstOrNull()
|
||||
|
||||
protected abstract fun getCurrentItem(): Int
|
||||
|
||||
protected abstract fun getCurrentPageScroll(): Int
|
||||
|
||||
protected abstract fun restorePageScroll(position: Int, scroll: Int)
|
||||
|
||||
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
|
||||
|
||||
protected abstract fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter
|
||||
|
||||
protected companion object {
|
||||
|
||||
const val ARG_STATE = "state"
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
|
||||
abstract class BaseReaderAdapter(protected val pages: List<ReaderPage>) :
|
||||
RecyclerView.Adapter<BaseViewHolder<ReaderPage, Unit, *>>() {
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit, *>, position: Int) {
|
||||
val item = pages[position]
|
||||
holder.bind(item, Unit)
|
||||
}
|
||||
|
||||
open fun getItem(position: Int) = pages[position]
|
||||
|
||||
open fun notifyItemsAppended(count: Int) {
|
||||
notifyItemRangeInserted(pages.size - count, count)
|
||||
}
|
||||
|
||||
open fun notifyItemsPrepended(count: Int) {
|
||||
notifyItemRangeInserted(0, count)
|
||||
}
|
||||
|
||||
open fun notifyItemsRemovedStart(count: Int) {
|
||||
notifyItemRangeRemoved(0, count)
|
||||
}
|
||||
|
||||
open fun notifyItemsRemovedEnd(count: Int) {
|
||||
notifyItemRangeRemoved(pages.size - count, count)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = pages[position].id
|
||||
|
||||
final override fun getItemCount() = pages.size
|
||||
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BaseViewHolder<ReaderPage, Unit, *> {
|
||||
return onCreateViewHolder(parent).also(this::onViewHolderCreated)
|
||||
}
|
||||
|
||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<ReaderPage, Unit, *>) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<ReaderPage, Unit, *>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
|
||||
abstract class BasePageHolder<B : ViewBinding>(
|
||||
protected val binding: B,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
||||
|
||||
protected val delegate = PageHolderDelegate(loader, settings, this)
|
||||
|
||||
val context: Context
|
||||
get() = itemView.context
|
||||
|
||||
var boundData: ReaderPage? = null
|
||||
private set
|
||||
|
||||
fun requireData(): ReaderPage {
|
||||
return checkNotNull(boundData) { "Calling requireData() before bind()" }
|
||||
}
|
||||
|
||||
fun bind(data: ReaderPage) {
|
||||
boundData = data
|
||||
onBind(data)
|
||||
}
|
||||
|
||||
protected abstract fun onBind(data: ReaderPage)
|
||||
|
||||
protected open fun onRecycled() = Unit
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
||||
|
||||
protected val viewModel by sharedViewModel<ReaderViewModel>()
|
||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
PageLoader(lifecycleScope, get(), get())
|
||||
}
|
||||
private var lastReaderState: ReaderState? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
lastReaderState = savedInstanceState?.getParcelable(KEY_STATE) ?: lastReaderState
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
onPagesChanged(it.pages, lastReaderState ?: it.state)
|
||||
lastReaderState = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
lastReaderState = getCurrentState()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
getCurrentState()?.let {
|
||||
lastReaderState = it
|
||||
}
|
||||
outState.putParcelable(KEY_STATE, lastReaderState)
|
||||
}
|
||||
|
||||
abstract fun switchPageBy(delta: Int)
|
||||
|
||||
abstract fun switchPageTo(position: Int, smooth: Boolean)
|
||||
|
||||
abstract fun getCurrentState(): ReaderState?
|
||||
|
||||
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
|
||||
|
||||
private companion object {
|
||||
|
||||
const val KEY_STATE = "state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
private val loader: PageLoader,
|
||||
private val settings: AppSettings
|
||||
) : RecyclerView.Adapter<H>() {
|
||||
|
||||
private val differ = AsyncListDiffer(this, DiffCallback())
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: H, position: Int) {
|
||||
holder.bind(differ.currentList[position])
|
||||
}
|
||||
|
||||
open fun getItem(position: Int): ReaderPage = differ.currentList[position]
|
||||
|
||||
open fun getItemOrNull(position: Int) = differ.currentList.getOrNull(position)
|
||||
|
||||
override fun getItemId(position: Int) = differ.currentList[position].id
|
||||
|
||||
final override fun getItemCount() = differ.currentList.size
|
||||
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): H = onCreateViewHolder(parent, loader, settings).also(this::onViewHolderCreated)
|
||||
|
||||
fun setItems(items: List<ReaderPage>, callback: Runnable) {
|
||||
differ.submitList(items, callback)
|
||||
}
|
||||
|
||||
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
|
||||
differ.submitList(items) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onViewHolderCreated(holder: H) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
): H
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
interface OnBoundsScrollListener {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -16,11 +15,11 @@ import java.io.IOException
|
||||
|
||||
class PageHolderDelegate(
|
||||
private val loader: PageLoader,
|
||||
private val settings: AppSettings,
|
||||
private val callback: Callback
|
||||
) : SubsamplingScaleImageView.DefaultOnImageEventListener(),
|
||||
CoroutineScope by loader {
|
||||
|
||||
private val settings by loader.inject<AppSettings>()
|
||||
private var state = State.EMPTY
|
||||
private var job: Job? = null
|
||||
private var file: File? = null
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
data class ReaderUiState(
|
||||
val mangaName: String?,
|
||||
val chapterName: String?,
|
||||
val chapterNumber: Int,
|
||||
val chaptersTotal: Int
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.reversed
|
||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.view.View
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -1,13 +1,18 @@
|
||||
package org.koitharu.kotatsu.reader.ui.reversed
|
||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.view.ViewGroup
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.standard.PageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||
|
||||
class ReversedPageHolder(parent: ViewGroup, loader: PageLoader) : PageHolder(parent, loader) {
|
||||
class ReversedPageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : PageHolder(binding, loader, settings) {
|
||||
|
||||
override fun onImageShowing(zoom: ZoomMode) {
|
||||
with(binding.ssiv) {
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class ReversedPagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) = ReversedPageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
|
||||
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
private var pagerAdapter: ReversedPagesAdapter? = null
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagerAdapter = ReversedPagesAdapter(loader, get())
|
||||
with(binding.pager) {
|
||||
adapter = pagerAdapter
|
||||
offscreenPageLimit = 2
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
pagerAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
with(binding.pager) {
|
||||
setCurrentItem(currentItem - delta, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||
binding.pager.setCurrentItem(reversed(position), smooth)
|
||||
}
|
||||
|
||||
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||
pagerAdapter?.setItems(pages.asReversed()) {
|
||||
if (pendingState != null) {
|
||||
val position = pages.indexOfFirst {
|
||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||
}
|
||||
if (position == -1) return@setItems
|
||||
binding.pager.setCurrentItem(position, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
||||
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
||||
ReaderState(
|
||||
chapterId = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(reversed(page))
|
||||
}
|
||||
|
||||
private fun reversed(position: Int): Int {
|
||||
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.standard
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.view.View
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -1,35 +1,32 @@
|
||||
package org.koitharu.kotatsu.reader.ui.standard
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.PageHolderDelegate
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
open class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
||||
BaseViewHolder<ReaderPage, Unit, ItemPageBinding>(
|
||||
ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
), PageHolderDelegate.Callback, View.OnClickListener {
|
||||
|
||||
private val delegate = PageHolderDelegate(loader, this)
|
||||
open class PageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings), View.OnClickListener {
|
||||
|
||||
init {
|
||||
binding.ssiv.setOnImageEventListener(delegate)
|
||||
binding.buttonRetry.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.reader.ui.standard
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||
|
||||
class PagerPaginationListener(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.async
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
private var pagesAdapter: PagesAdapter? = null
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagesAdapter = PagesAdapter(loader, get())
|
||||
with(binding.pager) {
|
||||
adapter = pagesAdapter
|
||||
offscreenPageLimit = 2
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
viewModel.readerAnimation.observe(viewLifecycleOwner) {
|
||||
val transformer = if (it) PageAnimTransformer() else null
|
||||
binding.pager.setPageTransformer(transformer)
|
||||
}
|
||||
viewModel.onZoomChanged.observe(viewLifecycleOwner) {
|
||||
pagesAdapter = PagesAdapter(loader, get())
|
||||
binding.pager.swapAdapter(pagesAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
pagesAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||
viewLifecycleScope.launchWhenCreated {
|
||||
val items = async {
|
||||
pagesAdapter?.setItems(pages)
|
||||
}
|
||||
if (pendingState != null) {
|
||||
val position = pages.indexOfFirst {
|
||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||
}
|
||||
items.await() ?: return@launchWhenCreated
|
||||
if (position != -1) {
|
||||
binding.pager.setCurrentItem(position, false)
|
||||
}
|
||||
} else {
|
||||
items.await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
with(binding.pager) {
|
||||
setCurrentItem(currentItem + delta, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||
binding.pager.setCurrentItem(position, smooth)
|
||||
}
|
||||
|
||||
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
||||
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
||||
ReaderState(
|
||||
chapterId = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class PagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) = PageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||
|
||||
class ListPaginationListener(
|
||||
private val offset: Int,
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class WebtoonAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) = WebtoonHolder(
|
||||
binding = ItemPageWebtoonBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
loader = loader,
|
||||
settings = settings
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,28 +1,26 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.PageHolderDelegate
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
|
||||
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
BaseViewHolder<ReaderPage, Unit, ItemPageWebtoonBinding>(
|
||||
ItemPageWebtoonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
), PageHolderDelegate.Callback, View.OnClickListener {
|
||||
class WebtoonHolder(
|
||||
binding: ItemPageWebtoonBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings), View.OnClickListener {
|
||||
|
||||
private val delegate = PageHolderDelegate(loader, this)
|
||||
private var scrollToRestore = 0
|
||||
|
||||
init {
|
||||
@@ -30,7 +28,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
binding.buttonRetry.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import kotlinx.coroutines.async
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
|
||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||
private var webtoonAdapter: WebtoonAdapter? = null
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
webtoonAdapter = WebtoonAdapter(loader, get())
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = webtoonAdapter
|
||||
doOnCurrentItemChanged(::notifyPageChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
webtoonAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||
viewLifecycleScope.launchWhenCreated {
|
||||
val setItems = async { webtoonAdapter?.setItems(pages) }
|
||||
if (pendingState != null) {
|
||||
val position = pages.indexOfFirst {
|
||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||
}
|
||||
setItems.await() ?: return@launchWhenCreated
|
||||
if (position != -1) {
|
||||
binding.recyclerView.firstItem = position
|
||||
// TODO check
|
||||
(binding.recyclerView.findViewHolderForAdapterPosition(position) as? WebtoonHolder)
|
||||
?.restoreScroll(pendingState.scroll)
|
||||
}
|
||||
} else {
|
||||
setItems.await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||
val currentItem = recyclerView.findCenterViewPosition()
|
||||
val adapter = recyclerView.adapter as? BaseReaderAdapter<*>
|
||||
val page = adapter?.getItemOrNull(currentItem) ?: return@run null
|
||||
ReaderState(
|
||||
chapterId = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = (recyclerView.findViewHolderForAdapterPosition(currentItem) as? WebtoonHolder)
|
||||
?.getScrollY() ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
binding.recyclerView.smoothScrollBy(
|
||||
0,
|
||||
(binding.recyclerView.height * 0.9).toInt() * delta,
|
||||
scrollInterpolator
|
||||
)
|
||||
}
|
||||
|
||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||
if (smooth) {
|
||||
binding.recyclerView.smoothScrollToPosition(position)
|
||||
} else {
|
||||
binding.recyclerView.firstItem = position
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.reversed
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
|
||||
class ReversedPagesAdapter(
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = ReversedPageHolder(parent, loader)
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit, *>, position: Int) {
|
||||
super.onBindViewHolder(holder, reversed(position))
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): ReaderPage {
|
||||
return super.getItem(reversed(position))
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return super.getItemId(reversed(position))
|
||||
}
|
||||
|
||||
override fun notifyItemsAppended(count: Int) {
|
||||
super.notifyItemsPrepended(count)
|
||||
}
|
||||
|
||||
override fun notifyItemsPrepended(count: Int) {
|
||||
super.notifyItemsAppended(count)
|
||||
}
|
||||
|
||||
override fun notifyItemsRemovedStart(count: Int) {
|
||||
super.notifyItemsRemovedEnd(count)
|
||||
}
|
||||
|
||||
override fun notifyItemsRemovedEnd(count: Int) {
|
||||
super.notifyItemsRemovedStart(count)
|
||||
}
|
||||
|
||||
private fun reversed(position: Int) = pages.size - position - 1
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.reversed
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.standard.PageAnimTransformer
|
||||
import org.koitharu.kotatsu.reader.ui.standard.PagerPaginationListener
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class ReversedReaderFragment : AbstractReader<FragmentReaderStandardBinding>(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private var paginationListener: PagerPaginationListener? = null
|
||||
private val settings by inject<AppSettings>()
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
paginationListener = PagerPaginationListener(readerAdapter!!, 2, this)
|
||||
with(binding.pager) {
|
||||
adapter = readerAdapter
|
||||
if (settings.readerAnimation) {
|
||||
setPageTransformer(ReversedPageAnimTransformer())
|
||||
}
|
||||
offscreenPageLimit = 2
|
||||
registerOnPageChangeCallback(paginationListener!!)
|
||||
doOnPageChanged {
|
||||
notifyPageChanged(reversed(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
paginationListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return ReversedPagesAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override fun recreateAdapter() {
|
||||
super.recreateAdapter()
|
||||
binding.pager.swapAdapter(readerAdapter)
|
||||
}
|
||||
|
||||
override fun getCurrentItem() = reversed(binding.pager.currentItem)
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
binding.pager.setCurrentItem(reversed(position), isSmooth)
|
||||
}
|
||||
|
||||
override fun getCurrentPageScroll() = 0
|
||||
|
||||
override fun restorePageScroll(position: Int, scroll: Int) = Unit
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_READER_ANIMATION -> {
|
||||
if (settings.readerAnimation) {
|
||||
binding.pager.setPageTransformer(PageAnimTransformer())
|
||||
} else {
|
||||
binding.pager.setPageTransformer(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLastPage() = pages.firstOrNull()
|
||||
|
||||
override fun getFirstPage() = pages.lastOrNull()
|
||||
|
||||
private fun reversed(position: Int) = (itemsCount - position - 1).coerceAtLeast(0)
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(state: ReaderState) = ReversedReaderFragment().withArgs(1) {
|
||||
putParcelable(ARG_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.standard
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class PagerReaderFragment : AbstractReader<FragmentReaderStandardBinding>(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private var paginationListener: PagerPaginationListener? = null
|
||||
private val settings by inject<AppSettings>()
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
paginationListener = PagerPaginationListener(readerAdapter!!, 2, this)
|
||||
with(binding.pager) {
|
||||
adapter = readerAdapter
|
||||
if (settings.readerAnimation) {
|
||||
setPageTransformer(PageAnimTransformer())
|
||||
}
|
||||
offscreenPageLimit = 2
|
||||
registerOnPageChangeCallback(paginationListener!!)
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
paginationListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return PagesAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override fun recreateAdapter() {
|
||||
super.recreateAdapter()
|
||||
binding.pager.swapAdapter(readerAdapter)
|
||||
}
|
||||
|
||||
override fun getCurrentItem() = binding.pager.currentItem
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
binding.pager.setCurrentItem(position, isSmooth)
|
||||
}
|
||||
|
||||
override fun getCurrentPageScroll() = 0
|
||||
|
||||
override fun restorePageScroll(position: Int, scroll: Int) = Unit
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_READER_ANIMATION -> {
|
||||
if (settings.readerAnimation) {
|
||||
binding.pager.setPageTransformer(PageAnimTransformer())
|
||||
} else {
|
||||
binding.pager.setPageTransformer(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {
|
||||
putParcelable(ARG_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.standard
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
|
||||
class PagesAdapter(
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
|
||||
data class PageThumbnail(
|
||||
val number: Int,
|
||||
val isCurrent: Boolean,
|
||||
val repository: MangaRepository,
|
||||
val page: MangaPage
|
||||
)
|
||||
@@ -14,9 +14,10 @@ import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
@@ -24,34 +25,58 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
class PagesThumbnailsSheet : BaseBottomSheet<SheetPagesBinding>(),
|
||||
OnListItemClickListener<MangaPage> {
|
||||
|
||||
private lateinit var thumbnails: List<PageThumbnail>
|
||||
private val spanResolver = MangaListSpanResolver()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pages = arguments?.getParcelableArrayList<MangaPage>(ARG_PAGES)
|
||||
if (pages.isNullOrEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val current = arguments?.getInt(ARG_CURRENT, -1) ?: -1
|
||||
val repository = pages.first().source.repository
|
||||
thumbnails = pages.mapIndexed { i, x ->
|
||||
PageThumbnail(
|
||||
number = i + 1,
|
||||
isCurrent = i == current,
|
||||
repository = repository,
|
||||
page = x
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
||||
return SheetPagesBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.recyclerView.addItemDecoration(SpacingItemDecoration(view.resources.resolveDp(8)))
|
||||
val pages = arguments?.getParcelableArrayList<MangaPage>(ARG_PAGES)
|
||||
if (pages == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
with(binding.recyclerView) {
|
||||
addItemDecoration(SpacingItemDecoration(view.resources.resolveDp(8)))
|
||||
adapter = PageThumbnailAdapter(
|
||||
thumbnails,
|
||||
get(),
|
||||
viewLifecycleScope,
|
||||
get(),
|
||||
this@PagesThumbnailsSheet
|
||||
)
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
spanResolver.setGridSize(get<AppSettings>().gridSize / 100f, this)
|
||||
}
|
||||
binding.recyclerView.adapter =
|
||||
PageThumbnailAdapter(get(), viewLifecycleScope, get(), this).apply {
|
||||
items = pages
|
||||
}
|
||||
|
||||
val title = arguments?.getString(ARG_TITLE)
|
||||
binding.toolbar.title = title
|
||||
binding.toolbar.setNavigationOnClickListener { dismiss() }
|
||||
binding.toolbar.subtitle =
|
||||
resources.getQuantityString(R.plurals.pages, pages.size, pages.size)
|
||||
resources.getQuantityString(R.plurals.pages, thumbnails.size, thumbnails.size)
|
||||
binding.textViewTitle.text = title
|
||||
if (dialog !is BottomSheetDialog) {
|
||||
binding.toolbar.isVisible = true
|
||||
binding.textViewTitle.isVisible = false
|
||||
binding.appbar.elevation = resources.getDimension(R.dimen.elevation_large)
|
||||
}
|
||||
binding.recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) =
|
||||
@@ -77,11 +102,6 @@ class PagesThumbnailsSheet : BaseBottomSheet<SheetPagesBinding>(),
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.recyclerView.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaPage, view: View) {
|
||||
((parentFragment as? OnPageSelectListener)
|
||||
?: (activity as? OnPageSelectListener))?.run {
|
||||
@@ -94,13 +114,15 @@ class PagesThumbnailsSheet : BaseBottomSheet<SheetPagesBinding>(),
|
||||
|
||||
private const val ARG_PAGES = "pages"
|
||||
private const val ARG_TITLE = "title"
|
||||
private const val ARG_CURRENT = "current"
|
||||
|
||||
private const val TAG = "PagesThumbnailsSheet"
|
||||
|
||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String) =
|
||||
PagesThumbnailsSheet().withArgs(2) {
|
||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
|
||||
PagesThumbnailsSheet().withArgs(3) {
|
||||
putParcelableArrayList(ARG_PAGES, ArrayList<MangaPage>(pages))
|
||||
putString(ARG_TITLE, title)
|
||||
putInt(ARG_CURRENT, currentPage)
|
||||
}.show(fm, TAG)
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
|
||||
|
||||
fun pageThumbnailAD(
|
||||
@@ -18,7 +19,7 @@ fun pageThumbnailAD(
|
||||
scope: CoroutineScope,
|
||||
cache: PagesCache,
|
||||
clickListener: OnListItemClickListener<MangaPage>
|
||||
) = adapterDelegateViewBinding<MangaPage, MangaPage, ItemPageThumbBinding>(
|
||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
@@ -30,16 +31,19 @@ fun pageThumbnailAD(
|
||||
)
|
||||
|
||||
binding.handle.setOnClickListener {
|
||||
clickListener.onItemClick(item, itemView)
|
||||
clickListener.onItemClick(item.page, itemView)
|
||||
}
|
||||
|
||||
bind {
|
||||
job?.cancel()
|
||||
binding.imageViewThumb.setImageDrawable(null)
|
||||
binding.textViewNumber.text = (bindingAdapterPosition + 1).toString()
|
||||
with(binding.textViewNumber) {
|
||||
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_default)
|
||||
text = (item.number).toString()
|
||||
}
|
||||
job = scope.launch(Dispatchers.Default + IgnoreErrors) {
|
||||
val url = item.preview ?: item.url.let {
|
||||
val pageUrl = item.source.repository.getPageFullUrl(item)
|
||||
val url = item.page.preview ?: item.page.url.let {
|
||||
val pageUrl = item.repository.getPageFullUrl(item.page)
|
||||
cache[pageUrl]?.toUri()?.toString() ?: pageUrl
|
||||
}
|
||||
val drawable = coil.execute(
|
||||
|
||||
@@ -6,15 +6,18 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
|
||||
class PageThumbnailAdapter(
|
||||
dataSet: List<PageThumbnail>,
|
||||
coil: ImageLoader,
|
||||
scope: CoroutineScope,
|
||||
cache: PagesCache,
|
||||
clickListener: OnListItemClickListener<MangaPage>
|
||||
) : ListDelegationAdapter<List<MangaPage>>() {
|
||||
) : ListDelegationAdapter<List<PageThumbnail>>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, cache, clickListener))
|
||||
setItems(dataSet)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
|
||||
class WebtoonAdapter(
|
||||
pages: List<ReaderPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter(pages) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = WebtoonHolder(parent, loader)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class WebtoonReaderFragment : AbstractReader<FragmentReaderWebtoonBinding>() {
|
||||
|
||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||
private var paginationListener: ListPaginationListener? = null
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
paginationListener = ListPaginationListener(2, this)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = readerAdapter
|
||||
addOnScrollListener(paginationListener!!)
|
||||
doOnCurrentItemChanged(::notifyPageChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
||||
return WebtoonAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override fun recreateAdapter() {
|
||||
super.recreateAdapter()
|
||||
binding.recyclerView.swapAdapter(readerAdapter, true)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
paginationListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun getCurrentItem(): Int {
|
||||
return binding.recyclerView.findCenterViewPosition()
|
||||
}
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
if (isSmooth) {
|
||||
binding.recyclerView.smoothScrollToPosition(position)
|
||||
} else {
|
||||
binding.recyclerView.firstItem = position
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
binding.recyclerView.smoothScrollBy(
|
||||
0,
|
||||
(binding.recyclerView.height * 0.9).toInt() * delta,
|
||||
scrollInterpolator
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCurrentPageScroll(): Int {
|
||||
return (binding.recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder)
|
||||
?.getScrollY() ?: 0
|
||||
}
|
||||
|
||||
override fun restorePageScroll(position: Int, scroll: Int) {
|
||||
binding.recyclerView.post {
|
||||
val holder = binding.recyclerView.findViewHolderForAdapterPosition(position) ?: return@post
|
||||
(holder as WebtoonHolder).restoreScroll(scroll)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(state: ReaderState) = WebtoonReaderFragment().withArgs(1) {
|
||||
putParcelable(ARG_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.koitharu.kotatsu.remotelist.ui
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
@@ -16,6 +15,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaFilterConfig
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import java.util.*
|
||||
|
||||
class RemoteListViewModel(
|
||||
@@ -49,9 +49,7 @@ class RemoteListViewModel(
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
loadList(false)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package org.koitharu.kotatsu.search.ui
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import java.util.*
|
||||
|
||||
class SearchViewModel(
|
||||
@@ -46,9 +46,7 @@ class SearchViewModel(
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
loadList(append = false)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.search.ui.global
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.util.*
|
||||
|
||||
@@ -46,9 +46,7 @@ class GlobalSearchViewModel(
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
|
||||
@@ -2,20 +2,20 @@ package org.koitharu.kotatsu.tracker.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
|
||||
class FeedViewModel(
|
||||
@@ -35,9 +35,7 @@ class FeedViewModel(
|
||||
hasNextPage
|
||||
) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + LoadingFooter else list
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
loadList(append = false)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
fun interface BufferedObserver<T> {
|
||||
|
||||
fun onChanged(t: T, previous: T?)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class FlowLiveEvent<T>(
|
||||
private val source: Flow<T>,
|
||||
private val context: CoroutineContext
|
||||
) : LiveData<T>() {
|
||||
|
||||
private val scope = CoroutineScope(
|
||||
Dispatchers.Main.immediate + context + SupervisorJob(context[Job])
|
||||
)
|
||||
private val pending = AtomicBoolean(false)
|
||||
private var collectJob: Job? = null
|
||||
|
||||
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||
super.observe(owner) {
|
||||
if (pending.compareAndSet(true, false)) {
|
||||
observer.onChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
if (collectJob == null) {
|
||||
collectJob = source.onEach {
|
||||
setValue(it)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
collectJob?.cancel()
|
||||
collectJob = null
|
||||
super.onInactive()
|
||||
}
|
||||
|
||||
override fun setValue(value: T) {
|
||||
pending.set(true)
|
||||
super.setValue(value)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ object UiUtils : KoinComponent {
|
||||
|
||||
fun isTablet(context: Context) = context.resources.getBoolean(R.bool.is_tablet)
|
||||
|
||||
@Deprecated("Use MangaListSpanResolver")
|
||||
object SpanCountResolver : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
v: View?, left: Int, top: Int, right: Int, bottom: Int,
|
||||
|
||||
@@ -3,6 +3,13 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
||||
import org.koitharu.kotatsu.utils.FlowLiveEvent
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun <T> LiveData<T?>.observeNotNull(owner: LifecycleOwner, observer: Observer<T>) {
|
||||
this.observe(owner) {
|
||||
@@ -10,4 +17,28 @@ fun <T> LiveData<T?>.observeNotNull(owner: LifecycleOwner, observer: Observer<T>
|
||||
observer.onChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver<T>) {
|
||||
var previous: T? = null
|
||||
this.observe(owner) {
|
||||
observer.onChanged(it, previous)
|
||||
previous = it
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.asLiveData(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
defaultValue: T
|
||||
): LiveData<T> = liveData(context) {
|
||||
if (latestValue == null) {
|
||||
emit(defaultValue)
|
||||
}
|
||||
collect {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.asLiveEvent(
|
||||
context: CoroutineContext = EmptyCoroutineContext
|
||||
): LiveData<T> = FlowLiveEvent(this, context)
|
||||
@@ -206,6 +206,6 @@ fun ViewPager2.swapAdapter(newAdapter: RecyclerView.Adapter<*>?) {
|
||||
val position = currentItem
|
||||
adapter = newAdapter
|
||||
if (adapter != null && position != RecyclerView.NO_POSITION) {
|
||||
currentItem = position
|
||||
setCurrentItem(position, false)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
|
||||
@@ -26,7 +27,7 @@ class ShelfConfigViewModel(
|
||||
CategoryItem(it.id, it.title, selectedId == it.id)
|
||||
}
|
||||
list
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
var checkedId: Long by selectedCategoryId::value
|
||||
}
|
||||
Reference in New Issue
Block a user