Fixes and improvements batch

This commit is contained in:
Koitharu
2025-02-26 16:51:33 +02:00
parent b2f0da9245
commit 5e9dc87470
48 changed files with 475 additions and 266 deletions

View File

@@ -1,38 +1,5 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.os.Bundle
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class AllBookmarksActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner,
SnackbarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override val snackbarHost: CoordinatorLayout
get() = viewBinding.root
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, AllBookmarksFragment::class.java, null)
}
}
}
}
class AllBookmarksActivity : FragmentContainerActivity(AllBookmarksFragment::class.java)

View File

@@ -370,6 +370,25 @@ class AppRouter private constructor(
}.show()
}
fun showAuthorDialog(author: String, source: MangaSource) {
buildAlertDialog(contextOrNull() ?: return) {
setTitle(author)
setItems(
arrayOf(
context.getString(R.string.search_on_s, source.getTitle(context)),
context.getString(R.string.search_everywhere),
),
) { _, which ->
when (which) {
0 -> openList(source, MangaListFilter(author = author), null)
1 -> openSearch(author, SearchKind.AUTHOR)
}
}
setNegativeButton(R.string.close, null)
setCancelable(true)
}.show()
}
fun showErrorDialog(error: Throwable, url: String? = null) {
ErrorDetailsDialog().withArgs(2) {
putSerializable(KEY_ERROR, error)

View File

@@ -252,7 +252,7 @@ class ExternalPluginContentSource(
publicUrl = getString(COLUMN_PUBLIC_URL),
rating = getFloat(COLUMN_RATING),
isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false),
coverUrl = getString(COLUMN_COVER_URL),
coverUrl = getStringOrNull(COLUMN_COVER_URL),
tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet {
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
MangaTag(key = parts.first, title = parts.second, source = source)

View File

@@ -20,7 +20,7 @@ class ExternalPluginCursor(private val source: ExternalMangaSource, cursor: Curs
return when {
columnIndex < 0 -> null
isNull(columnIndex) -> null
else -> getString(columnIndex)
else -> getString(columnIndex).takeUnless { it == "null" }
}
}

View File

@@ -35,7 +35,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
@Inject
lateinit var settings: AppSettings
override val recyclerView: RecyclerView
override val recyclerView: RecyclerView?
get() = listView
override fun onAttach(context: Context) {

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.core.ui
import android.os.Bundle
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@AndroidEntryPoint
abstract class FragmentContainerActivity(private val fragmentClass: Class<out Fragment>) :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner,
SnackbarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override val snackbarHost: CoordinatorLayout
get() = viewBinding.root
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, fragmentClass, getFragmentExtras())
}
}
}
protected open fun getFragmentExtras(): Bundle? = intent.extras
}

View File

