New details activity

This commit is contained in:
Koitharu
2024-04-03 07:40:01 +03:00
parent 905f565766
commit b27d5607ac
35 changed files with 1757 additions and 42 deletions

View File

@@ -94,6 +94,34 @@
<data android:host="kotatsu.app" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity2"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="kotatsu.app" />
<data android:path="/manga" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kotatsu" />
<data android:host="manga" />
<data android:host="kotatsu.app" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
android:exported="true">

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
@@ -71,7 +72,7 @@ class BookmarksFragment :
) {
super.onViewBindingCreated(binding, savedInstanceState)
selectionController = ListSelectionController(
activity = requireActivity(),
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = BookmarksSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
@@ -100,7 +101,7 @@ class BookmarksFragment :
}
viewModel.onError.observeEvent(
viewLifecycleOwner,
SnackbarErrorObserver(binding.recyclerView, this)
SnackbarErrorObserver(binding.recyclerView, this),
)
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
@@ -206,10 +207,11 @@ class BookmarksFragment :
companion object {
@Deprecated(
"", ReplaceWith(
"",
ReplaceWith(
"BookmarksFragment()",
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment"
)
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
),
)
fun newInstance() = BookmarksFragment()
}

View File

@@ -132,6 +132,8 @@ abstract class BaseActivity<B : ViewBinding> :
} else {
ContextCompat.getColor(this, R.color.kotatsu_m3_background)
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
@@ -140,8 +142,6 @@ abstract class BaseActivity<B : ViewBinding> :
topMargin = insets.top
}
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
}
@CallSuper

View File

@@ -1,10 +1,9 @@
package org.koitharu.kotatsu.core.ui.list
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -20,7 +19,7 @@ private const val KEY_SELECTION = "selection"
private const val PROVIDER_NAME = "selection_decoration"
class ListSelectionController(
private val activity: Activity,
private val appCompatDelegate: AppCompatDelegate,
private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2,
@@ -108,7 +107,7 @@ class ListSelectionController(
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
actionMode = appCompatDelegate.startSupportActionMode(this)
}
}

View File

@@ -1,25 +1,39 @@
package org.koitharu.kotatsu.core.ui.sheet
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.activity.OnBackPressedDispatcher
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.sidesheet.SideSheetDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false
private var defaultStatusBarColor = Color.TRANSPARENT
var viewBinding: B? = null
private set
@@ -31,6 +45,9 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
protected val behavior: AdaptiveSheetBehavior?
get() = AdaptiveSheetBehavior.from(dialog)
@JvmField
val actionModeDelegate = ActionModeDelegate()
val isExpanded: Boolean
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
@@ -60,11 +77,45 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
return if (context.resources.getBoolean(R.bool.is_tablet)) {
SideSheetDialog(context, theme)
val dialog = if (context.resources.getBoolean(R.bool.is_tablet)) {
SideSheetDialogImpl(context, theme)
} else {
BottomSheetDialog(context, theme)
BottomSheetDialogImpl(context, theme)
}
dialog.onBackPressedDispatcher.addCallback(actionModeDelegate)
return dialog
}
@CallSuper
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
actionModeDelegate.onSupportActionModeStarted(mode)
val ctx = requireContext()
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(ctx, com.google.android.material.R.color.m3_appbar_overlay_color),
ctx.getThemeColor(R.attr.m3ColorBackground),
)
} else {
ContextCompat.getColor(ctx, R.color.kotatsu_m3_background)
}
dialog?.window?.let {
defaultStatusBarColor = it.statusBarColor
it.statusBarColor = actionModeColor
}
val insets = ViewCompat.getRootWindowInsets(requireView())
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
dialog?.window?.decorView?.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
@CallSuper
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
actionModeDelegate.onSupportActionModeFinished(mode)
dialog?.window?.statusBarColor = defaultStatusBarColor
}
fun addSheetCallback(callback: AdaptiveSheetCallback) {
@@ -81,6 +132,11 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
val appCompatDialog = dialog as? AppCompatDialog ?: return null
return appCompatDialog.delegate.startSupportActionMode(callback)
}
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
val b = behavior ?: return
if (isExpanded) {
@@ -171,4 +227,38 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
override fun onSlide(sheet: View, slideOffset: Float) {}
}
private inner class SideSheetDialogImpl(context: Context, theme: Int) : SideSheetDialog(context, theme) {
override fun onSupportActionModeStarted(mode: ActionMode?) {
super.onSupportActionModeStarted(mode)
if (mode != null) {
dispatchSupportActionModeStarted(mode)
}
}
override fun onSupportActionModeFinished(mode: ActionMode?) {
super.onSupportActionModeFinished(mode)
if (mode != null) {
dispatchSupportActionModeFinished(mode)
}
}
}
private inner class BottomSheetDialogImpl(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
override fun onSupportActionModeStarted(mode: ActionMode?) {
super.onSupportActionModeStarted(mode)
if (mode != null) {
dispatchSupportActionModeStarted(mode)
}
}
override fun onSupportActionModeFinished(mode: ActionMode?) {
super.onSupportActionModeFinished(mode)
if (mode != null) {
dispatchSupportActionModeFinished(mode)
}
}
}
}

