From 59dd53c02516e1a3282ab2df7695de161f255de1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 11 Apr 2024 08:10:26 +0300 Subject: [PATCH] Cleanup --- app/src/main/AndroidManifest.xml | 2 +- .../bookmarks/ui/sheet/BookmarksSheet.kt | 170 ----- .../ui/sheet/BookmarksSheetViewModel.kt | 56 -- .../org/koitharu/kotatsu/core/AppModule.kt | 2 +- .../koitharu/kotatsu/details/ui/ButtonTip.kt | 92 --- .../details/ui/ChaptersBottomSheetMediator.kt | 97 --- .../details/ui/ChaptersMenuProvider.kt | 73 -- .../kotatsu/details/ui/DetailsActivity.kt | 589 +++++++++++---- .../kotatsu/details/ui/DetailsActivity2.kt | 712 ------------------ .../details/ui/DetailsErrorObserver.kt | 2 +- .../kotatsu/details/ui/DetailsFragment.kt | 424 ----------- .../kotatsu/details/ui/DetailsMenuProvider.kt | 11 - .../kotatsu/details/ui/DetailsViewModel.kt | 12 - ...gerAdapter2.kt => ChaptersPagesAdapter.kt} | 3 +- .../details/ui/pager/ChaptersPagesSheet.kt | 2 +- .../details/ui/pager/DetailsPagerAdapter.kt | 38 - .../ui/pager/chapters/ChaptersFragment.kt | 9 - .../ui/pager/pages}/MangaPageFetcher.kt | 2 +- .../ui/pager/pages}/PageThumbnail.kt | 2 +- .../ui/pager/pages}/PageThumbnailAD.kt | 3 +- .../ui/pager/pages}/PageThumbnailAdapter.kt | 3 +- .../details/ui/pager/pages/PagesFragment.kt | 2 - .../details/ui/pager/pages/PagesViewModel.kt | 1 - .../kotatsu/reader/ui/ChaptersSheet.kt | 124 --- .../kotatsu/reader/ui/ReaderActivity.kt | 5 +- .../reader/ui/ReaderBottomMenuProvider.kt | 1 - .../kotatsu/reader/ui/ReaderSliderListener.kt | 5 +- .../ui/thumbnails/OnPageSelectListener.kt | 9 - .../ui/thumbnails/PagesThumbnailsSheet.kt | 195 ----- .../ui/thumbnails/PagesThumbnailsViewModel.kt | 97 --- .../layout-w600dp-land/activity_details.xml | 444 ++++++++--- .../activity_details_new.xml | 410 ---------- app/src/main/res/layout/activity_details.xml | 458 ++++++++--- .../main/res/layout/activity_details_new.xml | 393 ---------- app/src/main/res/layout/fragment_details.xml | 410 ---------- app/src/main/res/layout/sheet_chapters.xml | 28 - app/src/main/res/layout/sheet_pages.xml | 48 -- app/src/main/res/menu/opt_details.xml | 7 - 38 files changed, 1160 insertions(+), 3781 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheet.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheetViewModel.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity2.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt rename app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/{DetailsPagerAdapter2.kt => ChaptersPagesAdapter.kt} (94%) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter.kt rename app/src/main/kotlin/org/koitharu/kotatsu/{reader/ui/thumbnails => details/ui/pager/pages}/MangaPageFetcher.kt (98%) rename app/src/main/kotlin/org/koitharu/kotatsu/{reader/ui/thumbnails => details/ui/pager/pages}/PageThumbnail.kt (87%) rename app/src/main/kotlin/org/koitharu/kotatsu/{reader/ui/thumbnails/adapter => details/ui/pager/pages}/PageThumbnailAD.kt (94%) rename app/src/main/kotlin/org/koitharu/kotatsu/{reader/ui/thumbnails/adapter => details/ui/pager/pages}/PageThumbnailAdapter.kt (88%) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt delete mode 100644 app/src/main/res/layout-w600dp-land/activity_details_new.xml delete mode 100644 app/src/main/res/layout/activity_details_new.xml delete mode 100644 app/src/main/res/layout/fragment_details.xml delete mode 100644 app/src/main/res/layout/sheet_chapters.xml delete mode 100644 app/src/main/res/layout/sheet_pages.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3ad19ec4..0b9a3026c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,7 +95,7 @@ diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheet.kt deleted file mode 100644 index a087d73c2..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheet.kt +++ /dev/null @@ -1,170 +0,0 @@ -package org.koitharu.kotatsu.bookmarks.ui.sheet - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.GridLayoutManager -import coil.ImageLoader -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior -import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback -import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet -import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.plus -import org.koitharu.kotatsu.core.util.ext.showDistinct -import org.koitharu.kotatsu.core.util.ext.withArgs -import org.koitharu.kotatsu.databinding.SheetPagesBinding -import org.koitharu.kotatsu.list.ui.GridSpanResolver -import org.koitharu.kotatsu.list.ui.adapter.ListItemType -import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder -import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail -import javax.inject.Inject -import kotlin.math.roundToInt - -@Deprecated("") -@AndroidEntryPoint -class BookmarksSheet : - BaseAdaptiveSheet(), - AdaptiveSheetCallback, - OnListItemClickListener { - - private val viewModel by viewModels() - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var settings: AppSettings - - private var bookmarksAdapter: BookmarksAdapter? = null - private var spanResolver: GridSpanResolver? = null - - private val spanSizeLookup = SpanSizeLookup() - private val listCommitCallback = Runnable { - spanSizeLookup.invalidateCache() - } - - override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding { - return SheetPagesBinding.inflate(inflater, container, false) - } - - override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - addSheetCallback(this) - spanResolver = GridSpanResolver(binding.root.resources) - bookmarksAdapter = BookmarksAdapter( - coil = coil, - lifecycleOwner = viewLifecycleOwner, - clickListener = this@BookmarksSheet, - headerClickListener = null, - ) - viewBinding?.headerBar?.setTitle(R.string.bookmarks) - with(binding.recyclerView) { - addItemDecoration(TypedListSpacingDecoration(context, false)) - adapter = bookmarksAdapter - addOnLayoutChangeListener(spanResolver) - spanResolver?.setGridSize(settings.gridSize / 100f, this) - (layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup - } - viewModel.content.observe(viewLifecycleOwner, ::onThumbnailsChanged) - viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - } - - override fun onDestroyView() { - spanResolver = null - bookmarksAdapter = null - spanSizeLookup.invalidateCache() - super.onDestroyView() - } - - override fun onItemClick(item: Bookmark, view: View) { - val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener) - if (listener != null) { - listener.onPageSelected(ReaderPage(item.toMangaPage(), item.page, item.chapterId)) - } else { - val intent = IntentBuilder(view.context) - .manga(viewModel.manga) - .bookmark(item) - .incognito(true) - .build() - startActivity(intent) - } - dismiss() - } - - override fun onStateChanged(sheet: View, newState: Int) { - viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED - } - - private fun onThumbnailsChanged(list: List) { - val adapter = bookmarksAdapter ?: return - if (adapter.itemCount == 0) { - var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent } - if (position > 0) { - val spanCount = spanResolver?.spanCount ?: 0 - val offset = if (position > spanCount + 1) { - (resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt() - } else { - position = 0 - 0 - } - val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset) - adapter.setItems(list, listCommitCallback + scrollCallback) - } else { - adapter.setItems(list, listCommitCallback) - } - } else { - adapter.setItems(list, listCommitCallback) - } - } - - private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() { - - init { - isSpanIndexCacheEnabled = true - isSpanGroupIndexCacheEnabled = true - } - - override fun getSpanSize(position: Int): Int { - val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 - return when (bookmarksAdapter?.getItemViewType(position)) { - ListItemType.PAGE_THUMB.ordinal -> 1 - else -> total - } - } - - fun invalidateCache() { - invalidateSpanGroupIndexCache() - invalidateSpanIndexCache() - } - } - - companion object { - - const val ARG_MANGA = "manga" - - private const val TAG = "BookmarksSheet" - - fun show(fm: FragmentManager, manga: Manga) { - BookmarksSheet().withArgs(1) { - putParcelable(ARG_MANGA, ParcelableManga(manga)) - }.showDistinct(fm, TAG) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheetViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheetViewModel.kt deleted file mode 100644 index 5a6c5d45a..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksSheetViewModel.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.koitharu.kotatsu.bookmarks.ui.sheet - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.ui.BaseViewModel -import org.koitharu.kotatsu.core.util.ext.require -import org.koitharu.kotatsu.list.ui.model.ListHeader -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.LoadingFooter -import org.koitharu.kotatsu.parsers.util.SuspendLazy -import javax.inject.Inject - -@Deprecated("") -@HiltViewModel -class BookmarksSheetViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - mangaRepositoryFactory: MangaRepository.Factory, - bookmarksRepository: BookmarksRepository, -) : BaseViewModel() { - - val manga = savedStateHandle.require(BookmarksSheet.ARG_MANGA).manga - private val chaptersLazy = SuspendLazy { - requireNotNull(manga.chapters ?: mangaRepositoryFactory.create(manga.source).getDetails(manga).chapters) - } - - val content: StateFlow> = bookmarksRepository.observeBookmarks(manga) - .map { mapList(it) } - .withErrorHandling() - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingFooter())) - - private suspend fun mapList(bookmarks: List): List { - val chapters = chaptersLazy.get() - val bookmarksMap = bookmarks.groupBy { it.chapterId } - val result = ArrayList(bookmarks.size + bookmarksMap.size) - for (chapter in chapters) { - val b = bookmarksMap[chapter.id] - if (b.isNullOrEmpty()) { - continue - } - result += ListHeader(chapter.name) - result.addAll(b) - } - return result - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index 8e1eaff43..65592dd2b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -43,6 +43,7 @@ import org.koitharu.kotatsu.core.util.AcraScreenLogger import org.koitharu.kotatsu.core.util.IncognitoModeIndicator import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.isLowRamDevice +import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.LocalStorageChanges @@ -50,7 +51,6 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.sync.domain.SyncController diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt deleted file mode 100644 index cc6a94502..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.transition.TransitionManager -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.graphics.Insets -import androidx.core.view.setMargins -import androidx.core.view.updateLayoutParams -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate -import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize -import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled -import org.koitharu.kotatsu.databinding.ItemTipBinding -import com.google.android.material.R as materialR - -class ButtonTip( - private val root: ViewGroup, - private val insetsDelegate: WindowInsetsDelegate, - private val viewModel: DetailsViewModel, -) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener { - - private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false) - private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize) - - init { - selfBinding.textView.setText(R.string.details_button_tip) - selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap) - selfBinding.root.id = R.id.layout_tip - selfBinding.buttonClose.setOnClickListener(this) - } - - override fun onClick(v: View?) { - remove() - } - - override fun onWindowInsetsChanged(insets: Insets) { - if (root is CoordinatorLayout) { - selfBinding.root.updateLayoutParams { - bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize - } - } - } - - fun addToRoot() { - val lp: ViewGroup.LayoutParams = when (root) { - is CoordinatorLayout -> CoordinatorLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ).apply { - // anchorId = R.id.layout_bottom - // anchorGravity = Gravity.TOP - gravity = Gravity.BOTTOM - setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) - bottomMargin += actionBarSize - } - - is ConstraintLayout -> ConstraintLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ).apply { - width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width) - setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) - } - - else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - } - root.addView(selfBinding.root, lp) - if (root is ConstraintLayout) { - val cs = ConstraintSet() - cs.clone(root) - cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM) - cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START) - cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END) - cs.applyTo(root) - } - insetsDelegate.addInsetsListener(this) - } - - fun remove() { - if (root.context.isAnimationsEnabled) { - TransitionManager.beginDelayedTransition(root) - } - insetsDelegate.removeInsetsListener(this) - root.removeView(selfBinding.root) - viewModel.onButtonTipClosed() - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt deleted file mode 100644 index 0bf10ad63..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.view.InputDevice -import android.view.MotionEvent -import android.view.View -import android.view.View.OnLayoutChangeListener -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.view.ActionMode -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.tabs.TabLayout -import org.koitharu.kotatsu.core.ui.util.ActionModeListener -import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged -import org.koitharu.kotatsu.core.util.ext.setTabsEnabled - -class ChaptersBottomSheetMediator( - private val behavior: BottomSheetBehavior<*>, - private val pager: ViewPager2, - private val tabLayout: TabLayout, -) : OnBackPressedCallback(false), - ActionModeListener, - OnLayoutChangeListener, View.OnGenericMotionListener { - - private var lockCounter = 0 - - init { - behavior.doOnExpansionsChanged { isExpanded -> - isEnabled = isExpanded - if (!isExpanded) { - unlock() - } - } - } - - override fun handleOnBackPressed() { - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - } - - override fun onActionModeStarted(mode: ActionMode) { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - lock() - } - - override fun onActionModeFinished(mode: ActionMode) { - unlock() - } - - override fun onLayoutChange( - v: View?, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int, - ) { - val height = bottom - top - if (height != behavior.peekHeight) { - behavior.peekHeight = height - } - } - - override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { - if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { - if (event.actionMasked == MotionEvent.ACTION_SCROLL) { - if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) { - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - } else { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - } - return true - } - } - return false - } - - fun lock() { - lockCounter++ - updateLock() - } - - fun unlock() { - lockCounter-- - if (lockCounter < 0) { - lockCounter = 0 - } - updateLock() - } - - private fun updateLock() { - behavior.isDraggable = lockCounter <= 0 - pager.isUserInputEnabled = lockCounter <= 0 - tabLayout.setTabsEnabled(lockCounter <= 0) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt deleted file mode 100644 index 8bc0cf218..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.widget.SearchView -import androidx.core.view.MenuProvider -import org.koitharu.kotatsu.R -import java.lang.ref.WeakReference - -class ChaptersMenuProvider( - private val viewModel: DetailsViewModel, - private val bottomSheetMediator: ChaptersBottomSheetMediator?, -) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { - - private var searchItemRef: WeakReference? = null - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.opt_chapters, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - searchItemRef = WeakReference(searchMenuItem) - } - - override fun onPrepareMenu(menu: Menu) { - menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false - menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true - menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_reversed -> { - viewModel.setChaptersReversed(!menuItem.isChecked) - true - } - R.id.action_grid_view-> { - viewModel.setChaptersInGridView(!menuItem.isChecked) - true - } - - else -> false - } - - override fun handleOnBackPressed() { - searchItemRef?.get()?.collapseActionView() - } - - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - bottomSheetMediator?.lock() - isEnabled = true - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - isEnabled = false - (item.actionView as? SearchView)?.setQuery("", false) - viewModel.performChapterSearch(null) - bottomSheetMediator?.unlock() - return true - } - - override fun onQueryTextSubmit(query: String?): Boolean = false - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.performChapterSearch(newText) - return true - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index b38b6e893..13216d25f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -8,93 +8,118 @@ import android.text.style.DynamicDrawableSpan import android.text.style.ForegroundColorSpan import android.text.style.ImageSpan import android.text.style.RelativeSizeSpan -import android.transition.AutoTransition -import android.transition.Slide import android.transition.TransitionManager -import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams -import android.view.animation.AccelerateDecelerateInterpolator +import android.view.ViewTreeObserver import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets import androidx.core.text.buildSpannedString import androidx.core.text.inSpans -import androidx.core.view.MenuHost +import androidx.core.text.method.LinkMovementMethodCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.preference.PreferenceManager -import com.google.android.material.bottomsheet.BottomSheetBehavior +import androidx.swiperefreshlayout.widget.CircularProgressDrawable +import coil.ImageLoader +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.transform.CircleCropTransformation +import coil.util.CoilUtils +import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.filterNotNull import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.model.iconResId import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.parser.MangaIntent -import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.parser.favicon.faviconUri import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseListAdapter +import org.koitharu.kotatsu.core.ui.image.ChipIconTarget +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver -import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged -import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ViewBadge +import org.koitharu.kotatsu.core.util.ext.crossfade +import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.getThemeColor -import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled -import org.koitharu.kotatsu.core.util.ext.measureHeight -import org.koitharu.kotatsu.core.util.ext.menuView +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.isTextTruncated import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.recyclerView -import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat -import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe +import org.koitharu.kotatsu.core.util.ext.parentView +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ActivityDetailsBinding +import org.koitharu.kotatsu.details.data.MangaDetails +import org.koitharu.kotatsu.details.data.ReadingTime import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.HistoryInfo -import org.koitharu.kotatsu.details.ui.pager.DetailsPagerAdapter +import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet +import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity +import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration +import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner +import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet +import org.koitharu.kotatsu.image.ui.ImageActivity +import org.koitharu.kotatsu.list.domain.ListExtraProvider +import org.koitharu.kotatsu.list.ui.adapter.ListItemType +import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.MangaItemModel +import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver +import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder -import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet -import java.lang.ref.WeakReference +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet +import org.koitharu.kotatsu.search.ui.MangaListActivity +import org.koitharu.kotatsu.search.ui.SearchActivity +import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet import javax.inject.Inject import com.google.android.material.R as materialR -@Deprecated("") @AndroidEntryPoint class DetailsActivity : BaseActivity(), View.OnClickListener, - NoModalBottomSheetOwner, - View.OnLongClickListener, - PopupMenu.OnMenuItemClickListener { + View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener, + ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener { @Inject - lateinit var appShortcutManager: AppShortcutManager + lateinit var shortcutManager: AppShortcutManager @Inject - lateinit var settings: AppSettings + lateinit var coil: ImageLoader - private var buttonTip: WeakReference? = null + @Inject + lateinit var tagHighlighter: ListExtraProvider private val viewModel: DetailsViewModel by viewModels() - val secondaryMenuHost: MenuHost - get() = viewBinding.toolbarChapters ?: this - - var bottomSheetMediator: ChaptersBottomSheetMediator? = null - private set + private lateinit var chaptersBadge: ViewBadge override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -106,78 +131,157 @@ class DetailsActivity : viewBinding.buttonRead.setOnClickListener(this) viewBinding.buttonRead.setOnLongClickListener(this) viewBinding.buttonRead.setOnContextClickListenerCompat(this) - viewBinding.buttonDropdown.setOnClickListener(this) - - if (viewBinding.layoutBottom != null) { - val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom)) - val bsMediator = ChaptersBottomSheetMediator(behavior, viewBinding.pager, viewBinding.tabs) - actionModeDelegate.addListener(bsMediator) - checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator) - onBackPressedDispatcher.addCallback(bsMediator) - bottomSheetMediator = bsMediator - behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged) - viewBinding.toolbarChapters?.setNavigationOnClickListener { - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - } - viewBinding.toolbarChapters?.setOnGenericMotionListener(bsMediator) - } - initPager() + viewBinding.buttonChapters?.setOnClickListener(this) + viewBinding.infoLayout.chipBranch.setOnClickListener(this) + viewBinding.infoLayout.chipSize.setOnClickListener(this) + viewBinding.infoLayout.chipSource.setOnClickListener(this) + viewBinding.infoLayout.chipFavorite.setOnClickListener(this) + viewBinding.infoLayout.chipAuthor.setOnClickListener(this) + viewBinding.infoLayout.chipTime.setOnClickListener(this) + viewBinding.imageViewCover.setOnClickListener(this) + viewBinding.buttonDescriptionMore.setOnClickListener(this) + viewBinding.buttonScrobblingMore.setOnClickListener(this) + viewBinding.buttonRelatedMore.setOnClickListener(this) + viewBinding.infoLayout.chipSource.setOnClickListener(this) + viewBinding.infoLayout.chipSize.setOnClickListener(this) + viewBinding.textViewDescription.addOnLayoutChangeListener(this) + viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this) + viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance() + viewBinding.chipsTags.onChipClickListener = this + viewBinding.recyclerViewRelated.addItemDecoration( + SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), + ) + TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) + + chaptersBadge = ViewBadge(viewBinding.buttonChapters ?: viewBinding.buttonRead, this) - viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated) + viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) viewModel.onError.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver)) - viewModel.onActionDone.observeEvent( - this, - ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom), - ) - viewModel.onShowTip.observeEvent(this) { showTip() } + viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null)) viewModel.historyInfo.observe(this, ::onHistoryChanged) + viewModel.isLoading.observe(this, ::onLoadingStateChanged) + viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged) + viewModel.localSize.observe(this, ::onLocalSizeChanged) + viewModel.relatedManga.observe(this, ::onRelatedMangaChanged) + // viewModel.chapters.observe(this, ::onChaptersChanged) + viewModel.readingTime.observe(this, ::onReadingTimeChanged) viewModel.selectedBranch.observe(this) { - viewBinding.toolbarChapters?.subtitle = it - viewBinding.textViewSubtitle?.textAndVisible = it + viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) } } - val chaptersMenuInvalidator = MenuInvalidator(viewBinding.toolbarChapters ?: this) - viewModel.isChaptersReversed.observe(this, chaptersMenuInvalidator) - viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator) + viewModel.favouriteCategories.observe(this, ::onFavoritesChanged) val menuInvalidator = MenuInvalidator(this) - viewModel.favouriteCategories.observe(this, menuInvalidator) viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator) viewModel.branches.observe(this) { - viewBinding.buttonDropdown.isVisible = it.size > 1 + viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 } viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.onDownloadStarted.observeEvent( this, - DownloadStartedObserver(viewBinding.containerDetails), + DownloadStartedObserver(viewBinding.scrollView), ) addMenuProvider( DetailsMenuProvider( activity = this, viewModel = viewModel, - snackbarHost = viewBinding.pager, - appShortcutManager = appShortcutManager, + snackbarHost = viewBinding.scrollView, + appShortcutManager = shortcutManager, ), ) } - override fun getBottomSheetCollapsedHeight(): Int { - return viewBinding.layoutBsHeader?.measureHeight() ?: 0 - } - override fun onClick(v: View) { when (v.id) { R.id.button_read -> openReader(isIncognitoMode = false) - R.id.button_dropdown -> showBranchPopupMenu(v) + R.id.chip_branch -> showBranchPopupMenu(v) + R.id.button_chapters -> { + ChaptersPagesSheet.show(supportFragmentManager) + } + + R.id.chip_author -> { + val manga = viewModel.manga.value ?: return + startActivity( + SearchActivity.newIntent( + context = v.context, + source = manga.source, + query = manga.author ?: return, + ), + ) + } + + R.id.chip_source -> { + val manga = viewModel.manga.value ?: return + startActivity( + MangaListActivity.newIntent( + context = v.context, + source = manga.source, + ), + ) + } + + R.id.chip_size -> { + val manga = viewModel.manga.value ?: return + LocalInfoDialog.show(supportFragmentManager, manga) + } + + R.id.chip_favorite -> { + val manga = viewModel.manga.value ?: return + FavoriteSheet.show(supportFragmentManager, manga) + } + + R.id.chip_time -> { + if (viewModel.isStatsAvailable.value) { + val manga = viewModel.manga.value ?: return + MangaStatsSheet.show(supportFragmentManager, manga) + } else { + // TODO + } + } + + R.id.imageView_cover -> { + val manga = viewModel.manga.value ?: return + startActivity( + ImageActivity.newIntent( + v.context, + manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, + manga.source, + ), + scaleUpActivityOptionsOf(v), + ) + } + + R.id.button_description_more -> { + val tv = viewBinding.textViewDescription + TransitionManager.beginDelayedTransition(tv.parentView) + if (tv.maxLines in 1 until Integer.MAX_VALUE) { + tv.maxLines = Integer.MAX_VALUE + } else { + tv.maxLines = resources.getInteger(R.integer.details_description_lines) + } + } + + R.id.button_scrobbling_more -> { + val manga = viewModel.manga.value ?: return + ScrobblingSelectorSheet.show(supportFragmentManager, manga, null) + } + + R.id.button_related_more -> { + val manga = viewModel.manga.value ?: return + startActivity(RelatedMangaActivity.newIntent(v.context, manga)) + } } } + override fun onChipClick(chip: Chip, data: Any?) { + val tag = data as? MangaTag ?: return + startActivity(MangaListActivity.newIntent(this, setOf(tag))) + } + override fun onLongClick(v: View): Boolean = when (v.id) { R.id.button_read -> { - buttonTip?.get()?.remove() - buttonTip = null val menu = PopupMenu(v.context, v) menu.inflate(R.menu.popup_read) menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run { @@ -204,46 +308,203 @@ class DetailsActivity : true } - R.id.action_pages_thumbs -> { - val history = viewModel.historyInfo.value.history - PagesThumbnailsSheet.show( - fm = supportFragmentManager, - manga = viewModel.manga.value ?: return false, - chapterId = history?.chapterId - ?: viewModel.chapters.value.firstOrNull()?.chapter?.id - ?: return false, - currentPage = history?.page ?: 0, - ) - true + else -> false + } + } + + override fun onItemClick(item: Bookmark, view: View) { + startActivity( + IntentBuilder(view.context).bookmark(item).incognito(true).build(), + ) + Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() + } + + override fun onDraw() { + viewBinding.run { + buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE || + textViewDescription.isTextTruncated + } + } + + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + with(viewBinding) { + buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated + } + } + + private fun onChaptersChanged(chapters: List?) { + // TODO + } + + private fun onFavoritesChanged(categories: Set) { + val chip = viewBinding.infoLayout.chipFavorite + chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart) + chip.text = if (categories.isEmpty()) { + getString(R.string.add_to_favourites) + } else { + if (categories.size == 1) { + categories.first().title.ellipsize(FAV_LABEL_LIMIT) } + buildString(FAV_LABEL_LIMIT + 6) { + for ((i, cat) in categories.withIndex()) { + if (i == 0) { + append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4)) + } else if (length + cat.title.length > FAV_LABEL_LIMIT) { + append(", ") + append(getString(R.string.list_ellipsize_pattern, categories.size - i)) + break + } else { + append(", ") + append(cat.title) + } + } + } + } + } - else -> false + private fun onReadingTimeChanged(time: ReadingTime?) { + val chip = viewBinding.infoLayout.chipTime + chip.textAndVisible = time?.formatShort(chip.resources) + } + + private fun onDescriptionChanged(description: CharSequence?) { + val tv = viewBinding.textViewDescription + if (description.isNullOrBlank()) { + tv.setText(R.string.no_description) + } else { + tv.text = description } } - private fun onChaptersSheetStateChanged(isExpanded: Boolean) { - val toolbar = viewBinding.toolbarChapters ?: return - if (isAnimationsEnabled) { - val transition = AutoTransition() - transition.duration = getAnimationDuration(R.integer.config_shorterAnimTime) - TransitionManager.beginDelayedTransition(toolbar, transition) + private fun onLocalSizeChanged(size: Long) { + val chip = viewBinding.infoLayout.chipSize + if (size == 0L) { + chip.isVisible = false + } else { + chip.text = FileSize.BYTES.format(chip.context, size) + chip.isVisible = true } - if (isExpanded) { - toolbar.setNavigationIconSafe(materialR.drawable.abc_ic_clear_material) + } + + private fun onRelatedMangaChanged(related: List) { + if (related.isEmpty()) { + viewBinding.groupRelated.isVisible = false + return + } + val rv = viewBinding.recyclerViewRelated + + @Suppress("UNCHECKED_CAST") + val adapter = (rv.adapter as? BaseListAdapter) ?: BaseListAdapter() + .addDelegate( + ListItemType.MANGA_GRID, + mangaGridItemAD( + coil, this, + StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)), + ) { item, view -> + startActivity(newIntent(view.context, item)) + }, + ).also { rv.adapter = it } + adapter.items = related + viewBinding.groupRelated.isVisible = true + } + + private fun onLoadingStateChanged(isLoading: Boolean) { + val button = viewBinding.buttonChapters ?: return + if (isLoading) { + button.setImageDrawable( + CircularProgressDrawable(this).also { + it.setStyle(CircularProgressDrawable.LARGE) + it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal)) + it.start() + }, + ) + } else { + button.setImageResource(R.drawable.ic_list_sheet) + } + } + + private fun onScrobblingInfoChanged(scrobblings: List) { + var adapter = viewBinding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter + viewBinding.groupScrobbling.isGone = scrobblings.isEmpty() + if (adapter != null) { + adapter.items = scrobblings } else { - toolbar.navigationIcon = null + adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager) + adapter.items = scrobblings + viewBinding.recyclerViewScrobbling.adapter = adapter + viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) } - toolbar.menuView?.isVisible = isExpanded - viewBinding.buttonRead.isGone = isExpanded } - private fun onMangaUpdated(manga: Manga) { - title = manga.title - val hasChapters = !manga.chapters.isNullOrEmpty() - viewBinding.buttonRead.isEnabled = hasChapters - invalidateOptionsMenu() - showBottomSheet(manga.chapters != null) - viewBinding.groupHeader?.isVisible = hasChapters + private fun onMangaUpdated(details: MangaDetails) { + with(viewBinding) { + val manga = details.toManga() + val hasChapters = !manga.chapters.isNullOrEmpty() + // Main + loadCover(manga) + textViewTitle.text = manga.title + textViewSubtitle.textAndVisible = manga.altTitle + infoLayout.chipAuthor.textAndVisible = manga.author + if (manga.hasRating) { + ratingBar.rating = manga.rating * ratingBar.numStars + ratingBar.isVisible = true + } else { + ratingBar.isVisible = false + } + + manga.state?.let { state -> + textViewState.textAndVisible = resources.getString(state.titleResId) + imageViewState.setImageResource(state.iconResId) + } ?: run { + textViewState.isVisible = false + imageViewState.isVisible = false + } + + if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) { + infoLayout.chipSource.isVisible = false + } else { + infoLayout.chipSource.text = manga.source.title + infoLayout.chipSource.isVisible = true + } + + textViewNsfw.isVisible = manga.isNsfw + + // Chips + bindTags(manga) + + textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) } + + viewBinding.infoLayout.chipSource.also { chip -> + ImageRequest.Builder(this@DetailsActivity) + .data(manga.source.faviconUri()) + .lifecycle(this@DetailsActivity) + .crossfade(false) + .size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size)) + .target(ChipIconTarget(chip)) + .placeholder(R.drawable.ic_web) + .fallback(R.drawable.ic_web) + .error(R.drawable.ic_web) + .source(manga.source) + .transformations(CircleCropTransformation()) + .allowRgb565(true) + .enqueueWith(coil) + } + + buttonChapters?.isEnabled = hasChapters + title = manga.title + buttonRead.isEnabled = hasChapters + invalidateOptionsMenu() + } } private fun onMangaRemoved(manga: Manga) { @@ -260,32 +521,25 @@ class DetailsActivity : left = insets.left, right = insets.right, ) - if (insets.bottom > 0) { - window.setNavigationBarTransparentCompat( - this, - viewBinding.layoutBottom?.elevation ?: 0f, - 0.9f, - ) - } viewBinding.cardChapters?.updateLayoutParams { - bottomMargin = insets.bottom + marginEnd - } - viewBinding.dragHandle?.updateLayoutParams { - bottomMargin = insets.top + val baseOffset = leftMargin + bottomMargin = insets.bottom + baseOffset + topMargin = insets.bottom + baseOffset } + viewBinding.scrollView.updatePadding( + bottom = insets.bottom, + ) } private fun onHistoryChanged(info: HistoryInfo) { with(viewBinding.buttonRead) { if (info.history != null) { - setText(R.string._continue) - setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play) + setTitle(R.string._continue) } else { - setText(R.string.read) - setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play) + setTitle(R.string.read) } } - val text = when { + viewBinding.buttonRead.subtitle = when { !info.isValid -> getString(R.string.loading_) info.currentChapter >= 0 -> getString( R.string.chapter_d_of_d, @@ -300,20 +554,11 @@ class DetailsActivity : info.totalChapters, ) } - viewBinding.toolbarChapters?.title = text - viewBinding.textViewTitle?.text = text + viewBinding.buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true) } private fun onNewChaptersChanged(count: Int) { - val tab = viewBinding.tabs.getTabAt(0) ?: return - if (count == 0) { - tab.removeBadge() - } else { - val badge = tab.orCreateBadge - badge.horizontalOffsetWithText = -resources.getDimensionPixelOffset(R.dimen.margin_small) - badge.number = count - badge.isVisible = true - } + chaptersBadge.counter = count } private fun showBranchPopupMenu(v: View) { @@ -348,8 +593,11 @@ class DetailsActivity : append(branch.count.toString()) } } - menu.menu.add(Menu.NONE, Menu.NONE, i, title) + val item = menu.menu.add(R.id.group_branches, Menu.NONE, i, title) + item.isCheckable = true + item.isChecked = branch.isSelected } + menu.menu.setGroupCheckable(R.id.group_branches, true, true) menu.setOnMenuItemClickListener { viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name) true @@ -361,7 +609,7 @@ class DetailsActivity : val manga = viewModel.manga.value ?: return val chapterId = viewModel.historyInfo.value.history?.chapterId if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { - Snackbar.make(viewBinding.containerDetails, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT) + Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT) .show() } else { startActivity( @@ -377,24 +625,47 @@ class DetailsActivity : } } - private fun initPager() { - val adapter = DetailsPagerAdapter(this, settings) - viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false - viewBinding.pager.offscreenPageLimit = 1 - viewBinding.pager.adapter = adapter - TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach() - viewBinding.pager.setCurrentItem(settings.defaultDetailsTab, false) - viewBinding.tabs.isVisible = adapter.itemCount > 1 + private fun bindTags(manga: Manga) { + viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty() + viewBinding.chipsTags.setChips( + manga.tags.map { tag -> + ChipsView.ChipModel( + title = tag.title, + tint = tagHighlighter.getTagTint(tag), + icon = 0, + data = tag, + isCheckable = false, + isChecked = false, + ) + }, + ) } - private fun showBottomSheet(isVisible: Boolean) { - val view = viewBinding.layoutBottom ?: return - if (view.isVisible == isVisible) return - val transition = Slide(Gravity.BOTTOM) - transition.addTarget(view) - transition.interpolator = AccelerateDecelerateInterpolator() - TransitionManager.beginDelayedTransition(viewBinding.root as ViewGroup, transition) - view.isVisible = isVisible + private fun loadCover(manga: Manga) { + val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } + val lastResult = CoilUtils.result(viewBinding.imageViewCover) + if (lastResult is SuccessResult && lastResult.request.data == imageUrl) { + return + } + val request = ImageRequest.Builder(this) + .target(viewBinding.imageViewCover) + .size(CoverSizeResolver(viewBinding.imageViewCover)) + .data(imageUrl) + .tag(manga.source) + .crossfade(this) + .lifecycle(this) + .placeholderMemoryCacheKey(manga.coverUrl) + val previousDrawable = lastResult?.drawable + if (previousDrawable != null) { + request.fallback(previousDrawable) + .placeholder(previousDrawable) + .error(previousDrawable) + } else { + request.fallback(R.drawable.ic_placeholder) + .placeholder(R.drawable.ic_placeholder) + .error(R.drawable.ic_error_placeholder) + } + request.enqueueWith(coil) } private class PrefetchObserver( @@ -415,34 +686,18 @@ class DetailsActivity : } } - private fun showTip() { - val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel) - tip.addToRoot() - buttonTip = WeakReference(tip) - } - companion object { - const val TIP_BUTTON = "btn_read" - private const val KEY_NEW_ACTIVITY = "new_details_screen" + private const val FAV_LABEL_LIMIT = 10 fun newIntent(context: Context, manga: Manga): Intent { - return getActivityIntent(context) + return Intent(context, DetailsActivity::class.java) .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) } fun newIntent(context: Context, mangaId: Long): Intent { - return getActivityIntent(context) + return Intent(context, DetailsActivity::class.java) .putExtra(MangaIntent.KEY_ID, mangaId) } - - private fun getActivityIntent(context: Context): Intent { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val useNewActivity = prefs.getBoolean(KEY_NEW_ACTIVITY, true) - return Intent( - context, - if (useNewActivity) DetailsActivity2::class.java else DetailsActivity::class.java, - ) - } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity2.kt deleted file mode 100644 index fa374b2f3..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity2.kt +++ /dev/null @@ -1,712 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.os.Bundle -import android.text.style.DynamicDrawableSpan -import android.text.style.ForegroundColorSpan -import android.text.style.ImageSpan -import android.text.style.RelativeSizeSpan -import android.transition.TransitionManager -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup.MarginLayoutParams -import android.view.ViewTreeObserver -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.widget.PopupMenu -import androidx.core.graphics.Insets -import androidx.core.text.buildSpannedString -import androidx.core.text.inSpans -import androidx.core.text.method.LinkMovementMethodCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding -import androidx.swiperefreshlayout.widget.CircularProgressDrawable -import coil.ImageLoader -import coil.request.ImageRequest -import coil.request.SuccessResult -import coil.transform.CircleCropTransformation -import coil.util.CoilUtils -import com.google.android.material.chip.Chip -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.filterNotNull -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver -import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.model.iconResId -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.model.titleResId -import org.koitharu.kotatsu.core.os.AppShortcutManager -import org.koitharu.kotatsu.core.parser.MangaIntent -import org.koitharu.kotatsu.core.parser.favicon.faviconUri -import org.koitharu.kotatsu.core.ui.BaseActivity -import org.koitharu.kotatsu.core.ui.BaseListAdapter -import org.koitharu.kotatsu.core.ui.image.ChipIconTarget -import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.core.ui.util.MenuInvalidator -import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver -import org.koitharu.kotatsu.core.ui.widgets.ChipsView -import org.koitharu.kotatsu.core.util.FileSize -import org.koitharu.kotatsu.core.util.ViewBadge -import org.koitharu.kotatsu.core.util.ext.crossfade -import org.koitharu.kotatsu.core.util.ext.enqueueWith -import org.koitharu.kotatsu.core.util.ext.getThemeColor -import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty -import org.koitharu.kotatsu.core.util.ext.isTextTruncated -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.parentView -import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf -import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat -import org.koitharu.kotatsu.core.util.ext.source -import org.koitharu.kotatsu.core.util.ext.textAndVisible -import org.koitharu.kotatsu.databinding.ActivityDetailsNewBinding -import org.koitharu.kotatsu.details.data.MangaDetails -import org.koitharu.kotatsu.details.data.ReadingTime -import org.koitharu.kotatsu.details.service.MangaPrefetchService -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.HistoryInfo -import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet -import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity -import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration -import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter -import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet -import org.koitharu.kotatsu.image.ui.ImageActivity -import org.koitharu.kotatsu.list.domain.ListExtraProvider -import org.koitharu.kotatsu.list.ui.adapter.ListItemType -import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.MangaItemModel -import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver -import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.util.ellipsize -import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder -import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet -import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.search.ui.SearchActivity -import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet -import javax.inject.Inject -import com.google.android.material.R as materialR - -@AndroidEntryPoint -class DetailsActivity2 : - BaseActivity(), - View.OnClickListener, - View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener, - ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener { - - @Inject - lateinit var shortcutManager: AppShortcutManager - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var tagHighlighter: ListExtraProvider - - private val viewModel: DetailsViewModel by viewModels() - - var bottomSheetMediator: ChaptersBottomSheetMediator? = null - private set - - private lateinit var chaptersBadge: ViewBadge - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityDetailsNewBinding.inflate(layoutInflater)) - supportActionBar?.run { - setDisplayHomeAsUpEnabled(true) - setDisplayShowTitleEnabled(false) - } - viewBinding.buttonRead.setOnClickListener(this) - viewBinding.buttonRead.setOnLongClickListener(this) - viewBinding.buttonRead.setOnContextClickListenerCompat(this) - viewBinding.buttonChapters?.setOnClickListener(this) - viewBinding.infoLayout.chipBranch.setOnClickListener(this) - viewBinding.infoLayout.chipSize.setOnClickListener(this) - viewBinding.infoLayout.chipSource.setOnClickListener(this) - viewBinding.infoLayout.chipFavorite.setOnClickListener(this) - viewBinding.infoLayout.chipAuthor.setOnClickListener(this) - viewBinding.infoLayout.chipTime.setOnClickListener(this) - viewBinding.imageViewCover.setOnClickListener(this) - viewBinding.buttonDescriptionMore.setOnClickListener(this) - viewBinding.buttonScrobblingMore.setOnClickListener(this) - viewBinding.buttonRelatedMore.setOnClickListener(this) - viewBinding.infoLayout.chipSource.setOnClickListener(this) - viewBinding.infoLayout.chipSize.setOnClickListener(this) - viewBinding.textViewDescription.addOnLayoutChangeListener(this) - viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this) - viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance() - viewBinding.chipsTags.onChipClickListener = this - viewBinding.recyclerViewRelated.addItemDecoration( - SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), - ) - TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) - - chaptersBadge = ViewBadge(viewBinding.buttonChapters ?: viewBinding.buttonRead, this) - - viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) - viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) - viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) - viewModel.onError.observeEvent( - this, - SnackbarErrorObserver(viewBinding.scrollView, null, exceptionResolver) { - if (it) viewModel.reload() - }, - ) - viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null)) - viewModel.historyInfo.observe(this, ::onHistoryChanged) - viewModel.isLoading.observe(this, ::onLoadingStateChanged) - viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged) - viewModel.localSize.observe(this, ::onLocalSizeChanged) - viewModel.relatedManga.observe(this, ::onRelatedMangaChanged) - // viewModel.chapters.observe(this, ::onChaptersChanged) - viewModel.readingTime.observe(this, ::onReadingTimeChanged) - viewModel.selectedBranch.observe(this) { - viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) } - } - viewModel.favouriteCategories.observe(this, ::onFavoritesChanged) - val menuInvalidator = MenuInvalidator(this) - viewModel.isStatsAvailable.observe(this, menuInvalidator) - viewModel.remoteManga.observe(this, menuInvalidator) - viewModel.branches.observe(this) { - viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 - } - viewModel.chapters.observe(this, PrefetchObserver(this)) - viewModel.onDownloadStarted.observeEvent( - this, - DownloadStartedObserver(viewBinding.scrollView), - ) - - addMenuProvider( - DetailsMenuProvider( - activity = this, - viewModel = viewModel, - snackbarHost = viewBinding.scrollView, - appShortcutManager = shortcutManager, - ), - ) - } - - override fun onClick(v: View) { - when (v.id) { - R.id.button_read -> openReader(isIncognitoMode = false) - R.id.chip_branch -> showBranchPopupMenu(v) - R.id.button_chapters -> { - ChaptersPagesSheet.show(supportFragmentManager) - } - - R.id.chip_author -> { - val manga = viewModel.manga.value ?: return - startActivity( - SearchActivity.newIntent( - context = v.context, - source = manga.source, - query = manga.author ?: return, - ), - ) - } - - R.id.chip_source -> { - val manga = viewModel.manga.value ?: return - startActivity( - MangaListActivity.newIntent( - context = v.context, - source = manga.source, - ), - ) - } - - R.id.chip_size -> { - val manga = viewModel.manga.value ?: return - LocalInfoDialog.show(supportFragmentManager, manga) - } - - R.id.chip_favorite -> { - val manga = viewModel.manga.value ?: return - FavoriteSheet.show(supportFragmentManager, manga) - } - - R.id.chip_time -> { - if (viewModel.isStatsAvailable.value) { - val manga = viewModel.manga.value ?: return - MangaStatsSheet.show(supportFragmentManager, manga) - } else { - // TODO - } - } - - R.id.imageView_cover -> { - val manga = viewModel.manga.value ?: return - startActivity( - ImageActivity.newIntent( - v.context, - manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, - manga.source, - ), - scaleUpActivityOptionsOf(v), - ) - } - - R.id.button_description_more -> { - val tv = viewBinding.textViewDescription - TransitionManager.beginDelayedTransition(tv.parentView) - if (tv.maxLines in 1 until Integer.MAX_VALUE) { - tv.maxLines = Integer.MAX_VALUE - } else { - tv.maxLines = resources.getInteger(R.integer.details_description_lines) - } - } - - R.id.button_scrobbling_more -> { - val manga = viewModel.manga.value ?: return - ScrobblingSelectorSheet.show(supportFragmentManager, manga, null) - } - - R.id.button_related_more -> { - val manga = viewModel.manga.value ?: return - startActivity(RelatedMangaActivity.newIntent(v.context, manga)) - } - } - } - - override fun onChipClick(chip: Chip, data: Any?) { - val tag = data as? MangaTag ?: return - startActivity(MangaListActivity.newIntent(this, setOf(tag))) - } - - override fun onLongClick(v: View): Boolean = when (v.id) { - R.id.button_read -> { - val menu = PopupMenu(v.context, v) - menu.inflate(R.menu.popup_read) - menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run { - !isIncognitoMode && history != null - } - menu.setOnMenuItemClickListener(this) - menu.setForceShowIcon(true) - menu.show() - true - } - - else -> false - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_incognito -> { - openReader(isIncognitoMode = true) - true - } - - R.id.action_forget -> { - viewModel.removeFromHistory() - true - } - - else -> false - } - } - - override fun onItemClick(item: Bookmark, view: View) { - startActivity( - IntentBuilder(view.context).bookmark(item).incognito(true).build(), - ) - Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() - } - - override fun onDraw() { - viewBinding.run { - buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE || - textViewDescription.isTextTruncated - } - } - - override fun onLayoutChange( - v: View?, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - with(viewBinding) { - buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated - } - } - - private fun onChaptersChanged(chapters: List?) { - // TODO - } - - private fun onFavoritesChanged(categories: Set) { - val chip = viewBinding.infoLayout.chipFavorite - chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart) - chip.text = if (categories.isEmpty()) { - getString(R.string.add_to_favourites) - } else { - if (categories.size == 1) { - categories.first().title.ellipsize(FAV_LABEL_LIMIT) - } - buildString(FAV_LABEL_LIMIT + 6) { - for ((i, cat) in categories.withIndex()) { - if (i == 0) { - append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4)) - } else if (length + cat.title.length > FAV_LABEL_LIMIT) { - append(", ") - append(getString(R.string.list_ellipsize_pattern, categories.size - i)) - break - } else { - append(", ") - append(cat.title) - } - } - } - } - } - - private fun onReadingTimeChanged(time: ReadingTime?) { - val chip = viewBinding.infoLayout.chipTime - chip.textAndVisible = time?.formatShort(chip.resources) - } - - private fun onDescriptionChanged(description: CharSequence?) { - val tv = viewBinding.textViewDescription - if (description.isNullOrBlank()) { - tv.setText(R.string.no_description) - } else { - tv.text = description - } - } - - private fun onLocalSizeChanged(size: Long) { - val chip = viewBinding.infoLayout.chipSize - if (size == 0L) { - chip.isVisible = false - } else { - chip.text = FileSize.BYTES.format(chip.context, size) - chip.isVisible = true - } - } - - private fun onRelatedMangaChanged(related: List) { - if (related.isEmpty()) { - viewBinding.groupRelated.isVisible = false - return - } - val rv = viewBinding.recyclerViewRelated - - @Suppress("UNCHECKED_CAST") - val adapter = (rv.adapter as? BaseListAdapter) ?: BaseListAdapter() - .addDelegate( - ListItemType.MANGA_GRID, - mangaGridItemAD( - coil, this, - StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)), - ) { item, view -> - startActivity(DetailsActivity.newIntent(view.context, item)) - }, - ).also { rv.adapter = it } - adapter.items = related - viewBinding.groupRelated.isVisible = true - } - - private fun onLoadingStateChanged(isLoading: Boolean) { - val button = viewBinding.buttonChapters ?: return - if (isLoading) { - button.setImageDrawable( - CircularProgressDrawable(this).also { - it.setStyle(CircularProgressDrawable.LARGE) - it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal)) - it.start() - }, - ) - } else { - button.setImageResource(R.drawable.ic_list_sheet) - } - } - - private fun onScrobblingInfoChanged(scrobblings: List) { - var adapter = viewBinding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter - viewBinding.groupScrobbling.isGone = scrobblings.isEmpty() - if (adapter != null) { - adapter.items = scrobblings - } else { - adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager) - adapter.items = scrobblings - viewBinding.recyclerViewScrobbling.adapter = adapter - viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) - } - } - - private fun onMangaUpdated(details: MangaDetails) { - with(viewBinding) { - val manga = details.toManga() - val hasChapters = !manga.chapters.isNullOrEmpty() - // Main - loadCover(manga) - textViewTitle.text = manga.title - textViewSubtitle.textAndVisible = manga.altTitle - infoLayout.chipAuthor.textAndVisible = manga.author - if (manga.hasRating) { - ratingBar.rating = manga.rating * ratingBar.numStars - ratingBar.isVisible = true - } else { - ratingBar.isVisible = false - } - - manga.state?.let { state -> - textViewState.textAndVisible = resources.getString(state.titleResId) - imageViewState.setImageResource(state.iconResId) - } ?: run { - textViewState.isVisible = false - imageViewState.isVisible = false - } - - if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) { - infoLayout.chipSource.isVisible = false - } else { - infoLayout.chipSource.text = manga.source.title - infoLayout.chipSource.isVisible = true - } - - textViewNsfw.isVisible = manga.isNsfw - - // Chips - bindTags(manga) - - textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) } - - viewBinding.infoLayout.chipSource.also { chip -> - ImageRequest.Builder(this@DetailsActivity2) - .data(manga.source.faviconUri()) - .lifecycle(this@DetailsActivity2) - .crossfade(false) - .size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size)) - .target(ChipIconTarget(chip)) - .placeholder(R.drawable.ic_web) - .fallback(R.drawable.ic_web) - .error(R.drawable.ic_web) - .source(manga.source) - .transformations(CircleCropTransformation()) - .allowRgb565(true) - .enqueueWith(coil) - } - - buttonChapters?.isEnabled = hasChapters - title = manga.title - buttonRead.isEnabled = hasChapters - invalidateOptionsMenu() - } - } - - private fun onMangaRemoved(manga: Manga) { - Toast.makeText( - this, - getString(R.string._s_deleted_from_local_storage, manga.title), - Toast.LENGTH_SHORT, - ).show() - finishAfterTransition() - } - - override fun onWindowInsetsChanged(insets: Insets) { - viewBinding.root.updatePadding( - left = insets.left, - right = insets.right, - ) - viewBinding.cardChapters?.updateLayoutParams { - val baseOffset = leftMargin - bottomMargin = insets.bottom + baseOffset - topMargin = insets.bottom + baseOffset - } - viewBinding.scrollView.updatePadding( - bottom = insets.bottom, - ) - } - - private fun onHistoryChanged(info: HistoryInfo) { - with(viewBinding.buttonRead) { - if (info.history != null) { - setTitle(R.string._continue) - } else { - setTitle(R.string.read) - } - } - viewBinding.buttonRead.subtitle = when { - !info.isValid -> getString(R.string.loading_) - info.currentChapter >= 0 -> getString( - R.string.chapter_d_of_d, - info.currentChapter + 1, - info.totalChapters, - ) - - info.totalChapters == 0 -> getString(R.string.no_chapters) - else -> resources.getQuantityString( - R.plurals.chapters, - info.totalChapters, - info.totalChapters, - ) - } - viewBinding.buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true) - } - - private fun onNewChaptersChanged(count: Int) { - chaptersBadge.counter = count - } - - private fun showBranchPopupMenu(v: View) { - val menu = PopupMenu(v.context, v) - val branches = viewModel.branches.value - for ((i, branch) in branches.withIndex()) { - val title = buildSpannedString { - if (branch.isCurrent) { - inSpans( - ImageSpan( - this@DetailsActivity2, - R.drawable.ic_current_chapter, - DynamicDrawableSpan.ALIGN_BASELINE, - ), - ) { - append(' ') - } - append(' ') - } - append(branch.name ?: getString(R.string.system_default)) - append(' ') - append(' ') - inSpans( - ForegroundColorSpan( - v.context.getThemeColor( - android.R.attr.textColorSecondary, - Color.LTGRAY, - ), - ), - RelativeSizeSpan(0.74f), - ) { - append(branch.count.toString()) - } - } - val item = menu.menu.add(R.id.group_branches, Menu.NONE, i, title) - item.isCheckable = true - item.isChecked = branch.isSelected - } - menu.menu.setGroupCheckable(R.id.group_branches, true, true) - menu.setOnMenuItemClickListener { - viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name) - true - } - menu.show() - } - - private fun openReader(isIncognitoMode: Boolean) { - val manga = viewModel.manga.value ?: return - val chapterId = viewModel.historyInfo.value.history?.chapterId - if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { - Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT) - .show() - } else { - startActivity( - IntentBuilder(this) - .manga(manga) - .branch(viewModel.selectedBranchValue) - .incognito(isIncognitoMode) - .build(), - ) - if (isIncognitoMode) { - Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show() - } - } - } - - private fun bindTags(manga: Manga) { - viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty() - viewBinding.chipsTags.setChips( - manga.tags.map { tag -> - ChipsView.ChipModel( - title = tag.title, - tint = tagHighlighter.getTagTint(tag), - icon = 0, - data = tag, - isCheckable = false, - isChecked = false, - ) - }, - ) - } - - private fun loadCover(manga: Manga) { - val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } - val lastResult = CoilUtils.result(viewBinding.imageViewCover) - if (lastResult is SuccessResult && lastResult.request.data == imageUrl) { - return - } - val request = ImageRequest.Builder(this) - .target(viewBinding.imageViewCover) - .size(CoverSizeResolver(viewBinding.imageViewCover)) - .data(imageUrl) - .tag(manga.source) - .crossfade(this) - .lifecycle(this) - .placeholderMemoryCacheKey(manga.coverUrl) - val previousDrawable = lastResult?.drawable - if (previousDrawable != null) { - request.fallback(previousDrawable) - .placeholder(previousDrawable) - .error(previousDrawable) - } else { - request.fallback(R.drawable.ic_placeholder) - .placeholder(R.drawable.ic_placeholder) - .error(R.drawable.ic_error_placeholder) - } - request.enqueueWith(coil) - } - - private class PrefetchObserver( - private val context: Context, - ) : FlowCollector?> { - - private var isCalled = false - - override suspend fun emit(value: List?) { - if (value.isNullOrEmpty()) { - return - } - if (!isCalled) { - isCalled = true - val item = value.find { it.isCurrent } ?: value.first() - MangaPrefetchService.prefetchPages(context, item.chapter) - } - } - } - - companion object { - - private const val FAV_LABEL_LIMIT = 10 - - fun newIntent(context: Context, manga: Manga): Intent { - return Intent(context, DetailsActivity2::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) - } - - fun newIntent(context: Context, mangaId: Long): Intent { - return Intent(context, DetailsActivity2::class.java) - .putExtra(MangaIntent.KEY_ID, mangaId) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsErrorObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsErrorObserver.kt index c879cea9b..482618a0a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsErrorObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsErrorObserver.kt @@ -16,7 +16,7 @@ class DetailsErrorObserver( private val viewModel: DetailsViewModel, resolver: ExceptionResolver?, ) : ErrorObserver( - activity.viewBinding.containerDetails, null, resolver, + activity.viewBinding.scrollView, null, resolver, { isResolved -> if (isResolved) { viewModel.reload() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt deleted file mode 100644 index 7eaf53c2a..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ /dev/null @@ -1,424 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.os.Bundle -import android.transition.TransitionManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewTreeObserver -import android.widget.Toast -import androidx.appcompat.widget.PopupMenu -import androidx.core.content.ContextCompat -import androidx.core.graphics.Insets -import androidx.core.text.method.LinkMovementMethodCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.view.updatePadding -import androidx.fragment.app.activityViewModels -import coil.ImageLoader -import coil.request.ImageRequest -import coil.request.SuccessResult -import coil.util.CoilUtils -import com.google.android.material.chip.Chip -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.filterNotNull -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter -import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet -import org.koitharu.kotatsu.core.model.countChaptersByBranch -import org.koitharu.kotatsu.core.model.iconResId -import org.koitharu.kotatsu.core.model.titleResId -import org.koitharu.kotatsu.core.ui.BaseFragment -import org.koitharu.kotatsu.core.ui.BaseListAdapter -import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.core.ui.widgets.ChipsView -import org.koitharu.kotatsu.core.util.FileSize -import org.koitharu.kotatsu.core.util.ext.crossfade -import org.koitharu.kotatsu.core.util.ext.drawableTop -import org.koitharu.kotatsu.core.util.ext.enqueueWith -import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty -import org.koitharu.kotatsu.core.util.ext.isTextTruncated -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.core.util.ext.parentView -import org.koitharu.kotatsu.core.util.ext.resolveDp -import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf -import org.koitharu.kotatsu.core.util.ext.showOrHide -import org.koitharu.kotatsu.core.util.ext.textAndVisible -import org.koitharu.kotatsu.databinding.FragmentDetailsBinding -import org.koitharu.kotatsu.details.data.ReadingTime -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.HistoryInfo -import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity -import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration -import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter -import org.koitharu.kotatsu.history.data.PROGRESS_NONE -import org.koitharu.kotatsu.image.ui.ImageActivity -import org.koitharu.kotatsu.list.domain.ListExtraProvider -import org.koitharu.kotatsu.list.ui.adapter.ListItemType -import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.MangaItemModel -import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver -import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog -import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.reader.ui.ReaderActivity -import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet -import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.search.ui.SearchActivity -import javax.inject.Inject - -@AndroidEntryPoint -class DetailsFragment : - BaseFragment(), - View.OnClickListener, - ChipsView.OnChipClickListener, - OnListItemClickListener, ViewTreeObserver.OnDrawListener, View.OnLayoutChangeListener { - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var tagHighlighter: ListExtraProvider - - private val viewModel by activityViewModels() - - override fun onCreateViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ) = FragmentDetailsBinding.inflate(inflater, container, false) - - override fun onViewBindingCreated(binding: FragmentDetailsBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - binding.textViewAuthor.setOnClickListener(this) - binding.imageViewCover.setOnClickListener(this) - binding.buttonDescriptionMore.setOnClickListener(this) - binding.buttonBookmarksMore.setOnClickListener(this) - binding.buttonScrobblingMore.setOnClickListener(this) - binding.buttonRelatedMore.setOnClickListener(this) - binding.infoLayout.textViewSource.setOnClickListener(this) - binding.infoLayout.textViewSize.setOnClickListener(this) - binding.textViewDescription.addOnLayoutChangeListener(this) - binding.textViewDescription.viewTreeObserver.addOnDrawListener(this) - binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance() - binding.chipsTags.onChipClickListener = this - binding.recyclerViewRelated.addItemDecoration( - SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), - ) - TitleScrollCoordinator(binding.textViewTitle).attach(binding.scrollView) - viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated) - viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) - viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged) - viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) - viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged) - viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged) - viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged) - viewModel.relatedManga.observe(viewLifecycleOwner, ::onRelatedMangaChanged) - viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged) - viewModel.readingTime.observe(viewLifecycleOwner, ::onReadingTimeChanged) - } - - override fun onItemClick(item: Bookmark, view: View) { - startActivity( - ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(), - ) - Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() - } - - override fun onItemLongClick(item: Bookmark, view: View): Boolean { - val menu = PopupMenu(view.context, view) - menu.inflate(R.menu.popup_bookmark) - menu.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.action_remove -> viewModel.removeBookmark(item) - } - true - } - menu.show() - return true - } - - override fun onDraw() { - viewBinding?.run { - buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE || - textViewDescription.isTextTruncated - } - } - - override fun onLayoutChange( - v: View?, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - with(viewBinding ?: return) { - buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated - } - } - - private fun onMangaUpdated(manga: Manga) { - with(requireViewBinding()) { - // Main - loadCover(manga) - textViewTitle.text = manga.title - textViewSubtitle.textAndVisible = manga.altTitle - textViewAuthor.textAndVisible = manga.author - if (manga.hasRating) { - ratingBar.rating = manga.rating * ratingBar.numStars - ratingBar.isVisible = true - } else { - ratingBar.isVisible = false - } - - infoLayout.textViewState.apply { - manga.state?.let { state -> - textAndVisible = resources.getString(state.titleResId) - drawableTop = ContextCompat.getDrawable(context, state.iconResId) - } ?: run { - isVisible = false - } - } - if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) { - infoLayout.textViewSource.isVisible = false - } else { - infoLayout.textViewSource.text = manga.source.title - infoLayout.textViewSource.isVisible = true - } - - infoLayout.textViewNsfw.isVisible = manga.isNsfw - - // Chips - bindTags(manga) - } - } - - private fun onChaptersChanged(chapters: List?) { - val infoLayout = requireViewBinding().infoLayout - if (chapters.isNullOrEmpty()) { - infoLayout.textViewChapters.isVisible = false - } else { - val count = chapters.countChaptersByBranch() - infoLayout.textViewChapters.isVisible = true - val chaptersText = resources.getQuantityString(R.plurals.chapters, count, count) - infoLayout.textViewChapters.text = chaptersText - } - } - - private fun onReadingTimeChanged(time: ReadingTime?) { - val binding = viewBinding ?: return - if (time == null) { - binding.approximateReadTimeLayout.isVisible = false - return - } - binding.approximateReadTime.text = time.format(resources) - binding.approximateReadTimeTitle.setText( - if (time.isContinue) R.string.approximate_remaining_time else R.string.approximate_reading_time, - ) - binding.approximateReadTimeLayout.isVisible = true - } - - private fun onDescriptionChanged(description: CharSequence?) { - val tv = requireViewBinding().textViewDescription - if (description.isNullOrBlank()) { - tv.setText(R.string.no_description) - } else { - tv.text = description - } - } - - private fun onLocalSizeChanged(size: Long) { - val textView = requireViewBinding().infoLayout.textViewSize - if (size == 0L) { - textView.isVisible = false - } else { - textView.text = FileSize.BYTES.format(textView.context, size) - textView.isVisible = true - } - } - - private fun onRelatedMangaChanged(related: List) { - if (related.isEmpty()) { - requireViewBinding().groupRelated.isVisible = false - return - } - val rv = viewBinding?.recyclerViewRelated ?: return - - @Suppress("UNCHECKED_CAST") - val adapter = (rv.adapter as? BaseListAdapter) ?: BaseListAdapter() - .addDelegate( - ListItemType.MANGA_GRID, - mangaGridItemAD( - coil, viewLifecycleOwner, - StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)), - ) { item, view -> - startActivity(DetailsActivity.newIntent(view.context, item)) - }, - ).also { rv.adapter = it } - adapter.items = related - requireViewBinding().groupRelated.isVisible = true - } - - private fun onHistoryChanged(history: HistoryInfo) { - requireViewBinding().progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true) - } - - private fun onLoadingStateChanged(isLoading: Boolean) { - requireViewBinding().progressBar.showOrHide(isLoading) - } - - private fun onBookmarksChanged(bookmarks: List) { - var adapter = requireViewBinding().recyclerViewBookmarks.adapter as? BookmarksAdapter - requireViewBinding().groupBookmarks.isGone = bookmarks.isEmpty() - if (adapter != null) { - adapter.items = bookmarks - } else { - adapter = BookmarksAdapter(coil, viewLifecycleOwner, this) - adapter.items = bookmarks - requireViewBinding().recyclerViewBookmarks.adapter = adapter - val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing) - requireViewBinding().recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing)) - } - } - - private fun onScrobblingInfoChanged(scrobblings: List) { - var adapter = requireViewBinding().recyclerViewScrobbling.adapter as? ScrollingInfoAdapter - requireViewBinding().groupScrobbling.isGone = scrobblings.isEmpty() - if (adapter != null) { - adapter.items = scrobblings - } else { - adapter = ScrollingInfoAdapter(viewLifecycleOwner, coil, childFragmentManager) - adapter.items = scrobblings - requireViewBinding().recyclerViewScrobbling.adapter = adapter - requireViewBinding().recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) - } - } - - override fun onClick(v: View) { - val manga = viewModel.manga.value ?: return - when (v.id) { - R.id.textView_author -> { - startActivity( - SearchActivity.newIntent( - context = v.context, - source = manga.source, - query = manga.author ?: return, - ), - ) - } - - R.id.textView_source -> { - startActivity( - MangaListActivity.newIntent( - context = v.context, - source = manga.source, - ), - ) - } - - R.id.textView_size -> { - LocalInfoDialog.show(parentFragmentManager, manga) - } - - R.id.imageView_cover -> { - startActivity( - ImageActivity.newIntent( - v.context, - manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, - manga.source, - ), - scaleUpActivityOptionsOf(v), - ) - } - - R.id.button_description_more -> { - val tv = requireViewBinding().textViewDescription - TransitionManager.beginDelayedTransition(tv.parentView) - if (tv.maxLines in 1 until Integer.MAX_VALUE) { - tv.maxLines = Integer.MAX_VALUE - } else { - tv.maxLines = resources.getInteger(R.integer.details_description_lines) - } - } - - R.id.button_scrobbling_more -> { - ScrobblingSelectorSheet.show(parentFragmentManager, manga, null) - } - - R.id.button_bookmarks_more -> { - BookmarksSheet.show(parentFragmentManager, manga) - } - - R.id.button_related_more -> { - startActivity(RelatedMangaActivity.newIntent(v.context, manga)) - } - } - } - - override fun onChipClick(chip: Chip, data: Any?) { - val tag = data as? MangaTag ?: return - startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag))) - } - - override fun onWindowInsetsChanged(insets: Insets) { - requireViewBinding().root.updatePadding( - bottom = ( - (activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight() - ?.plus(insets.bottom)?.plus(resources.resolveDp(16)) - ) - ?: insets.bottom, - ) - } - - private fun bindTags(manga: Manga) { - requireViewBinding().chipsTags.setChips( - manga.tags.map { tag -> - ChipsView.ChipModel( - title = tag.title, - tint = tagHighlighter.getTagTint(tag), - icon = 0, - data = tag, - isCheckable = false, - isChecked = false, - ) - }, - ) - } - - private fun loadCover(manga: Manga) { - val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } - val lastResult = CoilUtils.result(requireViewBinding().imageViewCover) - if (lastResult is SuccessResult && lastResult.request.data == imageUrl) { - return - } - val request = ImageRequest.Builder(context ?: return) - .target(requireViewBinding().imageViewCover) - .size(CoverSizeResolver(requireViewBinding().imageViewCover)) - .data(imageUrl) - .tag(manga.source) - .crossfade(requireContext()) - .lifecycle(viewLifecycleOwner) - .placeholderMemoryCacheKey(manga.coverUrl) - val previousDrawable = lastResult?.drawable - if (previousDrawable != null) { - request.fallback(previousDrawable) - .placeholder(previousDrawable) - .error(previousDrawable) - } else { - request.fallback(R.drawable.ic_placeholder) - .placeholder(R.drawable.ic_placeholder) - .error(R.drawable.ic_error_placeholder) - } - request.enqueueWith(coil) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt index edc13345a..87ef10aa0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt @@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.download.ui.dialog.DownloadOption -import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity @@ -35,7 +34,6 @@ class DetailsMenuProvider( override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_details, menu) - menu.findItem(R.id.action_favourite).isVisible = activity is DetailsActivity } override fun onPrepareMenu(menu: Menu) { @@ -48,9 +46,6 @@ class DetailsMenuProvider( menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsAvailable.value - menu.findItem(R.id.action_favourite).setIcon( - if (viewModel.favouriteCategories.value.isNotEmpty()) R.drawable.ic_heart else R.drawable.ic_heart_outline, - ) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { @@ -66,12 +61,6 @@ class DetailsMenuProvider( } } - R.id.action_favourite -> { - viewModel.manga.value?.let { - FavoriteSheet.show(activity.supportFragmentManager, it) - } - } - R.id.action_delete -> { val title = viewModel.manga.value?.title.orEmpty() MaterialAlertDialogBuilder(activity) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 6f185d14c..2195ff995 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -11,8 +11,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -247,12 +245,6 @@ class DetailsViewModel @Inject constructor( localStorageChanges .collect { onDownloadComplete(it) } } - launchJob(Dispatchers.Default) { - if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) { - manga.filterNot { it?.chapters.isNullOrEmpty() }.first() - onShowTip.call(Unit) - } - } launchJob(Dispatchers.Default) { val manga = details.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob val h = history.firstOrNull() @@ -363,10 +355,6 @@ class DetailsViewModel @Inject constructor( onSelectChapter.call(chapter.chapter.id) } - fun onButtonTipClosed() { - settings.closeTip(DetailsActivity.TIP_BUTTON) - } - fun removeFromHistory() { launchJob(Dispatchers.Default) { val handle = historyRepository.delete(setOf(mangaId)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt similarity index 94% rename from app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter2.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt index 61e1c4c74..aeaa3092b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter2.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesAdapter.kt @@ -5,12 +5,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.details.ui.pager.bookmarks.MangaBookmarksFragment import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment -class DetailsPagerAdapter2( +class ChaptersPagesAdapter( fragment: Fragment, val isPagesTabEnabled: Boolean, ) : FragmentStateAdapter(fragment), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt index 5ad5b7b30..5177a8370 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt @@ -39,7 +39,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio disableFitToContents() val args = arguments ?: Bundle.EMPTY - val adapter = DetailsPagerAdapter2(this, args.getBoolean(ARG_SHOW_PAGES, settings.isPagesTabEnabled)) + val adapter = ChaptersPagesAdapter(this, args.getBoolean(ARG_SHOW_PAGES, settings.isPagesTabEnabled)) binding.pager.recyclerView?.isNestedScrollingEnabled = false binding.pager.offscreenPageLimit = adapter.itemCount binding.pager.adapter = adapter diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter.kt deleted file mode 100644 index f64879fa0..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/DetailsPagerAdapter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.koitharu.kotatsu.details.ui.pager - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment -import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment - -class DetailsPagerAdapter( - activity: FragmentActivity, - settings: AppSettings, -) : FragmentStateAdapter(activity), - TabLayoutMediator.TabConfigurationStrategy { - - val isPagesTabEnabled = settings.isPagesTabEnabled - - override fun getItemCount(): Int = if (isPagesTabEnabled) 2 else 1 - - override fun createFragment(position: Int): Fragment = when (position) { - 0 -> ChaptersFragment() - 1 -> PagesFragment() - else -> throw IllegalArgumentException("Invalid position $position") - } - - override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { - tab.setText( - when (position) { - 0 -> R.string.chapters - 1 -> R.string.pages - else -> 0 - }, - ) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt index 4bda835c0..e2204722f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt @@ -10,7 +10,6 @@ import androidx.appcompat.view.ActionMode import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -28,8 +27,6 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentChaptersBinding -import org.koitharu.kotatsu.details.ui.ChaptersMenuProvider -import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration @@ -96,12 +93,6 @@ class ChaptersFragment : viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) { selectionController?.onItemLongClick(it) } - val detailsActivity = activity as? DetailsActivity - if (detailsActivity != null) { - val menuProvider = ChaptersMenuProvider(viewModel, detailsActivity.bottomSheetMediator) - activity?.onBackPressedDispatcher?.addCallback(menuProvider) - detailsActivity.secondaryMenuHost.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED) - } } override fun onDestroyView() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt similarity index 98% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index 0a693d701..713c4eaf0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails +package org.koitharu.kotatsu.details.ui.pager.pages import android.content.Context import android.webkit.MimeTypeMap diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnail.kt similarity index 87% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnail.kt index a0efac812..3591446af 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnail.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails +package org.koitharu.kotatsu.details.ui.pager.pages import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.ui.pager.ReaderPage diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAD.kt similarity index 94% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAD.kt index e45238a80..e18c2e233 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAD.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails.adapter +package org.koitharu.kotatsu.details.ui.pager.pages import androidx.lifecycle.LifecycleOwner import coil.ImageLoader @@ -15,7 +15,6 @@ import org.koitharu.kotatsu.core.util.ext.setTextColorAttr import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemPageThumbBinding import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import com.google.android.material.R as materialR fun pageThumbnailAD( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAdapter.kt similarity index 88% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAdapter.kt index b9df32ce2..5c66f2d7c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PageThumbnailAdapter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails.adapter +package org.koitharu.kotatsu.details.ui.pager.pages import android.content.Context import androidx.lifecycle.LifecycleOwner @@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail class PageThumbnailAdapter( coil: ImageLoader, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt index d827c0a38..453f269c4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt @@ -37,8 +37,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback import org.koitharu.kotatsu.reader.ui.ReaderState -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail -import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter import javax.inject.Inject import kotlin.math.roundToInt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt index 0faab9b42..2700bba20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt @@ -16,7 +16,6 @@ import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.domain.ChaptersLoader -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt deleted file mode 100644 index f6274ee98..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt +++ /dev/null @@ -1,124 +0,0 @@ -package org.koitharu.kotatsu.reader.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.model.findById -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet -import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback -import org.koitharu.kotatsu.core.util.ext.showDistinct -import org.koitharu.kotatsu.databinding.SheetChaptersBinding -import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter -import org.koitharu.kotatsu.details.ui.mapChapters -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.pager.chapters.ChapterGridSpanHelper -import org.koitharu.kotatsu.details.ui.withVolumeHeaders -import org.koitharu.kotatsu.history.data.PROGRESS_NONE -import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration -import org.koitharu.kotatsu.parsers.model.MangaChapter -import java.time.Instant -import javax.inject.Inject -import kotlin.math.roundToInt - -@Deprecated("Use ChaptersPagesSheet instead") -@AndroidEntryPoint -class ChaptersSheet : BaseAdaptiveSheet(), - OnListItemClickListener { - - @Inject - lateinit var settings: AppSettings - - private val viewModel: ReaderViewModel by activityViewModels() - - override fun onCreateViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ) = SheetChaptersBinding.inflate(inflater, container, false) - - override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - val manga = viewModel.manga - if (manga == null) { - dismissAllowingStateLoss() - return - } - val state = viewModel.getCurrentState() - val currentChapter = state?.let { manga.allChapters.findById(it.chapterId) } - val chapters = manga.mapChapters( - history = state?.let { - MangaHistory( - createdAt = Instant.now(), - updatedAt = Instant.now(), - chapterId = it.chapterId, - page = it.page, - scroll = it.scroll, - percent = PROGRESS_NONE, - ) - }, - newCount = 0, - branch = currentChapter?.branch, - bookmarks = listOf(), - isGrid = settings.isChaptersGridView, - ).withVolumeHeaders(binding.root.context) - if (chapters.isEmpty()) { - dismissAllowingStateLoss() - return - } - val currentPosition = if (currentChapter != null) { - chapters.indexOfFirst { it is ChapterListItem && it.chapter.id == currentChapter.id } - } else { - -1 - } - binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.recyclerView.context, true)) - binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter -> - if (currentPosition >= 0) { - val targetPosition = (currentPosition - 1).coerceAtLeast(0) - val offset = - (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt() - adapter.setItems( - chapters, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset), - ) - } else { - adapter.items = chapters - } - } - ChapterGridSpanHelper.attach(binding.recyclerView) - binding.recyclerView.layoutManager = if (settings.isChaptersGridView) { - GridLayoutManager(context, ChapterGridSpanHelper.getSpanCount(binding.recyclerView)).apply { - spanSizeLookup = ChapterGridSpanHelper.SpanSizeLookup(binding.recyclerView) - } - } else { - LinearLayoutManager(context) - } - } - - override fun onItemClick(item: ChapterListItem, view: View) { - ((parentFragment as? OnChapterChangeListener) - ?: (activity as? OnChapterChangeListener))?.let { - dismiss() - it.onChapterSelected(item.chapter) - } - } - - fun interface OnChapterChangeListener { - - fun onChapterSelected(chapter: MangaChapter): Boolean - } - - companion object { - - private const val TAG = "ChaptersBottomSheet" - - fun show(fm: FragmentManager) = ChaptersSheet().showDistinct(fm, TAG) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index b91b81564..d60cd7f57 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -61,16 +61,13 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.tapgrid.TapGridDispatcher -import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import java.util.concurrent.TimeUnit import javax.inject.Inject @AndroidEntryPoint class ReaderActivity : BaseFullscreenActivity(), - ChaptersSheet.OnChapterChangeListener, TapGridDispatcher.OnGridTouchListener, - OnPageSelectListener, ReaderConfigSheet.Callback, ReaderControlDelegate.OnInteractionListener, OnApplyWindowInsetsListener, @@ -119,7 +116,7 @@ class ReaderActivity : controlDelegate = ReaderControlDelegate(resources, settings, tapGridSettings, this) viewBinding.slider.setLabelFormatter(PageLabelFormatter()) viewBinding.zoomControl.listener = this - ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider) + ReaderSliderListener(viewModel, this).attachToSlider(viewBinding.slider) insetsDelegate.interceptingWindowInsetsListener = this idlingDetector.bindToLifecycle(this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderBottomMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderBottomMenuProvider.kt index 00ba9a06b..2c8db46ed 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderBottomMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderBottomMenuProvider.kt @@ -8,7 +8,6 @@ import androidx.fragment.app.FragmentActivity import org.koitharu.kotatsu.R import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet -import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.settings.SettingsActivity class ReaderBottomMenuProvider( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt index 50e87a4d9..5bfc74953 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt @@ -2,11 +2,10 @@ package org.koitharu.kotatsu.reader.ui import com.google.android.material.slider.Slider import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener class ReaderSliderListener( - private val pageSelectListener: OnPageSelectListener, private val viewModel: ReaderViewModel, + private val callback: ReaderNavigationCallback, ) : Slider.OnChangeListener, Slider.OnSliderTouchListener { private var isChanged = false @@ -43,6 +42,6 @@ class ReaderSliderListener( val pages = viewModel.getCurrentChapterPages() val page = pages?.getOrNull(index) ?: return val chapterId = viewModel.getCurrentState()?.chapterId ?: return - pageSelectListener.onPageSelected(ReaderPage(page, index, chapterId)) + callback.onPageSelected(ReaderPage(page, index, chapterId)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt deleted file mode 100644 index eef690e53..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import org.koitharu.kotatsu.reader.ui.pager.ReaderPage - -@Deprecated("") -fun interface OnPageSelectListener { - - fun onPageSelected(page: ReaderPage): Boolean -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt deleted file mode 100644 index adf82e7e5..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ /dev/null @@ -1,195 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import coil.ImageLoader -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior -import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback -import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet -import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.plus -import org.koitharu.kotatsu.core.util.ext.showDistinct -import org.koitharu.kotatsu.core.util.ext.showOrHide -import org.koitharu.kotatsu.core.util.ext.withArgs -import org.koitharu.kotatsu.databinding.SheetPagesBinding -import org.koitharu.kotatsu.list.ui.GridSpanResolver -import org.koitharu.kotatsu.list.ui.adapter.ListItemType -import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder -import org.koitharu.kotatsu.reader.ui.ReaderState -import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter -import javax.inject.Inject -import kotlin.math.roundToInt - -@Deprecated("Use ChaptersPagesSheet instead") -@AndroidEntryPoint -class PagesThumbnailsSheet : - BaseAdaptiveSheet(), - AdaptiveSheetCallback, - OnListItemClickListener { - - private val viewModel by viewModels() - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var settings: AppSettings - - private var thumbnailsAdapter: PageThumbnailAdapter? = null - private var spanResolver: GridSpanResolver? = null - private var scrollListener: ScrollListener? = null - - private val spanSizeLookup = SpanSizeLookup() - private val listCommitCallback = Runnable { - spanSizeLookup.invalidateCache() - } - - override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding { - return SheetPagesBinding.inflate(inflater, container, false) - } - - override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - addSheetCallback(this) - spanResolver = GridSpanResolver(binding.root.resources) - thumbnailsAdapter = PageThumbnailAdapter( - coil = coil, - lifecycleOwner = viewLifecycleOwner, - clickListener = this@PagesThumbnailsSheet, - ) - with(binding.recyclerView) { - addItemDecoration(TypedListSpacingDecoration(context, false)) - adapter = thumbnailsAdapter - addOnLayoutChangeListener(spanResolver) - spanResolver?.setGridSize(settings.gridSize / 100f, this) - addOnScrollListener(ScrollListener().also { scrollListener = it }) - (layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup - } - viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) - viewModel.branch.observe(viewLifecycleOwner, ::updateTitle) - viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } - } - - override fun onDestroyView() { - spanResolver = null - scrollListener = null - thumbnailsAdapter = null - spanSizeLookup.invalidateCache() - super.onDestroyView() - } - - override fun onItemClick(item: PageThumbnail, view: View) { - val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener) - if (listener != null) { - listener.onPageSelected(item.page) - } else { - val state = ReaderState(item.page.chapterId, item.page.index, 0) - val intent = IntentBuilder(view.context).manga(viewModel.manga).state(state).build() - startActivity(intent) - } - dismiss() - } - - override fun onStateChanged(sheet: View, newState: Int) { - viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED - } - - private fun updateTitle(branch: String?) { - val mangaName = viewModel.manga.title - viewBinding?.headerBar?.title = if (branch != null) { - getString(R.string.manga_branch_title_template, mangaName, branch) - } else { - mangaName - } - } - - private fun onThumbnailsChanged(list: List) { - val adapter = thumbnailsAdapter ?: return - if (adapter.itemCount == 0) { - var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent } - if (position > 0) { - val spanCount = spanResolver?.spanCount ?: 0 - val offset = if (position > spanCount + 1) { - (resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt() - } else { - position = 0 - 0 - } - val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset) - adapter.setItems(list, listCommitCallback + scrollCallback) - } else { - adapter.setItems(list, listCommitCallback) - } - } else { - adapter.setItems(list, listCommitCallback) - } - } - - private inner class ScrollListener : BoundsScrollListener(3, 3) { - - override fun onScrolledToStart(recyclerView: RecyclerView) { - viewModel.loadPrevChapter() - } - - override fun onScrolledToEnd(recyclerView: RecyclerView) { - viewModel.loadNextChapter() - } - } - - private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() { - - init { - isSpanIndexCacheEnabled = true - isSpanGroupIndexCacheEnabled = true - } - - override fun getSpanSize(position: Int): Int { - val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 - return when (thumbnailsAdapter?.getItemViewType(position)) { - ListItemType.PAGE_THUMB.ordinal -> 1 - else -> total - } - } - - fun invalidateCache() { - invalidateSpanGroupIndexCache() - invalidateSpanIndexCache() - } - } - - companion object { - - const val ARG_MANGA = "manga" - const val ARG_CURRENT_PAGE = "current" - const val ARG_CHAPTER_ID = "chapter_id" - - private const val TAG = "PagesThumbnailsSheet" - - fun show(fm: FragmentManager, manga: Manga, chapterId: Long, currentPage: Int = -1) { - PagesThumbnailsSheet().withArgs(3) { - putParcelable(ARG_MANGA, ParcelableManga(manga)) - putLong(ARG_CHAPTER_ID, chapterId) - putInt(ARG_CURRENT_PAGE, currentPage) - }.showDistinct(fm, TAG) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt deleted file mode 100644 index 16ad32e1a..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import org.koitharu.kotatsu.core.model.findById -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.parser.MangaIntent -import org.koitharu.kotatsu.core.ui.BaseViewModel -import org.koitharu.kotatsu.core.util.ext.firstNotNull -import org.koitharu.kotatsu.core.util.ext.require -import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase -import org.koitharu.kotatsu.list.ui.model.ListHeader -import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.reader.domain.ChaptersLoader -import javax.inject.Inject - -@HiltViewModel -class PagesThumbnailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val chaptersLoader: ChaptersLoader, - detailsLoadUseCase: DetailsLoadUseCase, -) : BaseViewModel() { - - private val currentPageIndex: Int = - savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1 - private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L - val manga = savedStateHandle.require(PagesThumbnailsSheet.ARG_MANGA).manga - - private val mangaDetails = detailsLoadUseCase(MangaIntent.of(manga)).map { - val b = manga.chapters?.findById(initialChapterId)?.branch - branch.value = b - it.filterChapters(b) - }.withErrorHandling() - .stateIn(viewModelScope, SharingStarted.Lazily, null) - private var loadingJob: Job - private var loadingPrevJob: Job? = null - private var loadingNextJob: Job? = null - - val thumbnails = MutableStateFlow>(emptyList()) - val branch = MutableStateFlow(null) - - init { - loadingJob = launchLoadingJob(Dispatchers.Default) { - chaptersLoader.init(checkNotNull(mangaDetails.first { x -> x?.isLoaded == true })) - chaptersLoader.loadSingleChapter(initialChapterId) - updateList() - } - } - - fun loadPrevChapter() { - if (loadingJob.isActive || loadingPrevJob?.isActive == true) { - return - } - loadingPrevJob = loadPrevNextChapter(isNext = false) - } - - fun loadNextChapter() { - if (loadingJob.isActive || loadingNextJob?.isActive == true) { - return - } - loadingNextJob = loadPrevNextChapter(isNext = true) - } - - private fun loadPrevNextChapter(isNext: Boolean): Job = launchLoadingJob(Dispatchers.Default) { - val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId - chaptersLoader.loadPrevNextChapter(mangaDetails.firstNotNull(), currentId, isNext) - updateList() - } - - private fun updateList() { - val snapshot = chaptersLoader.snapshot() - val pages = buildList(snapshot.size + chaptersLoader.size + 2) { - var previousChapterId = 0L - for (page in snapshot) { - if (page.chapterId != previousChapterId) { - chaptersLoader.peekChapter(page.chapterId)?.let { - add(ListHeader(it.name)) - } - previousChapterId = page.chapterId - } - this += PageThumbnail( - isCurrent = page.chapterId == initialChapterId && page.index == currentPageIndex, - page = page, - ) - } - } - thumbnails.value = pages - } -} diff --git a/app/src/main/res/layout-w600dp-land/activity_details.xml b/app/src/main/res/layout-w600dp-land/activity_details.xml index 57b52a3cc..b23fdc17d 100644 --- a/app/src/main/res/layout-w600dp-land/activity_details.xml +++ b/app/src/main/res/layout-w600dp-land/activity_details.xml @@ -3,9 +3,11 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/layout_root" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".details.ui.DetailsActivity"> + tools:context=".details.ui.DetailsActivity" + tools:viewBindingType="android.view.ViewGroup"> + android:layout_height="wrap_content" /> - + + + + + + + + + + + app:layout_constraintStart_toEndOf="@id/imageView_cover" + app:layout_constraintTop_toTopOf="parent" + tools:text="@tools:sample/lorem" /> + + + + + + + android:layout_marginStart="16dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="16dp" + android:isIndicator="true" + android:max="1" + android:numStars="5" + android:stepSize="0.5" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/imageView_cover" + app:layout_constraintTop_toBottomOf="@id/textView_state" + tools:rating="4" /> + + + + + + + + - - - - - - - - - + android:layout_marginEnd="8dp" + android:text="@string/more" + app:layout_constraintBaseline_toBaselineOf="@id/textView_description_title" + app:layout_constraintEnd_toEndOf="parent" /> - + - + + + + +