@@ -4,6 +4,7 @@ import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import androidx.annotation.GravityInt
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
@@ -14,19 +15,30 @@ import org.koitharu.kotatsu.core.util.ext.start
class InsetsToMarginsListener(
@GravityInt
private val sides: Int,
private val baseMargins: Insets,
) : OnApplyWindowInsetsListener {
private val insetType = WindowInsetsCompat.Type.systemBars()
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val barsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val barsInsets = insets.getInsets(insetType)
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (sides and Gravity.START == Gravity.START) marginStart = barsInsets.start(v)
if (sides and Gravity.TOP == Gravity.TOP) topMargin = barsInsets.top
if (sides and Gravity.END == Gravity.END) marginEnd = barsInsets.end(v)
if (sides and Gravity.BOTTOM == Gravity.BOTTOM) bottomMargin = barsInsets.bottom
if (sides and Gravity.START == Gravity.START) {
marginStart = barsInsets.start(v) + baseMargins.start(v)
}
if (sides and Gravity.TOP == Gravity.TOP) {
topMargin = barsInsets.top + baseMargins.top
}
if (sides and Gravity.END == Gravity.END) {
marginEnd = barsInsets.end(v) + baseMargins.end(v)
}
if (sides and Gravity.BOTTOM == Gravity.BOTTOM) {
bottomMargin = barsInsets.bottom + baseMargins.bottom
}
}
return WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars(),
insetType,
barsInsets.consumeRelative(
v,
start = sides and Gravity.START == Gravity.START,

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.ui.util
import android.view.Gravity
import android.view.View
import androidx.annotation.GravityInt
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import org.koitharu.kotatsu.core.util.ext.consumeRelative
@@ -12,19 +13,42 @@ import org.koitharu.kotatsu.core.util.ext.start
class InsetsToPaddingListener(
@GravityInt
private val sides: Int,
private val basePaddings: Insets,
) : OnApplyWindowInsetsListener {
private val insetType = WindowInsetsCompat.Type.systemBars()
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val barsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val barsInsets = insets.getInsets(insetType)
v.setPaddingRelative(
/* start = */ if (sides and Gravity.START == Gravity.START) barsInsets.start(v) else v.paddingStart,
/* top = */ if (sides and Gravity.TOP == Gravity.TOP) barsInsets.top else v.paddingTop,
/* end = */ if (sides and Gravity.END == Gravity.END) barsInsets.end(v) else v.paddingEnd,
/* bottom = */ if (sides and Gravity.BOTTOM == Gravity.BOTTOM) barsInsets.bottom else v.paddingBottom,
/* start = */
if (sides and Gravity.START == Gravity.START) {
barsInsets.start(v) + basePaddings.start(v)
} else {
v.paddingStart
},
/* top = */
if (sides and Gravity.TOP == Gravity.TOP) {
barsInsets.top + basePaddings.top
} else {
v.paddingTop
},
/* end = */
if (sides and Gravity.END == Gravity.END) {
barsInsets.end(v) + basePaddings.end(v)
} else {
v.paddingEnd
},
/* bottom = */
if (sides and Gravity.BOTTOM == Gravity.BOTTOM) {
barsInsets.bottom + basePaddings.bottom
} else {
v.paddingBottom
},
)
return WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars(),
insetType,
barsInsets.consumeRelative(
v,
start = sides and Gravity.START == Gravity.START,

View File

@@ -4,5 +4,5 @@ import androidx.recyclerview.widget.RecyclerView
interface RecyclerViewOwner {
val recyclerView: RecyclerView
val recyclerView: RecyclerView?
}

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.LifecycleOwner
import coil3.Extras
import coil3.ImageLoader
@@ -116,8 +116,8 @@ fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Bui
0.25f,
)
return placeholder(AnimatedPlaceholderDrawable(context))
.fallback(ColorDrawable(context.getThemeColor(materialR.attr.colorSurfaceContainer)))
.error(ColorDrawable(errorColor))
.fallback(context.getThemeColor(materialR.attr.colorSurfaceContainer).toDrawable())
.error(errorColor.toDrawable())
}
private fun ImageView.ScaleType.toCoilScale(): Scale = if (this == ImageView.ScaleType.CENTER_CROP) {

View File

@@ -1,6 +1,10 @@
package org.koitharu.kotatsu.core.util.ext
import android.util.DisplayMetrics
import androidx.core.view.doOnNextLayout
import androidx.core.view.isEmpty
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
@@ -52,7 +56,7 @@ fun <T> RecyclerView.ViewHolder.getItem(clazz: Class<T>): T? {
val RecyclerView.isScrolledToTop: Boolean
get() {
if (childCount == 0) {
if (isEmpty()) {
return true
}
val holder = findViewHolderForAdapterPosition(0)
@@ -72,3 +76,35 @@ val RecyclerView.LayoutManager?.isLayoutReversed
is StaggeredGridLayoutManager -> reverseLayout
else -> false
}
// https://medium.com/flat-pack-tech/quickly-scroll-to-the-top-of-a-recyclerview-da15b717f3c4
fun RecyclerView.smoothScrollToTop() {
val layoutManager = layoutManager as? LinearLayoutManager ?: return
if (!context.isAnimationsEnabled) {
layoutManager.scrollToPositionWithOffset(0, 0)
return
}
val smoothScroller = object : LinearSmoothScroller(context) {
init {
targetPosition = 0
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?) =
super.calculateSpeedPerPixel(displayMetrics) / DEFAULT_SPEED_FACTOR
}
val jumpBeforeScroll = layoutManager.findFirstVisibleItemPosition() > DEFAULT_JUMP_THRESHOLD
if (jumpBeforeScroll) {
layoutManager.scrollToPositionWithOffset(DEFAULT_JUMP_THRESHOLD, 0)
doOnNextLayout {
layoutManager.startSmoothScroll(smoothScroller)
}
} else {
layoutManager.startSmoothScroll(smoothScroller)
}
}
private const val DEFAULT_JUMP_THRESHOLD = 30
private const val DEFAULT_SPEED_FACTOR = 1f

View File

@@ -9,10 +9,13 @@ import android.widget.Checkable
import androidx.annotation.GravityInt
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.core.view.descendants
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
@@ -72,6 +75,11 @@ fun ViewPager2.findCurrentViewHolder(): ViewHolder? {
return recyclerView?.findViewHolderForAdapterPosition(currentItem)
}
fun FragmentManager.findCurrentPagerFragment(pager: ViewPager2): Fragment? {
val currentId = pager.adapter?.getItemId(pager.currentItem) ?: pager.currentItem
return findFragmentByTag("f$currentId")
}
fun View.resetTransformations() {
alpha = 1f
translationX = 0f
@@ -187,12 +195,18 @@ fun Chip.setProgressIcon() {
progressDrawable.start()
}
private fun View.marginsSnapshot(): Insets = (layoutParams as? ViewGroup.MarginLayoutParams)?.let { lp ->
Insets.of(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
} ?: Insets.NONE
private fun View.paddingSnapshot(): Insets = Insets.of(paddingLeft, paddingTop, paddingRight, paddingBottom)
fun View.consumeInsetsAsPadding(@GravityInt sides: Int) = ViewCompat.setOnApplyWindowInsetsListener(
this,
InsetsToPaddingListener(sides),
InsetsToPaddingListener(sides, paddingSnapshot()),
)
fun View.consumeInsetsAsMargins(@GravityInt sides: Int) = ViewCompat.setOnApplyWindowInsetsListener(
this,
InsetsToMarginsListener(sides),
InsetsToMarginsListener(sides, marginsSnapshot()),
)

View File

@@ -4,6 +4,8 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.data.filterChapters
data class MangaDetails(
@@ -29,6 +31,12 @@ data class MangaDetails(
val local: LocalManga?
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
val coverUrl: String?
get() = manga.largeCoverUrl
.ifNullOrEmpty { manga.largeCoverUrl }
.ifNullOrEmpty { localManga?.manga?.coverUrl }
?.nullIfEmpty()
fun toManga() = manga
fun filterChapters(branch: String?) = MangaDetails(

View File

@@ -6,6 +6,7 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import coil3.request.CachePolicy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
@@ -15,6 +16,7 @@ import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.nav.MangaIntent
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.peek
@@ -40,7 +42,7 @@ class DetailsLoadUseCase @Inject constructor(
private val newChaptersUseCaseProvider: Provider<CheckNewChaptersUseCase>,
) {
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
operator fun invoke(intent: MangaIntent, force: Boolean): Flow<MangaDetails> = channelFlow {
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
"Cannot resolve intent $intent"
}.let { m ->
@@ -59,7 +61,7 @@ class DetailsLoadUseCase @Inject constructor(
null
}
try {
val details = getDetails(manga)
val details = getDetails(manga, force)
launch { mangaDataRepository.updateChapters(details) }
launch { updateTracker(details) }
send(
@@ -92,9 +94,13 @@ class DetailsLoadUseCase @Inject constructor(
}
}
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {
private suspend fun getDetails(seed: Manga, force: Boolean) = runCatchingCancellable {
val repository = mangaRepositoryFactory.create(seed.source)
repository.getDetails(seed)
if (repository is CachingMangaRepository) {
repository.getDetails(seed, if (force) CachePolicy.WRITE_ONLY else CachePolicy.ENABLED)
} else {
repository.getDetails(seed)
}
}.recoverNotNull { e ->
if (e is NotFoundException) {
recoverUseCase(seed)

View File

@@ -111,7 +111,6 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.domain.SearchKind
import javax.inject.Inject
import kotlin.math.roundToInt
import com.google.android.material.R as materialR
@@ -171,6 +170,7 @@ class DetailsActivity :
val appRouter = router
viewModel.mangaDetails.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.coverUrl.observe(this, ::loadCover)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.onError
.filterNot { appRouter.isChapterPagesSheetShown() }
@@ -217,8 +217,9 @@ class DetailsActivity :
override fun onClick(v: View) {
when (v.id) {
R.id.textView_author -> {
val author = viewModel.manga.value?.author ?: return
router.openSearch(author, SearchKind.AUTHOR)
val manga = viewModel.manga.value
val author = manga?.author ?: return
router.showAuthorDialog(author, manga.source)
}
R.id.textView_source -> {
@@ -239,7 +240,7 @@ class DetailsActivity :
R.id.imageView_cover -> {
val manga = viewModel.manga.value ?: return
router.openImage(
url = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } ?: return,
url = viewModel.coverUrl.value ?: return,
source = manga.source,
anchor = v,
)
@@ -443,7 +444,6 @@ class DetailsActivity :
private fun onMangaUpdated(details: MangaDetails) {
val manga = details.toManga()
loadCover(manga)
with(viewBinding) {
textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle
@@ -560,8 +560,7 @@ class DetailsActivity :
viewBinding.chipsTags.setChips(listMapper.mapTags(manga.tags))
}
private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
private fun loadCover(imageUrl: String?) {
val lastResult = CoilUtils.result(viewBinding.imageViewCover)
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
return
@@ -571,10 +570,9 @@ class DetailsActivity :
.size(CoverSizeResolver(viewBinding.imageViewCover))
.scale(Scale.FILL)
.data(imageUrl)
.mangaSourceExtra(manga.source)
.mangaSourceExtra(viewModel.getMangaOrNull()?.source)
.crossfade(this)
.lifecycle(this)
.placeholderMemoryCacheKey(manga.coverUrl)
val previousDrawable = lastResult?.drawable
if (previousDrawable != null) {
request.fallback(previousDrawable)

View File

@@ -176,7 +176,7 @@ class DetailsViewModel @Inject constructor(
get() = selectedBranch.value
init {
loadingJob = doLoad()
loadingJob = doLoad(force = false)
launchJob(Dispatchers.Default) {
val manga = mangaDetails.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
val h = history.firstOrNull()
@@ -192,7 +192,7 @@ class DetailsViewModel @Inject constructor(
fun reload() {
loadingJob.cancel()
loadingJob = doLoad()
loadingJob = doLoad(force = true)
}
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
@@ -223,8 +223,8 @@ class DetailsViewModel @Inject constructor(
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
detailsLoadUseCase.invoke(intent)
private fun doLoad(force: Boolean) = launchLoadingJob(Dispatchers.Default) {
detailsLoadUseCase.invoke(intent, force)
.onEachWhile {
if (it.allChapters.isNotEmpty()) {
val manga = it.toManga()

View File

@@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
@@ -20,13 +21,16 @@ import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.findCurrentPagerFragment
import org.koitharu.kotatsu.core.util.ext.menuView
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.core.util.ext.smoothScrollToTop
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.ReadButtonDelegate
@@ -34,7 +38,10 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import javax.inject.Inject
@AndroidEntryPoint
class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), ActionModeListener, AdaptiveSheetCallback {
class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(),
TabLayout.OnTabSelectedListener,
ActionModeListener,
AdaptiveSheetCallback {
@Inject
lateinit var settings: AppSettings
@@ -63,6 +70,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
binding.pager.adapter = adapter
binding.pager.doOnPageChanged(::onPageChanged)
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
binding.tabs.addOnTabSelectedListener(this)
binding.pager.setCurrentItem(defaultTab, false)
binding.tabs.isVisible = adapter.itemCount > 1
@@ -109,6 +117,17 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
viewBinding?.toolbar?.menuView?.isVisible = state != STATE_COLLAPSED
}
override fun onTabSelected(tab: TabLayout.Tab?) = Unit
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) {
val f = childFragmentManager.findCurrentPagerFragment(
viewBinding?.pager ?: return,
) as? RecyclerViewOwner ?: return
f.recyclerView?.smoothScrollToTop()
}
override fun expandAndLock() {
super.expandAndLock()
adjustLockState()

View File

@@ -71,6 +71,10 @@ abstract class ChaptersPagesViewModel(
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val coverUrl = mangaDetails.map { x -> x?.coverUrl }
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val isChaptersReversed = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_REVERSE_CHAPTERS,

View File

@@ -11,6 +11,7 @@ import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
@@ -26,6 +27,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.ui.util.PagerNestedScrollHelper
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
@@ -43,6 +45,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark>,
RecyclerViewOwner,
ListSelectionController.Callback {
private val activityViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
@@ -54,6 +57,9 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
@Inject
lateinit var settings: AppSettings
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
private var bookmarksAdapter: BookmarksAdapter? = null
private var spanResolver: GridSpanResolver? = null
private var selectionController: ListSelectionController? = null

View File

@@ -12,6 +12,7 @@ import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@@ -25,6 +26,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.ui.util.PagerNestedScrollHelper
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
@@ -48,6 +50,7 @@ class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>,
OnApplyWindowInsetsListener,
RecyclerViewOwner,
ChipsView.OnChipClickListener {
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
@@ -55,6 +58,9 @@ class ChaptersFragment :
private var chaptersAdapter: ChaptersAdapter? = null
private var selectionController: ListSelectionController? = null
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerViewChapters
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
@@ -55,6 +56,7 @@ import kotlin.math.roundToInt
class PagesFragment :
BaseFragment<FragmentPagesBinding>(),
OnListItemClickListener<PageThumbnail>,
RecyclerViewOwner,
ListSelectionController.Callback {
@Inject
@@ -77,6 +79,9 @@ class PagesFragment :
private val spanSizeLookup = SpanSizeLookup()
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageSaveHelper = pageSaveHelperFactory.create(this)

View File

@@ -1,30 +1,5 @@
package org.koitharu.kotatsu.details.ui.related
import android.os.Bundle
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class RelatedMangaActivity : BaseActivity<ActivityContainerBinding>(), AppBarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, RelatedListFragment::class.java, intent.extras)
}
}
}
}
class RelatedMangaActivity : FragmentContainerActivity(RelatedListFragment::class.java)

View File

@@ -60,8 +60,8 @@ class ExploreFragment :
private var exploreAdapter: ExploreAdapter? = null
private var sourceSelectionController: ListSelectionController? = null
override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding {
return FragmentExploreBinding.inflate(inflater, container, false)

View File

@@ -1,33 +1,18 @@
package org.koitharu.kotatsu.favourites.ui
import android.os.Bundle
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
@AndroidEntryPoint
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
class FavouritesActivity : FragmentContainerActivity(FavouritesListFragment::class.java) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val categoryTitle = intent.getStringExtra(AppRouter.KEY_TITLE)
if (categoryTitle != null) {
title = categoryTitle
}
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(AppRouter.KEY_ID, NO_ID))
replace(R.id.container, fragment)
}
}
}
}

View File

@@ -8,7 +8,9 @@ import android.view.ViewStub
import androidx.appcompat.view.ActionMode
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@@ -16,9 +18,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.findCurrentPagerFragment
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -30,14 +34,20 @@ import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import javax.inject.Inject
@AndroidEntryPoint
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(), ActionModeListener,
ViewStub.OnInflateListener, View.OnClickListener {
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(),
ActionModeListener,
RecyclerViewOwner,
ViewStub.OnInflateListener,
View.OnClickListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel: FavouritesContainerViewModel by viewModels()
override val recyclerView: RecyclerView?
get() = (findCurrentFragment() as? RecyclerViewOwner)?.recyclerView
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -103,4 +113,10 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
stubEmpty.isVisible = isEmpty
}
}
private fun findCurrentFragment(): Fragment? {
return childFragmentManager.findCurrentPagerFragment(
viewBinding?.pager ?: return null,
)
}
}

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.withArgs
@@ -90,10 +91,9 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
companion object {
const val NO_ID = 0L
const val ARG_CATEGORY_ID = "category_id"
fun newInstance(categoryId: Long) = FavouritesListFragment().withArgs(1) {
putLong(ARG_CATEGORY_ID, categoryId)
putLong(AppRouter.KEY_ID, categoryId)
}
}
}

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -25,7 +26,6 @@ import org.koitharu.kotatsu.core.util.ext.flattenLatest
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
import org.koitharu.kotatsu.list.domain.ListFilterOption
@@ -54,7 +54,7 @@ class FavouritesListViewModel @Inject constructor(
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
val categoryId: Long = savedStateHandle[AppRouter.KEY_ID] ?: NO_ID
private val quickFilter = quickFilterFactory.create(categoryId)
private val refreshTrigger = MutableStateFlow(Any())
private val limit = MutableStateFlow(PAGE_SIZE)

View File

@@ -267,6 +267,25 @@ class FilterCoordinator @Inject constructor(
currentListFilter.value = value
}
fun setAdjusted(value: MangaListFilter) {
var newFilter = value
if (!newFilter.author.isNullOrEmpty() && !capabilities.isAuthorSearchSupported) {
newFilter = newFilter.copy(
query = newFilter.author,
author = null,
)
}
if (!capabilities.isSearchSupported && !newFilter.query.isNullOrEmpty()) {
newFilter = newFilter.copy(
query = null,
)
}
if (!newFilter.query.isNullOrEmpty() && !newFilter.hasNonSearchOptions() && !capabilities.isSearchWithFiltersSupported) {
newFilter = MangaListFilter(query = newFilter.query)
}
set(newFilter)
}
fun setQuery(value: String?) {
val newQuery = value?.trim()?.nullIfEmpty()
currentListFilter.update { oldValue ->

View File

@@ -1,33 +1,5 @@
package org.koitharu.kotatsu.history.ui
import android.os.Bundle
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class HistoryActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = HistoryListFragment()
replace(R.id.container, fragment)
}
}
}
}
class HistoryActivity : FragmentContainerActivity(HistoryListFragment::class.java)

View File

@@ -12,6 +12,7 @@ import androidx.annotation.CallSuper
import androidx.appcompat.view.ActionMode
import androidx.collection.ArraySet
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
@@ -31,6 +32,7 @@ import org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ShareHelper
@@ -62,6 +64,7 @@ abstract class MangaListFragment :
BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback,
MangaListListener,
RecyclerViewOwner,
SwipeRefreshLayout.OnRefreshListener,
ListSelectionController.Callback,
FastScroller.FastScrollListener {
@@ -87,6 +90,9 @@ abstract class MangaListFragment :
protected val selectedItems: Set<Manga>
get() = collectSelectedItems()
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -159,8 +165,7 @@ abstract class MangaListFragment :
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
if (selectionController?.onItemClick(manga.id) != true) {
// TODO dialog
router.openList(tag)
router.showTagDialog(tag)
}
}

View File

@@ -26,8 +26,7 @@ import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.NavItem
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.smoothScrollToTop
import org.koitharu.kotatsu.explore.ui.ExploreFragment
import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
@@ -64,15 +63,8 @@ class MainNavigationDelegate(
override fun onNavigationItemReselected(item: MenuItem) {
val fragment = fragmentManager.findFragmentByTag(TAG_PRIMARY)
if (fragment == null || fragment !is RecyclerViewOwner || fragment.view == null) {
return
}
val recyclerView = fragment.recyclerView
if (recyclerView.context.isAnimationsEnabled) {
recyclerView.smoothScrollToPosition(0)
} else {
recyclerView.firstVisibleItemPosition = 0
}
val recyclerView = (fragment as? RecyclerViewOwner)?.recyclerView ?: return
recyclerView.smoothScrollToTop()
}
override fun handleOnBackPressed() {

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
@@ -20,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.observe
@@ -41,6 +43,7 @@ class ProtectActivity :
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
setContentView(ActivityProtectBinding.inflate(layoutInflater))
viewBinding.root.consumeInsetsAsPadding(Gravity.FILL)
viewBinding.editPassword.setOnEditorActionListener(this)
viewBinding.editPassword.addTextChangedListener(this)
viewBinding.buttonNext.setOnClickListener(this)

View File

@@ -394,7 +394,7 @@ class ReaderViewModel @Inject constructor(
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
val details = detailsLoadUseCase.invoke(intent, force = false).first { x -> x.isLoaded }
mangaDetails.value = details
chaptersLoader.init(details)
val manga = details.toManga()

View File

@@ -4,11 +4,13 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.Gravity
import android.view.View
import androidx.core.net.toUri
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding
import org.koitharu.kotatsu.parsers.util.urlEncoded
@@ -19,6 +21,7 @@ class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>(), View.OnClick
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater))
viewBinding.root.consumeInsetsAsPadding(Gravity.FILL)
viewBinding.buttonCancel.setOnClickListener(this)
viewBinding.buttonDone.setOnClickListener(this)
viewBinding.editEmail.addTextChangedListener(this)

View File

@@ -218,7 +218,7 @@ class MangaListActivity :
filterOwner.filterCoordinator.setSortOrder(sortOrder)
}
if (filter != null) {
filterOwner.filterCoordinator.set(filter)
filterOwner.filterCoordinator.setAdjusted(filter)
}
}
}

View File

@@ -32,8 +32,8 @@ class NavConfigFragment : BaseFragment<FragmentSettingsSourcesBinding>(), Recycl
private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModels<NavConfigViewModel>()
override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreateViewBinding(
inflater: LayoutInflater,

View File

@@ -4,6 +4,7 @@ import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
@@ -17,6 +18,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivitySetupProtectBinding
@@ -37,6 +39,7 @@ class ProtectSetupActivity :
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
setContentView(ActivitySetupProtectBinding.inflate(layoutInflater))
viewBinding.root.consumeInsetsAsPadding(Gravity.FILL)
viewBinding.editPassword.addTextChangedListener(this)
viewBinding.editPassword.setOnEditorActionListener(this)
viewBinding.buttonNext.setOnClickListener(this)

View File

@@ -39,7 +39,7 @@ class ReaderTapGridConfigActivity : BaseActivity<ActivityReaderTapActionsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityReaderTapActionsBinding.inflate(layoutInflater))
viewBinding.root.consumeInsetsAsPadding(Gravity.START or Gravity.END or Gravity.BOTTOM or Gravity.TOP)
viewBinding.root.consumeInsetsAsPadding(Gravity.FILL)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
controls[TapGridArea.TOP_LEFT] = viewBinding.textViewTopLeft
controls[TapGridArea.TOP_CENTER] = viewBinding.textViewTopCenter

View File

@@ -56,8 +56,8 @@ class SourcesManageFragment :
private var sourcesAdapter: SourceConfigAdapter? = null
private val viewModel by viewModels<SourcesManageViewModel>()
override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreateViewBinding(
inflater: LayoutInflater,

View File

@@ -40,6 +40,7 @@ abstract class StatsDao {
favouriteCategories: Set<Long>
): Map<MangaEntity, Long> {
val conditions = ArrayList<String>()
conditions.add("(SELECT deleted_at FROM history WHERE history.manga_id = stats.manga_id) = 0")
conditions.add("stats.started_at >= $fromDate")
if (favouriteCategories.isNotEmpty()) {
val ids = favouriteCategories.joinToString(",")

View File

@@ -1,32 +1,5 @@
package org.koitharu.kotatsu.suggestions.ui
import android.os.Bundle
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class SuggestionsActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, SuggestionsFragment::class.java, null)
}
}
}
}
class SuggestionsActivity : FragmentContainerActivity(SuggestionsFragment::class.java)

View File

@@ -6,6 +6,7 @@ import android.accounts.AccountManager
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.Toast
@@ -20,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.observe
@@ -40,6 +42,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySyncAuthBinding.inflate(layoutInflater))
viewBinding.root.consumeInsetsAsPadding(Gravity.FILL)
accountAuthenticatorResponse =
intent.getParcelableExtraCompat(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
accountAuthenticatorResponse?.onRequestContinued()

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
@@ -38,13 +39,18 @@ import javax.inject.Inject
class FeedFragment :
BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback,
MangaListListener, SwipeRefreshLayout.OnRefreshListener {
RecyclerViewOwner,
MangaListListener,
SwipeRefreshLayout.OnRefreshListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<FeedViewModel>()
override val recyclerView: RecyclerView?
get() = viewBinding?.recyclerView
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@@ -1,33 +1,5 @@
package org.koitharu.kotatsu.tracker.ui.updates
import android.os.Bundle
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class UpdatesActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = UpdatesFragment.newInstance()
replace(R.id.container, fragment)
}
}
}
}
class UpdatesActivity : FragmentContainerActivity(UpdatesFragment::class.java)

View File

@@ -38,9 +38,4 @@ class UpdatesFragment : MangaListFragment() {
else -> super.onActionItemClicked(controller, mode, item)
}
}
companion object {
fun newInstance() = UpdatesFragment()
}
}

View File

@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:orientation="horizontal"
android:paddingHorizontal="32dp">
<ImageView
@@ -16,32 +16,38 @@
android:scaleType="fitCenter"
tools:src="@drawable/ic_empty_favourites" />
<TextView
android:id="@+id/textPrimary"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:gravity="center"
android:textAppearance="?attr/textAppearanceTitleLarge"
tools:text="@tools:sample/lorem[3]" />
android:orientation="vertical">
<TextView
android:id="@+id/textSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[15]" />
<TextView
android:id="@+id/textPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:textAppearance="?attr/textAppearanceTitleLarge"
tools:text="@tools:sample/lorem[3]" />
<Button
android:id="@+id/button_retry"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
tools:text="@string/try_again"
tools:visibility="visible" />
<TextView
android:id="@+id/textSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:maxWidth="320dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[15]" />
<Button
android:id="@+id/button_retry"
style="?materialButtonTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
tools:text="@string/try_again"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toStartOf="@id/guideline_center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="noScroll"
tools:title="@string/reading_stats" />
</com.google.android.material.appbar.AppBarLayout>
<HorizontalScrollView
android:id="@+id/scrollView_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingHorizontal="12dp"
android:scrollIndicators="start"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_center"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.chip.ChipGroup
android:id="@+id/layout_chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/chip_period"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/week"
app:chipIcon="@drawable/ic_history" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<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_toBottomOf="@id/appbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:showAnimationBehavior="inward"
app:trackCornerRadius="0dp"
tools:visibility="visible" />
<org.koitharu.kotatsu.stats.ui.views.PieChartView
android:id="@+id/chart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toStartOf="@id/guideline_center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:overScrollMode="ifContentScrolls"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_center"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:itemCount="4"
tools:listitem="@layout/item_stats" />
<ViewStub
android:id="@+id/stub_empty"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout="@layout/item_empty_state"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -37,6 +37,7 @@
<Button
android:id="@+id/button_retry"
style="?materialButtonTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"