View File

@@ -110,7 +110,7 @@ class ChipsView @JvmOverloads constructor(
chip.isChipIconVisible = false
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false) // TODO remove
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
addView(chip)
return chip

View File

@@ -0,0 +1,144 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Paint
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewOutlineProvider
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.withStyledAttributes
import androidx.core.widget.TextViewCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import com.google.android.material.R as materialR
class ProgressButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : LinearLayoutCompat(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener {
private val textViewTitle = TextView(context)
private val textViewSubtitle = TextView(context)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var progress = 0f
private var colorBase = context.getThemeColor(materialR.attr.colorSecondaryContainer)
private var colorProgress = context.getThemeColor(materialR.attr.colorPrimary)
private var progressAnimator: ValueAnimator? = null
var title: CharSequence?
get() = textViewTitle.textAndVisible
set(value) {
textViewTitle.textAndVisible = value
}
var subtitle: CharSequence?
get() = textViewSubtitle.textAndVisible
set(value) {
textViewSubtitle.textAndVisible = value
}
init {
orientation = VERTICAL
outlineProvider = OutlineProvider()
clipToOutline = true
context.withStyledAttributes(attrs, R.styleable.ProgressButton, defStyleAttr) {
val textAppearanceFallback = androidx.appcompat.R.style.TextAppearance_AppCompat
TextViewCompat.setTextAppearance(
textViewTitle,
getResourceId(R.styleable.ProgressButton_titleTextAppearance, textAppearanceFallback),
)
TextViewCompat.setTextAppearance(
textViewSubtitle,
getResourceId(R.styleable.ProgressButton_subtitleTextAppearance, textAppearanceFallback),
)
textViewTitle.text = getText(R.styleable.ProgressButton_title)
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
colorBase = getColor(R.styleable.ProgressButton_baseColor, colorBase)
colorProgress = getColor(R.styleable.ProgressButton_progressColor, colorProgress)
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
}
addView(textViewTitle, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
addView(
textViewSubtitle,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).also { lp ->
lp.topMargin = context.resources.resolveDp(2)
},
)
paint.style = Paint.Style.FILL
paint.color = colorProgress
applyGravity()
setWillNotDraw(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(colorBase)
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
}
override fun setGravity(gravity: Int) {
super.setGravity(gravity)
if (childCount != 0) {
applyGravity()
}
}
override fun onAnimationUpdate(animation: ValueAnimator) {
progress = animation.animatedValue as Float
invalidate()
}
fun setTitle(@StringRes titleResId: Int) {
textViewTitle.setTextAndVisible(titleResId)
}
fun setSubtitle(@StringRes titleResId: Int) {
textViewSubtitle.setTextAndVisible(titleResId)
}
fun setProgress(value: Float, animate: Boolean) {
progressAnimator?.cancel()
if (animate) {
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener(this@ProgressButton)
start()
}
} else {
progressAnimator = null
progress = value
invalidate()
}
}
private fun applyGravity() {
val value = (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
textViewTitle.gravity = value
textViewSubtitle.gravity = value
}
private class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
}
}
}

View File

