From 904d12f611f885a7c2dd8b4356ca5810cf07fe5e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 16 Dec 2020 08:25:43 +0200 Subject: [PATCH] Refactor reader --- app/build.gradle | 6 +- .../koitharu/kotatsu/base/ui/BaseFragment.kt | 8 +- .../kotatsu/base/ui/list/BaseViewHolder.kt | 1 + .../kotatsu/details/ui/ChaptersFragment.kt | 5 +- .../kotatsu/details/ui/DetailsActivity.kt | 10 +- .../kotatsu/details/ui/DetailsFragment.kt | 8 +- .../kotatsu/details/ui/DetailsViewModel.kt | 17 +- .../ui/FavouritesContainerFragment.kt | 11 +- .../FavouritesCategoriesViewModel.kt | 3 +- .../select/MangaCategoriesViewModel.kt | 3 +- .../ui/list/FavouritesListViewModel.kt | 8 +- .../history/ui/HistoryListViewModel.kt | 6 +- .../kotatsu/list/ui/MangaListViewModel.kt | 3 +- .../kotatsu/local/ui/LocalListViewModel.kt | 3 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 6 +- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 19 +- .../main/ui/protect/AppProtectHelper.kt | 1 + .../kotatsu/reader/ReaderControlDelegate.kt | 95 ++++++ .../koitharu/kotatsu/reader/ReaderModule.kt | 6 +- .../koitharu/kotatsu/reader/ui/PageLoader.kt | 22 +- .../kotatsu/reader/ui/ReaderActivity.kt | 277 ++++++------------ .../kotatsu/reader/ui/ReaderConfigDialog.kt | 14 +- .../kotatsu/reader/ui/ReaderContent.kt | 8 + .../kotatsu/reader/ui/ReaderListener.kt | 14 - .../koitharu/kotatsu/reader/ui/ReaderState.kt | 20 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 250 ++++++++++++---- .../kotatsu/reader/ui/base/AbstractReader.kt | 267 ----------------- .../reader/ui/base/BaseReaderAdapter.kt | 52 ---- .../kotatsu/reader/ui/pager/BasePageHolder.kt | 35 +++ .../kotatsu/reader/ui/pager/BaseReader.kt | 57 ++++ .../reader/ui/pager/BaseReaderAdapter.kt | 69 +++++ .../{base => pager}/OnBoundsScrollListener.kt | 2 +- .../ui/{base => pager}/PageHolderDelegate.kt | 5 +- .../reader/ui/{base => pager}/ReaderPage.kt | 2 +- .../kotatsu/reader/ui/pager/ReaderUiState.kt | 8 + .../reversed/ReversedPageAnimTransformer.kt | 2 +- .../reversed/ReversedPageHolder.kt | 13 +- .../ui/pager/reversed/ReversedPagesAdapter.kt | 24 ++ .../pager/reversed/ReversedReaderFragment.kt | 78 +++++ .../standard/PageAnimTransformer.kt | 2 +- .../ui/{ => pager}/standard/PageHolder.kt | 23 +- .../standard/PagerPaginationListener.kt | 4 +- .../ui/pager/standard/PagerReaderFragment.kt | 93 ++++++ .../reader/ui/pager/standard/PagesAdapter.kt | 24 ++ .../wetoon/ListPaginationListener.kt | 4 +- .../reader/ui/pager/wetoon/WebtoonAdapter.kt | 28 ++ .../{ => pager}/wetoon/WebtoonFrameLayout.kt | 2 +- .../ui/{ => pager}/wetoon/WebtoonHolder.kt | 22 +- .../ui/{ => pager}/wetoon/WebtoonImageView.kt | 2 +- .../ui/pager/wetoon/WebtoonReaderFragment.kt | 96 ++++++ .../{ => pager}/wetoon/WebtoonRecyclerView.kt | 2 +- .../ui/reversed/ReversedPagesAdapter.kt | 45 --- .../ui/reversed/ReversedReaderFragment.kt | 107 ------- .../reader/ui/standard/PagerReaderFragment.kt | 97 ------ .../reader/ui/standard/PagesAdapter.kt | 14 - .../reader/ui/thumbnails/PageThumbnail.kt | 11 + .../ui/thumbnails/PagesThumbnailsSheet.kt | 60 ++-- .../ui/thumbnails/adapter/PageThumbnailAD.kt | 14 +- .../adapter/PageThumbnailAdapter.kt | 5 +- .../reader/ui/wetoon/WebtoonAdapter.kt | 14 - .../reader/ui/wetoon/WebtoonReaderFragment.kt | 91 ------ .../remotelist/ui/RemoteListViewModel.kt | 8 +- .../kotatsu/search/ui/SearchViewModel.kt | 8 +- .../search/ui/global/GlobalSearchViewModel.kt | 6 +- .../kotatsu/tracker/ui/FeedViewModel.kt | 8 +- .../kotatsu/utils/BufferedObserver.kt | 6 + .../koitharu/kotatsu/utils/FlowLiveEvent.kt | 54 ++++ .../org/koitharu/kotatsu/utils/UiUtils.kt | 1 + .../koitharu/kotatsu/utils/ext/LiveDataExt.kt | 33 ++- .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 2 +- .../widget/shelf/ShelfConfigViewModel.kt | 3 +- .../main/res/layout/dialog_reader_config.xml | 17 +- .../res/layout/fragment_reader_webtoon.xml | 2 +- app/src/main/res/layout/item_page_thumb.xml | 2 +- app/src/main/res/layout/item_page_webtoon.xml | 6 +- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 4 + app/src/main/res/values/themes.xml | 1 + build.gradle | 2 +- 80 files changed, 1235 insertions(+), 1128 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ReaderControlDelegate.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderListener.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/base/AbstractReader.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/base/BaseReaderAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{base => pager}/OnBoundsScrollListener.kt (65%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{base => pager}/PageHolderDelegate.kt (94%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{base => pager}/ReaderPage.kt (93%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/reversed/ReversedPageAnimTransformer.kt (92%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/reversed/ReversedPageHolder.kt (76%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/standard/PageAnimTransformer.kt (92%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/standard/PageHolder.kt (79%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/standard/PagerPaginationListener.kt (86%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/wetoon/ListPaginationListener.kt (90%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/wetoon/WebtoonFrameLayout.kt (91%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/wetoon/WebtoonHolder.kt (74%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/wetoon/WebtoonImageView.kt (96%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/{ => pager}/wetoon/WebtoonRecyclerView.kt (97%) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPagesAdapter.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedReaderFragment.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerReaderFragment.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagesAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonAdapter.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonReaderFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveEvent.kt diff --git a/app/build.gradle b/app/build.gradle index c58662ce1..f6e035042 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,8 +67,8 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'androidx.core:core-ktx:1.5.0-alpha05' - implementation 'androidx.activity:activity-ktx:1.2.0-beta01' - implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01' + implementation 'androidx.activity:activity-ktx:1.2.0-beta02' + implementation 'androidx.fragment:fragment-ktx:1.3.0-beta02' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01' @@ -76,7 +76,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.3.0-beta01' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06' + implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.4.0' diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt index a70c514fd..7288702b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt @@ -32,12 +32,14 @@ abstract class BaseFragment : 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 } } -} \ No newline at end of file + + protected fun bindingOrNull() = viewBinding + + protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BaseViewHolder.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BaseViewHolder.kt index cdbbd00d5..b878524ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BaseViewHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BaseViewHolder.kt @@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import org.koin.core.component.KoinComponent +@Deprecated("") abstract class BaseViewHolder protected constructor(val binding: B) : RecyclerView.ViewHolder(binding.root), KoinComponent { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index d6310eae6..8b5bbbe2d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -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(), OnListItemClickListener, ActionMode.Callback { @@ -84,9 +85,9 @@ class ChaptersFragment : BaseFragment(), ) startActivity( ReaderActivity.newIntent( - context ?: return, + view.context, viewModel.manga.value ?: return, - item.id + ReaderState(item.id, 0, 0) ), options.toBundle() ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 8d5ade675..623bcabc5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -209,12 +209,14 @@ class DetailsActivity : BaseActivity(), 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 35538c847..bca8d423a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -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(), View.OnClickList ReaderActivity.newIntent( context ?: return, manga ?: return, - viewModel.readingHistory.value + null ) ) } @@ -157,7 +158,10 @@ class DetailsFragment : BaseFragment(), 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 23642ba34..7eb584a18 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -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() @@ -76,7 +77,7 @@ class DetailsViewModel( } ) } - }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) + }.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext) init { launchLoadingJob(Dispatchers.Default) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index de1ec4435..907601d1a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -30,7 +30,6 @@ class FavouritesContainerFragment : BaseFragment(), 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(), 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(), 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(), override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) + adapterState = (bindingOrNull()?.pager?.adapter as? FavouritesPagerAdapter)?.saveState() + ?: adapterState outState.putParcelable(KEY_ADAPTER_STATE, adapterState) } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 557515cba..79e19fa58 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt index ced611e9c..663a8ffdb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 9ba9ff730..b42e99a58 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index af32db57b..74f8b58d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index a80d21432..4e85fff89 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 9faa943e2..1f7394022 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -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() diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index be159eb81..c6a988383 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -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(), 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(), 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) { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index bcd53035e..9fdd7888a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -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() + val onOpenReader = SingleLiveEvent() 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) } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt index be8ba3cff..3b622b771 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt @@ -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() diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderControlDelegate.kt new file mode 100644 index 000000000..c0306258a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderControlDelegate.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index edb784918..3d26814e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -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()) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLoader.kt index 7c42c05da..b671bce5a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLoader.kt @@ -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>() - private val okHttp by inject() - private val cache by inject() 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() - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 042060bff..e1d38dd08 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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(), ChaptersDialog.OnChapterChangeListener, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback, - ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener, - ActivityResultCallback, OnApplyWindowInsetsListener { + ActivityResultCallback, OnApplyWindowInsetsListener, + ReaderControlDelegate.OnInteractionListener { - private val viewModel by viewModel() - private val settings by inject() - - lateinit var state: ReaderState - private set + private val viewModel by viewModel { + parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra(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(), 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(EXTRA_STATE) - ?: intent.getParcelableExtra(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(), } } - 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(), 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(), 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(), 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(), } 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(), 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(), } } - 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(), .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(), 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt index 5201e489f..42824267d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt @@ -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(), 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(), 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt new file mode 100644 index 000000000..917e01d06 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.reader.ui + +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage + +data class ReaderContent( + val pages: List, + val state: ReaderState? +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderListener.kt deleted file mode 100644 index 777bd8c48..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderListener.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt index 0fd78b849..daf63e3ff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt @@ -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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 4c24311b0..0b610fb42 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -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>() + private var loadingJob: Job? = null + private val currentState = MutableStateFlow(state) + private val mangaData = MutableStateFlow(intent.manga) + private val chapters = LongSparseArray() + + val readerMode = MutableLiveData() val onPageSaved = SingleLiveEvent() + 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(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) - 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 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) + } + 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) - mode = getReaderMode(isWebtoon) + val newMode = getReaderMode(isWebtoon) if (isWebtoon != null) { - dataRepository.savePreferences( - manga = manga, - mode = mode - ) + dataRepository.savePreferences(manga, newMode) } - } - mode + 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) } - reader.value = manga to mode + 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().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? { + 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().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 { + 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) + } + } + + 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(state: ReaderState) { + fun saveState(manga: Manga, state: ReaderState) { processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) { get().addOrUpdate( - manga = state.manga, + manga = manga, chapterId = state.chapterId, page = state.page, scroll = state.scroll diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/AbstractReader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/AbstractReader.kt deleted file mode 100644 index 834c75f1b..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/AbstractReader.kt +++ /dev/null @@ -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 : BaseFragment(), OnBoundsScrollListener { - - protected lateinit var manga: Manga - private set - private lateinit var chapters: LongSparseArray - protected val loader by lazy(LazyThreadSafetyMode.NONE) { - PageLoader() - } - protected val pages = ArrayDeque() - 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(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(ARG_STATE) - ?: requireArguments().getParcelable(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? { - 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) -> 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): BaseReaderAdapter - - protected companion object { - - const val ARG_STATE = "state" - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/BaseReaderAdapter.kt deleted file mode 100644 index 33385d34c..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/BaseReaderAdapter.kt +++ /dev/null @@ -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) : - RecyclerView.Adapter>() { - - init { - @Suppress("LeakingThis") - setHasStableIds(true) - } - - override fun onBindViewHolder(holder: BaseViewHolder, 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 { - return onCreateViewHolder(parent).also(this::onViewHolderCreated) - } - - protected open fun onViewHolderCreated(holder: BaseViewHolder) = Unit - - protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt new file mode 100644 index 000000000..7d1bed977 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt @@ -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( + 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 +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt new file mode 100644 index 000000000..39e955962 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt @@ -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 : BaseFragment() { + + protected val viewModel by sharedViewModel() + 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, pendingState: ReaderState?) + + private companion object { + + const val KEY_STATE = "state" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt new file mode 100644 index 000000000..67f1781de --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt @@ -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>( + private val loader: PageLoader, + private val settings: AppSettings +) : RecyclerView.Adapter() { + + 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, callback: Runnable) { + differ.submitList(items, callback) + } + + suspend fun setItems(items: List) = suspendCoroutine { 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() { + + override fun areItemsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean { + return oldItem == newItem + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/OnBoundsScrollListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/base/OnBoundsScrollListener.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt index 44afcd6a2..a275908fb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/OnBoundsScrollListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.base +package org.koitharu.kotatsu.reader.ui.pager interface OnBoundsScrollListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/base/PageHolderDelegate.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 5c96fb65e..dfc4b2295 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/PageHolderDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -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() private var state = State.EMPTY private var job: Job? = null private var file: File? = null diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/ReaderPage.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/base/ReaderPage.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt index a7dacac10..24b24e41d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/base/ReaderPage.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt new file mode 100644 index 000000000..be72b896c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageAnimTransformer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageAnimTransformer.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt index f572c4616..1da4a9d39 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageAnimTransformer.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageHolder.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt index a00df4490..342df54b5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt @@ -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) { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt new file mode 100644 index 000000000..2562b54e5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt @@ -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(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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt new file mode 100644 index 000000000..4932c4a22 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt @@ -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() { + + 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, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageAnimTransformer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageAnimTransformer.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt index 6b8405327..cd03ce8eb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageAnimTransformer.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageHolder.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index 3b59a7a3d..e53fef51b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -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( - 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(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()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerPaginationListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerPaginationListener.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt index d8bad45b1..1f9c40cbf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerPaginationListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt @@ -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<*>, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt new file mode 100644 index 000000000..460ebdef8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt @@ -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() { + + 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, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt new file mode 100644 index 000000000..b21f5226b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt @@ -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(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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/ListPaginationListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/ListPaginationListener.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt index 7c1daccf3..bcd6ec292 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/ListPaginationListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt new file mode 100644 index 000000000..2d911e6a6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt @@ -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(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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonFrameLayout.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonFrameLayout.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt index 506fe322f..cf0879e4c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonFrameLayout.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt similarity index 74% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonHolder.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt index 51aa76bf8..38521c312 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt @@ -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( - ItemPageWebtoonBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ), PageHolderDelegate.Callback, View.OnClickListener { +class WebtoonHolder( + binding: ItemPageWebtoonBinding, + loader: PageLoader, + settings: AppSettings +) : BasePageHolder(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()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonImageView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonImageView.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt index fd8c21226..3e9e7a244 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonImageView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt new file mode 100644 index 000000000..c68b0bdc6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt @@ -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() { + + 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, 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonRecyclerView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonRecyclerView.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt index 1b9a42a29..16287fbeb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPagesAdapter.kt deleted file mode 100644 index ba631d8e1..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedPagesAdapter.kt +++ /dev/null @@ -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, - private val loader: PageLoader -) : BaseReaderAdapter(pages) { - - override fun onCreateViewHolder(parent: ViewGroup) = ReversedPageHolder(parent, loader) - - override fun onBindViewHolder(holder: BaseViewHolder, 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 -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedReaderFragment.kt deleted file mode 100644 index 02be05afc..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/reversed/ReversedReaderFragment.kt +++ /dev/null @@ -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(), - SharedPreferences.OnSharedPreferenceChangeListener { - - private var paginationListener: PagerPaginationListener? = null - private val settings by inject() - - 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): 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerReaderFragment.kt deleted file mode 100644 index c8047f060..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagerReaderFragment.kt +++ /dev/null @@ -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(), - SharedPreferences.OnSharedPreferenceChangeListener { - - private var paginationListener: PagerPaginationListener? = null - private val settings by inject() - - 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): 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagesAdapter.kt deleted file mode 100644 index 0ba2ec6bd..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/standard/PagesAdapter.kt +++ /dev/null @@ -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, - private val loader: PageLoader -) : BaseReaderAdapter(pages) { - - override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt new file mode 100644 index 000000000..18717e027 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index 365fb7cf0..ad8b053ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -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(), OnListItemClickListener { + private lateinit var thumbnails: List + private val spanResolver = MangaListSpanResolver() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pages = arguments?.getParcelableArrayList(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(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().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(), } - 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(), 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, title: String) = - PagesThumbnailsSheet().withArgs(2) { + fun show(fm: FragmentManager, pages: List, title: String, currentPage: Int) = + PagesThumbnailsSheet().withArgs(3) { putParcelableArrayList(ARG_PAGES, ArrayList(pages)) putString(ARG_TITLE, title) + putInt(ARG_CURRENT, currentPage) }.show(fm, TAG) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index 4f892c2e5..ffc4ba2ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -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 -) = adapterDelegateViewBinding( +) = adapterDelegateViewBinding( { 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( diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt index 4533910f0..5ca6b367b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt @@ -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, coil: ImageLoader, scope: CoroutineScope, cache: PagesCache, clickListener: OnListItemClickListener -) : ListDelegationAdapter>() { +) : ListDelegationAdapter>() { init { delegatesManager.addDelegate(pageThumbnailAD(coil, scope, cache, clickListener)) + setItems(dataSet) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonAdapter.kt deleted file mode 100644 index 52d8efc7e..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonAdapter.kt +++ /dev/null @@ -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, - private val loader: PageLoader -) : BaseReaderAdapter(pages) { - - override fun onCreateViewHolder(parent: ViewGroup) = WebtoonHolder(parent, loader) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonReaderFragment.kt deleted file mode 100644 index 33ebdd185..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/wetoon/WebtoonReaderFragment.kt +++ /dev/null @@ -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() { - - 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): 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 48e121d42..a8226c83d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -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) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index b6104b995..0ed262f74 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -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) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt index 019597155..d65ffba62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt @@ -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() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt index c4c6ddefd..28fb7564e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt @@ -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) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt b/app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt new file mode 100644 index 000000000..bc806ec7a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.utils + +fun interface BufferedObserver { + + fun onChanged(t: T, previous: T?) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveEvent.kt b/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveEvent.kt new file mode 100644 index 000000000..6102c9d1f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveEvent.kt @@ -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( + private val source: Flow, + private val context: CoroutineContext +) : LiveData() { + + 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) { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/UiUtils.kt b/app/src/main/java/org/koitharu/kotatsu/utils/UiUtils.kt index adf8ab4e3..4118e1bb1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/UiUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/UiUtils.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt index e19382be5..78b85b91a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt @@ -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 LiveData.observeNotNull(owner: LifecycleOwner, observer: Observer) { this.observe(owner) { @@ -10,4 +17,28 @@ fun LiveData.observeNotNull(owner: LifecycleOwner, observer: Observer observer.onChanged(it) } } -} \ No newline at end of file +} + +fun LiveData.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver) { + var previous: T? = null + this.observe(owner) { + observer.onChanged(it, previous) + previous = it + } +} + +fun Flow.asLiveData( + context: CoroutineContext = EmptyCoroutineContext, + defaultValue: T +): LiveData = liveData(context) { + if (latestValue == null) { + emit(defaultValue) + } + collect { + emit(it) + } +} + +fun Flow.asLiveEvent( + context: CoroutineContext = EmptyCoroutineContext +): LiveData = FlowLiveEvent(this, context) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index f4e581995..bdc677f24 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt index c6fc30181..290135399 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_reader_config.xml b/app/src/main/res/layout/dialog_reader_config.xml index fc7623caf..a9f153883 100644 --- a/app/src/main/res/layout/dialog_reader_config.xml +++ b/app/src/main/res/layout/dialog_reader_config.xml @@ -4,8 +4,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp" - android:orientation="vertical"> + android:orientation="vertical" + android:padding="16dp"> - + android:layout_marginTop="8dp" + android:text="@string/reader_mode_hint" + android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_reader_webtoon.xml b/app/src/main/res/layout/fragment_reader_webtoon.xml index 1f08901c7..ff906ec6e 100644 --- a/app/src/main/res/layout/fragment_reader_webtoon.xml +++ b/app/src/main/res/layout/fragment_reader_webtoon.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/app/src/main/res/layout/item_page_webtoon.xml b/app/src/main/res/layout/item_page_webtoon.xml index af8228aaf..7ee3bac7a 100644 --- a/app/src/main/res/layout/item_page_webtoon.xml +++ b/app/src/main/res/layout/item_page_webtoon.xml @@ -1,12 +1,12 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0149d06e8..a24742986 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -186,4 +186,5 @@ Группировать Сегодня Tap to try again + Chosen configuration will be remembered for this manga \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d1ad70db..4d6f71087 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,4 +188,5 @@ Group Today Tap to try again + Chosen configuration will be remembered for this manga \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 15caea8b4..9aa5e9924 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -32,4 +32,8 @@ @color/grey + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index cd631e285..8767267a1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,6 +5,7 @@ @drawable/ic_cross @style/AppActionMode @style/PreferenceThemeOverlay + @style/Widget.MaterialComponents.Badge