Details activity improvements

This commit is contained in:
Koitharu
2024-12-11 12:47:18 +02:00
parent ee10b013a1
commit 146ba95af6
12 changed files with 100 additions and 137 deletions

View File

@@ -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
}

View File

@@ -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<List<ChapterListItem>?> {

View File

@@ -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))

View File

@@ -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

View File

@@ -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,
)
}

View File

@@ -61,7 +61,6 @@ abstract class ChaptersPagesViewModel(
val readingState = MutableStateFlow<ReaderState?>(null)
val onActionDone = MutableEventFlow<ReversibleAction>()
val onSelectChapter = MutableEventFlow<Long>()
val onDownloadStarted = MutableEventFlow<Unit>()
val onMangaRemoved = MutableEventFlow<Manga>()

View File

@@ -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
}

View File

@@ -75,6 +75,7 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), 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)

View File

@@ -92,18 +92,21 @@
android:layout_height="wrap_content" />
<TextView
android:id="@+id/textView_tip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:drawablePadding="@dimen/margin_small"
android:paddingHorizontal="@dimen/margin_normal"
android:paddingVertical="@dimen/margin_small"
android:text="@string/chapter_selection_hint"
android:textAppearance="?attr/textAppearanceBodySmall" />
android:textAppearance="?attr/textAppearanceBodySmall"
app:drawableStartCompat="@drawable/ic_tap" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_start"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:checked="true"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"

View File

@@ -75,6 +75,32 @@
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="Author name" />
<TextView
android:id="@+id/textView_translation_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/translation"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_author_label" />
<TextView
android:id="@+id/textView_translation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal"
android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_translation_label"
app:layout_constraintEnd_toEndOf="@id/card_details"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/barrier_table"
tools:text="English" />
<TextView
android:id="@+id/textView_rating_label"
android:layout_width="wrap_content"
@@ -85,7 +111,7 @@
android:text="@string/rating"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_author_label" />
app:layout_constraintTop_toBottomOf="@id/textView_translation_label" />
<RatingBar
android:id="@+id/ratingBar_rating"
@@ -230,6 +256,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="end"
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" />
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" />
</merge>

View File

@@ -12,4 +12,9 @@
android:icon="@drawable/ic_delete"
android:title="@string/remove_from_history" />
<item
android:id="@+id/action_download"
android:icon="@drawable/ic_download"
android:title="@string/download" />
</menu>

View File

@@ -772,4 +772,6 @@
<string name="author">Author</string>
<string name="rating">Rating</string>
<string name="source">Source</string>
<string name="translation">Translation</string>
<string name="chapters_time_pattern" translatable="false">%1$s (%2$s)</string>
</resources>