@@ -31,10 +31,15 @@ import android.webkit.WebView
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker
@@ -216,6 +221,13 @@ fun Context.findActivity(): Activity? = when (this) {
else -> null
}
fun Fragment.findAppCompatDelegate(): AppCompatDelegate? {
((this as? DialogFragment)?.dialog as? AppCompatDialog)?.run {
return delegate
}
return parentFragment?.findAppCompatDelegate() ?: (activity as? AppCompatActivity)?.delegate
}
fun Context.checkNotificationPermission(channelId: String?): Boolean {
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PERMISSION_GRANTED

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.data.MangaDetails
@@ -35,6 +36,10 @@ class DetailsInteractor @Inject constructor(
.map { it.isNotEmpty() }
}
fun observeFavourite(mangaId: Long): Flow<Set<FavouriteCategory>> {
return favouritesRepository.observeCategories(mangaId)
}
fun observeNewChapters(mangaId: Long): Flow<Int> {
return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
.flatMapLatest { isEnabled ->

View File

@@ -29,6 +29,7 @@ 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 com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
@@ -422,15 +423,25 @@ class DetailsActivity :
companion object {
const val TIP_BUTTON = "btn_read"
private const val KEY_NEW_ACTIVITY = "new_details_screen"
fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity::class.java)
return getActivityIntent(context)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
}
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, DetailsActivity::class.java)
return getActivityIntent(context)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
private fun getActivityIntent(context: Context): Intent {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val useNewActivity = prefs.getBoolean(KEY_NEW_ACTIVITY, false)
return Intent(
context,
if (useNewActivity) DetailsActivity2::class.java else DetailsActivity::class.java,
)
}
}
}

View File

@@ -0,0 +1,728 @@
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.ViewTreeObserver
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
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.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.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
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.drawableStart
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.showDistinct
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
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
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
import com.google.android.material.R as materialR
@AndroidEntryPoint
class DetailsActivity2 :
BaseActivity<ActivityDetailsNewBinding>(),
View.OnClickListener,
View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener,
ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener<Bookmark> {
@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.imageViewCover.setOnClickListener(this)
viewBinding.buttonDescriptionMore.setOnClickListener(this)
viewBinding.buttonBookmarksMore.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, 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.bookmarks.observe(this, ::onBookmarksChanged)
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().showDistinct(supportFragmentManager, "ChaptersPagesSheet")
}
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.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_bookmarks_more -> {
val manga = viewModel.manga.value ?: return
BookmarksSheet.show(supportFragmentManager, manga)
}
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
}
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<ChapterListItem>?) {
// TODO
}
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
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?) {
// TODO
}
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<MangaItemModel>) {
if (related.isEmpty()) {
viewBinding.groupRelated.isVisible = false
return
}
val rv = viewBinding.recyclerViewRelated
@Suppress("UNCHECKED_CAST")
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
.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
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 onBookmarksChanged(bookmarks: List<Bookmark>) {
var adapter = viewBinding.recyclerViewBookmarks.adapter as? BookmarksAdapter
viewBinding.groupBookmarks.isGone = bookmarks.isEmpty()
if (adapter != null) {
adapter.items = bookmarks
} else {
adapter = BookmarksAdapter(coil, this, this)
adapter.items = bookmarks
viewBinding.recyclerViewBookmarks.adapter = adapter
val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing)
viewBinding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing))
}
}
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
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
}
textViewState.apply {
manga.state?.let { state ->
textAndVisible = resources.getString(state.titleResId)
drawableStart = ContextCompat.getDrawable(context, state.iconResId)
} ?: run {
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,
)
}
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())
}
}
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
}
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<List<ChapterListItem>?> {
private var isCalled = false
override suspend fun emit(value: List<ChapterListItem>?) {
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)
}
}
}

View File

@@ -35,6 +35,7 @@ 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,7 +49,7 @@ class DetailsMenuProvider(
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) R.drawable.ic_heart else R.drawable.ic_heart_outline,
if (viewModel.favouriteCategories.value.isNotEmpty()) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
}

View File

