From 146ba95af65869804c97e00a4b5db1c0ec21dafc Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 11 Dec 2024 12:47:18 +0200 Subject: [PATCH] Details activity improvements --- .../details/domain/ReadingTimeUseCase.kt | 2 +- .../kotatsu/details/ui/DetailsActivity.kt | 91 ++++--------------- .../kotatsu/details/ui/DetailsViewModel.kt | 30 ++---- .../kotatsu/details/ui/ReadButtonDelegate.kt | 29 ++++-- .../kotatsu/details/ui/model/HistoryInfo.kt | 6 +- .../ui/pager/ChaptersPagesViewModel.kt | 1 - .../ui/pager/chapters/ChaptersFragment.kt | 29 +----- .../ui/dialog/DownloadDialogFragment.kt | 1 + app/src/main/res/layout/dialog_download.xml | 11 ++- .../main/res/layout/layout_details_table.xml | 30 +++++- app/src/main/res/menu/popup_read.xml | 5 + app/src/main/res/values/strings.xml | 2 + 12 files changed, 100 insertions(+), 137 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt index 893d70677..e8e211b36 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt @@ -15,7 +15,7 @@ class ReadingTimeUseCase @Inject constructor( private val statsRepository: StatsRepository, ) { - suspend fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? { + suspend operator fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? { if (!settings.isReadingTimeEstimationEnabled) { return null } 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 e93dddd24..db6961067 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 @@ -2,15 +2,9 @@ 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.Gravity -import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup.MarginLayoutParams @@ -19,8 +13,6 @@ 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 @@ -79,7 +71,6 @@ import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.drawable 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.joinToStringWithLimit @@ -168,7 +159,8 @@ class DetailsActivity : TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) viewBinding.containerBottomSheet?.let { sheet -> onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet)) - BottomSheetBehavior.from(sheet).addBottomSheetCallback(DetailsBottomSheetCallback(viewBinding.swipeRefreshLayout)) + BottomSheetBehavior.from(sheet) + .addBottomSheetCallback(DetailsBottomSheetCallback(viewBinding.swipeRefreshLayout)) } TitleExpandListener(viewBinding.textViewTitle).attach() @@ -187,18 +179,14 @@ class DetailsActivity : viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged) viewModel.localSize.observe(this, ::onLocalSizeChanged) viewModel.relatedManga.observe(this, ::onRelatedMangaChanged) - 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 || !it.firstOrNull()?.name.isNullOrEmpty() - // viewBinding.infoLayout?.chipBranch?.isCloseIconVisible = it.size > 1 - // } + viewModel.branches.observe(this) { + infoBinding.textViewTranslation.textAndVisible = it.singleOrNull()?.name + infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible + } viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.onDownloadStarted .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } @@ -379,11 +367,6 @@ class DetailsActivity : } } - private fun onReadingTimeChanged(time: ReadingTime?) { - // TODO - // chip.textAndVisible = time?.formatShort(chip.resources) - } - private fun onLocalSizeChanged(size: Long) { if (size == 0L) { infoBinding.textViewLocal.isVisible = false @@ -518,13 +501,14 @@ class DetailsActivity : } private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) { - textViewChapters.textAndVisible = when { - isLoading -> null + textViewChapters.textAndVisible = if (isLoading) { + null + } else when { info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == -1 -> getString(R.string.error_occurred) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) - } + }.withEstimatedTime(info.estimatedTime) textViewProgress.textAndVisible = if (info.percent <= 0f) { null } else { @@ -541,53 +525,6 @@ class DetailsActivity : // buttonDownload?.isEnabled = info.isValid && info.canDownload } - private fun showBranchPopupMenu(v: View) { - val branches = viewModel.branches.value - if (branches.size <= 1) { - return - } - val menu = PopupMenu(v.context, v) - for ((i, branch) in branches.withIndex()) { - val title = buildSpannedString { - if (branch.isCurrent) { - inSpans( - ImageSpan( - this@DetailsActivity, - 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 if (viewModel.historyInfo.value.isChapterMissing) { @@ -638,6 +575,14 @@ class DetailsActivity : request.enqueueWith(coil) } + private fun String.withEstimatedTime(time: ReadingTime?): String { + if (time == null) { + return this + } + val timeFormatted = time.formatShort(resources) + return getString(R.string.chapters_time_pattern, this, timeFormatted) + } + private class PrefetchObserver( private val context: Context, ) : FlowCollector?> { 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 772647591..712daa0bf 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 @@ -112,12 +112,14 @@ class DetailsViewModel @Inject constructor( history, interactor.observeIncognitoMode(manga), ) { m, b, h, im -> - HistoryInfo(m, b, h, im) - }.stateIn( - scope = viewModelScope + Dispatchers.Default, - started = SharingStarted.Eagerly, - initialValue = HistoryInfo(null, null, null, false), - ) + val estimatedTime = readingTimeUseCase.invoke(m, b, h) + HistoryInfo(m, b, h, im, estimatedTime) + }.withErrorHandling() + .stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.Eagerly, + initialValue = HistoryInfo(null, null, null, false, null), + ) val localSize = mangaDetails .map { it?.local } @@ -170,14 +172,6 @@ class DetailsViewModel @Inject constructor( }.sortedWith(BranchComparator()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) - val readingTime = combine( - mangaDetails, - selectedBranch, - history, - ) { m, b, h -> - readingTimeUseCase.invoke(m, b, h) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null) - val selectedBranchValue: String? get() = selectedBranch.value @@ -222,14 +216,6 @@ class DetailsViewModel @Inject constructor( } } - fun startChaptersSelection() { - val chapters = chapters.value - val chapter = chapters.find { - it.isUnread && !it.isDownloaded - } ?: chapters.firstOrNull() ?: return - onSelectChapter.call(chapter.chapter.id) - } - fun removeFromHistory() { launchJob(Dispatchers.Default) { val handle = historyRepository.delete(setOf(mangaId)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt index 19ba6ab38..48a8d22c5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ReadButtonDelegate.kt @@ -13,15 +13,20 @@ import android.widget.Toast import androidx.appcompat.widget.PopupMenu import androidx.core.text.buildSpannedString import androidx.core.text.inSpans +import androidx.core.view.MenuCompat import androidx.core.view.get +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LifecycleOwner import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialSplitButton import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.details.ui.model.HistoryInfo +import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment import org.koitharu.kotatsu.reader.ui.ReaderActivity class ReadButtonDelegate( @@ -46,10 +51,17 @@ class ReadButtonDelegate( when (item.itemId) { R.id.action_incognito -> openReader(isIncognitoMode = true) R.id.action_forget -> viewModel.removeFromHistory() - else -> { + R.id.action_download -> DownloadDialogFragment.show( + fm = (context.findActivity() as? FragmentActivity)?.supportFragmentManager ?: return false, + manga = setOf(viewModel.getMangaOrNull() ?: return false), + ) + + Menu.NONE -> { val branch = viewModel.branches.value.getOrNull(item.order) ?: return false viewModel.setSelectedBranch(branch.name) } + + else -> return false } return true } @@ -67,11 +79,7 @@ class ReadButtonDelegate( private fun showMenu() { val menu = PopupMenu(context, buttonMenu) menu.inflate(R.menu.popup_read) - menu.menu.setGroupDividerEnabled(true) - menu.menu.populateBranchList() - menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run { - !isIncognitoMode && history != null - } + prepareMenu(menu.menu) menu.setOnMenuItemClickListener(this) menu.setForceShowIcon(true) menu.setOnDismissListener(this) @@ -79,6 +87,15 @@ class ReadButtonDelegate( menu.show() } + private fun prepareMenu(menu: Menu) { + MenuCompat.setGroupDividerEnabled(menu, true) + menu.populateBranchList() + val history = viewModel.historyInfo.value + menu.findItem(R.id.action_incognito)?.isVisible = !history.isIncognitoMode + menu.findItem(R.id.action_forget)?.isVisible = history.history != null + menu.findItem(R.id.action_download)?.isVisible = viewModel.getMangaOrNull()?.isLocal == false + } + private fun openReader(isIncognitoMode: Boolean) { val detailsViewModel = viewModel as? DetailsViewModel ?: return val manga = viewModel.manga.value ?: return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt index a87a32b97..c11f445b5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.details.data.MangaDetails +import org.koitharu.kotatsu.details.data.ReadingTime data class HistoryInfo( val totalChapters: Int, @@ -10,6 +11,7 @@ data class HistoryInfo( val isIncognitoMode: Boolean, val isChapterMissing: Boolean, val canDownload: Boolean, + val estimatedTime: ReadingTime?, ) { val isValid: Boolean get() = totalChapters >= 0 @@ -29,7 +31,8 @@ fun HistoryInfo( manga: MangaDetails?, branch: String?, history: MangaHistory?, - isIncognitoMode: Boolean + isIncognitoMode: Boolean, + estimatedTime: ReadingTime?, ): HistoryInfo { val chapters = if (manga?.chapters?.isEmpty() == true) { emptyList() @@ -48,5 +51,6 @@ fun HistoryInfo( isIncognitoMode = isIncognitoMode, isChapterMissing = history != null && manga?.isLoaded == true && manga.allChapters.none { it.id == history.chapterId }, canDownload = manga?.isLocal == false, + estimatedTime = estimatedTime, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index 0a1b8b4ab..e4ac4dfaf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -61,7 +61,6 @@ abstract class ChaptersPagesViewModel( val readingState = MutableStateFlow(null) val onActionDone = MutableEventFlow() - val onSelectChapter = MutableEventFlow() val onDownloadStarted = MutableEventFlow() val onMangaRemoved = MutableEventFlow() 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 ca719a40d..f365b0d8b 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 @@ -5,19 +5,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.graphics.Insets -import androidx.core.view.ancestors import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager -import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs @@ -30,7 +26,6 @@ import org.koitharu.kotatsu.core.util.ext.dismissParentDialog import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate 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.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration @@ -100,7 +95,6 @@ class ChaptersFragment : viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { binding.textViewHolder.isVisible = it } - viewModel.onSelectChapter.observeEvent(viewLifecycleOwner, ::onSelectChapter) } override fun onDestroyView() { @@ -127,11 +121,11 @@ class ChaptersFragment : } override fun onItemLongClick(item: ChapterListItem, view: View): Boolean { - return selectionController?.onItemLongClick(view, item.chapter.id) ?: false + return selectionController?.onItemLongClick(view, item.chapter.id) == true } override fun onItemContextClick(item: ChapterListItem, view: View): Boolean { - return selectionController?.onItemContextClick(view, item.chapter.id) ?: false + return selectionController?.onItemContextClick(view, item.chapter.id) == true } override fun onChipClick(chip: Chip, data: Any?) { @@ -166,25 +160,6 @@ class ChaptersFragment : } } - private suspend fun onSelectChapter(chapterId: Long) { - if (!isResumed) { - view?.ancestors?.firstNotNullOfOrNull { it as? ViewPager2 }?.setCurrentItem(0, true) - } - val position = withContext(Dispatchers.Default) { - val predicate: (ListModel) -> Boolean = { x -> x is ChapterListItem && x.chapter.id == chapterId } - val items = chaptersAdapter?.observeItems()?.firstOrNull { it.any(predicate) } - items?.indexOfFirst(predicate) ?: -1 - } - if (position >= 0) { - selectionController?.startSelection(chapterId) - val lm = (viewBinding?.recyclerViewChapters?.layoutManager as? LinearLayoutManager) - if (lm != null) { - val offset = resources.getDimensionPixelOffset(R.dimen.chapter_list_item_height) - lm.scrollToPositionWithOffset(position, offset) - } - } - } - private fun onLoadingStateChanged(isLoading: Boolean) { requireViewBinding().progressBar.isVisible = isLoading } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt index e0f6cee7c..304c58ef3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt @@ -75,6 +75,7 @@ class DownloadDialogFragment : AlertDialogFragment(), Vie binding.buttonConfirm.setOnClickListener(this) binding.textViewMore.setOnClickListener(this) + binding.textViewTip.isVisible = viewModel.manga.size == 1 binding.textViewSummary.text = viewModel.manga.joinToStringWithLimit(binding.root.context, 120) { it.title } viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) diff --git a/app/src/main/res/layout/dialog_download.xml b/app/src/main/res/layout/dialog_download.xml index 16841ae30..e02c3fa6b 100644 --- a/app/src/main/res/layout/dialog_download.xml +++ b/app/src/main/res/layout/dialog_download.xml @@ -92,18 +92,21 @@ android:layout_height="wrap_content" /> + android:textAppearance="?attr/textAppearanceBodySmall" + app:drawableStartCompat="@drawable/ic_tap" /> + + + + + app:layout_constraintTop_toBottomOf="@id/textView_translation_label" /> + app:constraint_referenced_ids="textView_source_label,textView_author_label,textView_rating_label,textView_state_label,textView_progress_label,textView_chapters_label,textView_local_label,textView_translation_label" /> diff --git a/app/src/main/res/menu/popup_read.xml b/app/src/main/res/menu/popup_read.xml index aa5232ade..41bea66ae 100644 --- a/app/src/main/res/menu/popup_read.xml +++ b/app/src/main/res/menu/popup_read.xml @@ -12,4 +12,9 @@ android:icon="@drawable/ic_delete" android:title="@string/remove_from_history" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfcd977a1..c98f417bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -772,4 +772,6 @@ Author Rating Source + Translation + %1$s (%2$s)