@@ -101,8 +101,8 @@ class DetailsViewModel @Inject constructor(
val history = historyRepository.observeOne(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val favouriteCategories = interactor.observeFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptySet())
val isStatsAvailable = statsRepository.observeHasStats(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.details.ui.pager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
import javax.inject.Inject
@AndroidEntryPoint
class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), ActionModeListener {
@Inject
lateinit var settings: AppSettings
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersPagesBinding {
return SheetChaptersPagesBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetChaptersPagesBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = DetailsPagerAdapter2(this, settings)
binding.pager.recyclerView?.isNestedScrollingEnabled = false
binding.pager.offscreenPageLimit = 1
binding.pager.adapter = adapter
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
binding.pager.setCurrentItem(settings.defaultDetailsTab, false)
binding.tabs.isVisible = adapter.itemCount > 1
actionModeDelegate.addListener(this, viewLifecycleOwner)
}
override fun onActionModeStarted(mode: ActionMode) {
setExpanded(true, true)
viewBinding?.run {
pager.isUserInputEnabled = false
tabs.setTabsEnabled(false)
}
}
override fun onActionModeFinished(mode: ActionMode) {
setExpanded(isExpanded, false)
viewBinding?.run {
pager.isUserInputEnabled = true
tabs.setTabsEnabled(true)
}
}
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.details.ui.pager
import androidx.fragment.app.Fragment
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 DetailsPagerAdapter2(
fragment: Fragment,
settings: AppSettings,
) : FragmentStateAdapter(fragment),
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
},
)
}
}

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
@@ -59,7 +60,7 @@ class ChaptersFragment :
super.onViewBindingCreated(binding, savedInstanceState)
chaptersAdapter = ChaptersAdapter(this)
selectionController = ListSelectionController(
activity = requireActivity(),
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = ChaptersSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,

View File

@@ -44,7 +44,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
val downloadsAdapter = DownloadsAdapter(this, coil, this)
val decoration = TypedListSpacingDecoration(this, false)
selectionController = ListSelectionController(
activity = this,
appCompatDelegate = delegate,
decoration = DownloadsSelectionDecoration(this),
registryOwner = this,
callback = this,

View File

@@ -129,7 +129,10 @@ abstract class FavouritesDao {
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
abstract fun observeIds(id: Long): Flow<List<Long>>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0")
@Query("SELECT favourite_categories.* FROM favourites LEFT JOIN favourite_categories ON favourite_categories.category_id = favourites.category_id WHERE favourites.manga_id = :mangaId AND favourites.deleted_at = 0")
abstract fun observeCategories(mangaId: Long): Flow<List<FavouriteCategoryEntity>>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
/** INSERT **/

View File

@@ -107,6 +107,12 @@ class FavouritesRepository @Inject constructor(
return db.getFavouritesDao().observeIds(mangaId).map { it.toSet() }
}
fun observeCategories(mangaId: Long): Flow<Set<FavouriteCategory>> {
return db.getFavouritesDao().observeCategories(mangaId).map {
it.mapTo(LinkedHashSet(it.size)) { x -> x.toFavouriteCategory() }
}
}
suspend fun getCategory(id: Long): FavouriteCategory {
return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
}

View File

@@ -51,7 +51,7 @@ class FavouriteCategoriesActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoriesAdapter(coil, this, this, this)
selectionController = ListSelectionController(
activity = this,
appCompatDelegate = delegate,
decoration = CategoriesSelectionDecoration(this),
registryOwner = this,
callback = CategoriesSelectionCallback(viewBinding.recyclerView, viewModel),

View File

@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -98,7 +99,7 @@ abstract class MangaListFragment :
listAdapter = onCreateAdapter()
spanResolver = MangaListSpanResolver(binding.root.resources)
selectionController = ListSelectionController(
activity = requireActivity(),
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = MangaSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,

View File

@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -72,7 +71,7 @@ open class RemoteListViewModel @Inject constructor(
get() = repository.isSearchSupported
override val content = combine(
mangaList.map { it?.distinctById()?.skipNsfwIfNeeded() },
mangaList.map { it?.skipNsfwIfNeeded() },
listMode,
listError,
hasNextPage,
@@ -136,17 +135,16 @@ open class RemoteListViewModel @Inject constructor(
offset = if (append) mangaList.value?.size ?: 0 else 0,
filter = filterState,
)
val oldList = mangaList.getAndUpdate { oldList ->
if (!append || oldList.isNullOrEmpty()) {
list
} else {
oldList + list
}
}.orEmpty()
val prevList = mangaList.value.orEmpty()
if (!append) {
mangaList.value = list.distinctById()
} else if (list.isNotEmpty()) {
mangaList.value = (prevList + list).distinctById()
}
hasNextPage.value = if (append) {
list.isNotEmpty()
prevList != mangaList.value
} else {
list.size > oldList.size || hasNextPage.value
list.size > prevList.size || hasNextPage.value
}
} catch (e: CancellationException) {
throw e

View File

@@ -9,9 +9,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.require
@@ -46,7 +48,7 @@ class SearchViewModel @Inject constructor(
private var loadingJob: Job? = null
override val content = combine(
mangaList,
mangaList.map { it?.skipNsfwIfNeeded() },
listMode,
listError,
hasNextPage,
@@ -102,14 +104,19 @@ class SearchViewModel @Inject constructor(
listError.value = null
val list = repository.getList(
offset = if (append) mangaList.value?.size ?: 0 else 0,
filter = MangaListFilter.Search(query)
filter = MangaListFilter.Search(query),
)
val prevList = mangaList.value.orEmpty()
if (!append) {
mangaList.value = list
mangaList.value = list.distinctById()
} else if (list.isNotEmpty()) {
mangaList.value = mangaList.value?.plus(list) ?: list
mangaList.value = (prevList + list).distinctById()
}
hasNextPage.value = if (append) {
prevList != mangaList.value
} else {
list.isNotEmpty()
}
hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {

View File

@@ -66,7 +66,7 @@ class MultiSearchActivity :
val sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true)
val selectionDecoration = MangaSelectionDecoration(this)
selectionController = ListSelectionController(
activity = this,
appCompatDelegate = delegate,
decoration = selectionDecoration,
registryOwner = this,
callback = this,

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="?colorSurface" />
</shape>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12.87,15.07L10.33,12.56L10.36,12.53C12.1,10.59 13.34,8.36 14.07,6H17V4H10V2H8V4H1V6H12.17C11.5,7.92 10.44,9.75 9,11.35C8.07,10.32 7.3,9.19 6.69,8H4.69C5.42,9.63 6.42,11.17 7.67,12.56L2.58,17.58L4,19L9,14L12.11,17.11L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.88,17L17.5,12.67L19.12,17H15.88Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17 7V9H15V7H17M13 7V9H7V7H13M13 11H7V13H13V11M15 11V13H17V11H15M21 22L18 20L15 22L12 20L9 22L6 20L3 22V3H21V22M19 18.26V5H5V18.26L6 17.6L9 19.6L12 17.6L15 19.6L18 17.6L19 18.26Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</vector>

View File

@@ -0,0 +1,428 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollIndicators="top"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer"
android:foreground="?selectableItemBackground"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />
<TextView
android:id="@+id/textView_nsfw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset"
android:background="@drawable/bg_chip"
android:backgroundTint="@color/warning"
android:gravity="center"
android:paddingHorizontal="4dp"
android:paddingVertical="2dp"
android:text="@string/nsfw"
android:textAlignment="center"
android:textAppearance="?textAppearanceLabelMedium"
android:textColor="?colorOnError"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="5"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem[12]" />
<TextView
android:id="@+id/textView_state"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?colorTertiary"
android:textStyle="bold"
app:drawableTint="?colorTertiary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
app:layout_constraintWidth_default="wrap"
tools:drawableStart="@drawable/ic_state_ongoing"
tools:text="@string/state_ongoing" />
<RatingBar
android:id="@+id/rating_bar"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="@dimen/margin_normal"
app:constraint_referenced_ids="imageView_cover,rating_bar" />
<include
android:id="@+id/info_layout"
layout="@layout/layout_details_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton
android:id="@+id/button_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="12dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:layout_constraintEnd_toStartOf="@id/button_chapters"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout"
app:progressColor="?colorSecondaryFixedDim"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<ImageView
android:id="@+id/button_chapters"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="?colorSecondaryContainer"
android:contentDescription="@string/chapters"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read"
app:srcCompat="@drawable/ic_list_sheet" />
<TextView
android:id="@+id/textView_description_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_normal"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing"
android:singleLine="true"
android:text="@string/description"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toStartOf="@id/button_description_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_read" />
<Button
android:id="@+id/button_description_more"
style="@style/Widget.Kotatsu.Button.More"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/more"
app:layout_constraintBaseline_toBaselineOf="@id/textView_description_title"
app:layout_constraintEnd_toEndOf="parent" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:paddingStart="@dimen/screen_padding"
android:paddingEnd="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView_description_title" />
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_normal"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2"
android:maxLines="@integer/details_description_lines"
android:paddingBottom="@dimen/margin_normal"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chips_tags"
tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/textView_bookmarks_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_small"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing"
android:singleLine="true"
android:text="@string/bookmarks"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toStartOf="@id/button_bookmarks_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_description" />
<Button
android:id="@+id/button_bookmarks_more"
style="@style/Widget.Kotatsu.Button.More"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/show_all"
app:layout_constraintBaseline_toBaselineOf="@id/textView_bookmarks_title"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_bookmarks"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_bookmarks_title"
tools:listitem="@layout/item_bookmark" />
<TextView
android:id="@+id/textView_scrobbling_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_small"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing"
android:singleLine="true"
android:text="@string/tracking"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toStartOf="@id/button_scrobbling_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerView_bookmarks" />
<Button
android:id="@+id/button_scrobbling_more"
style="@style/Widget.Kotatsu.Button.More"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/manage"
app:layout_constraintBaseline_toBaselineOf="@id/textView_scrobbling_title"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_scrobbling"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:nestedScrollingEnabled="false"
android:orientation="vertical"
android:overScrollMode="never"
android:scrollbars="none"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_scrobbling_title"
tools:itemCount="2"
tools:listitem="@layout/item_scrobbling_info"
tools:visibility="visible" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:showAnimationBehavior="inward"
app:trackCornerRadius="0dp"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_bookmarks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="recyclerView_bookmarks,textView_bookmarks_title,button_bookmarks_more"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_scrobbling"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="recyclerView_scrobbling,textView_scrobbling_title,button_scrobbling_more"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_related_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_small"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing"
android:singleLine="true"
android:text="@string/related_manga"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toStartOf="@id/button_related_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerView_scrobbling" />
<Button
android:id="@+id/button_related_more"
style="@style/Widget.Kotatsu.Button.More"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/show_all"
android:visibility="gone"
app:layout_constraintBaseline_toBaselineOf="@id/textView_related_title"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_related"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_related_title"
tools:listitem="@layout/item_manga_grid" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_related"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="recyclerView_related,textView_related_title,button_related_more"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -21,6 +21,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
app:iconifiedByDefault="false"
app:queryBackground="@null"
app:searchHintIcon="@null"

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.ChipGroup
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/info_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp">
<com.google.android.material.chip.Chip
android:id="@+id/chip_favorite"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_heart_outline"
app:ensureMinTouchTargetSize="false"
tools:text="@string/add_to_favourites" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_source"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_web"
app:ensureMinTouchTargetSize="false"
tools:text="Source"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_author"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_user"
app:ensureMinTouchTargetSize="false"
tools:text="Source"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_size"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_storage"
app:ensureMinTouchTargetSize="false"
tools:text="1.8 GiB"
tools:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_branch"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_language"
app:ensureMinTouchTargetSize="false"
tools:text="English"
tools:visibility="visible" />
</com.google.android.material.chip.ChipGroup>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/chapters" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
style="@style/Widget.Material3.TabLayout.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
app:tabUnboundedRipple="false" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -8,6 +8,7 @@
<attr name="listItemTextViewStyle" />
<attr name="fastScrollerStyle" />
<attr name="tipViewStyle" />
<attr name="progressButtonStyle" />
<attr name="m3ColorBackground" />
<attr name="m3ColorExploreButton" />
<attr name="m3ColorCardBackground" />
@@ -147,4 +148,15 @@
<attr name="android:strokeWidth" />
</declare-styleable>
<declare-styleable name="ProgressButton">
<attr name="titleTextAppearance" />
<attr name="subtitleTextAppearance" />
<attr name="title" />
<attr name="subtitle" />
<attr name="android:max" />
<attr name="android:progress" />
<attr name="baseColor" format="color" />
<attr name="progressColor" format="color" />
</declare-styleable>
</resources>

View File

@@ -642,4 +642,5 @@
<string name="show_pages_thumbs_summary">Enable the \"Pages\" tab on the details screen</string>
<string name="error_no_data_received">No data was received from server</string>
<string name="unsupported_backup_message">Please select a proper Kotatsu backup file</string>
<string name="list_ellipsize_pattern" translatable="false">(+%d)</string>
</resources>

View File

@@ -48,6 +48,11 @@
<PreferenceCategory android:title="@string/details">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="new_details_screen"
android:title="Use new details screen (experimental)" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="pages_tab"