Compare commits

..

17 Commits

Author SHA1 Message Date
Koitharu
ae57561591 Update parsers 2024-01-31 16:16:04 +02:00
Zakhar Timoshenko
2379efc191 TLS 1.3 support for Android < 10
(cherry picked from commit 889b799d8d)
2024-01-31 15:59:04 +02:00
Koitharu
4252ebd24d Bump versionCode 2024-01-24 12:40:18 +02:00
Koitharu
cd0575a524 Update parsers 2024-01-24 12:28:50 +02:00
Koitharu
6eb2608f88 Disable autofill for protect password fields #702
(cherry picked from commit e7c9d1943d)
2024-01-24 12:16:18 +02:00
Koitharu
39e21ff93c Last read order in favorites #705
(cherry picked from commit b1240e7efa)
2024-01-24 12:16:12 +02:00
Koitharu
5ec2eab6b8 Fix webtoon scroll dispatching
(cherry picked from commit a0a72b1192)
2024-01-24 12:15:41 +02:00
Koitharu
850f6c2f3e Fix pages numbers
(cherry picked from commit 83cb35fe6e)
2024-01-24 12:09:11 +02:00
Koitharu
ec53eb9c70 Incognito mode indicator in reader
(cherry picked from commit db1ddf539c)
2024-01-24 12:08:52 +02:00
Koitharu
cdd76f723f Fix favorites counters
(cherry picked from commit d56fc674ab)
2024-01-24 12:08:17 +02:00
Koitharu
a37e8825b0 All favorites item change payload 2024-01-20 09:31:17 +02:00
Koitharu
2450544454 Update parsers
(cherry picked from commit da2ad40adf)
2024-01-20 09:22:28 +02:00
Koitharu
f6a510653e Fix import dialog injection
(cherry picked from commit af5716a8ce)
2024-01-20 09:21:43 +02:00
Koitharu
5990da587c Update supported links domains
(cherry picked from commit a98202e15e)
2024-01-20 09:21:40 +02:00
Koitharu
91e3d2f5db Skip unsupported sources in global search
(cherry picked from commit d6887e2d75)
2024-01-20 09:21:35 +02:00
Koitharu
971c683746 Show all favorites on categories screen
(cherry picked from commit 2a5300a634)
2024-01-20 09:21:27 +02:00
Koitharu
15e9aaab26 Fix pages list scrolling
(cherry picked from commit a1120ea709)
2024-01-20 09:21:18 +02:00
47 changed files with 1010 additions and 338 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 614 versionCode = 617
versionName = '6.6.4' versionName = '6.6.7'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:8e7d7e0bde') { implementation('com.github.KotatsuApp:kotatsu-parsers:57c9d26916') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -141,6 +141,8 @@ dependencies {
compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1' compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1'
ksp 'dev.zacsweers.autoservice:auto-service-ksp:1.1.0' ksp 'dev.zacsweers.autoservice:auto-service-ksp:1.1.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View File

@@ -36,7 +36,7 @@ class KotatsuApp : BaseApp() {
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder() FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath() .penaltyDeath()
.detectFragmentReuse() .detectFragmentReuse()
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2 .detectWrongFragmentContainer()
.detectRetainInstanceUsage() .detectRetainInstanceUsage()
.detectSetUserVisibleHint() .detectSetUserVisibleHint()
.detectFragmentTagUsage() .detectFragmentTagUsage()

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
@@ -19,6 +20,7 @@ import org.acra.config.httpSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -27,6 +29,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.settings.work.WorkScheduleManager import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import java.security.Security
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -66,6 +69,10 @@ open class BaseApp : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales) AppCompatDelegate.setApplicationLocales(settings.appLocales)
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
setupActivityLifecycleCallbacks() setupActivityLifecycleCallbacks()
processLifecycleScope.launch { processLifecycleScope.launch {
val isOriginalApp = withContext(Dispatchers.Default) { val isOriginalApp = withContext(Dispatchers.Default) {

View File

@@ -19,7 +19,8 @@ data class ParcelableChapter(
MangaChapter( MangaChapter(
id = parcel.readLong(), id = parcel.readLong(),
name = parcel.readString().orEmpty(), name = parcel.readString().orEmpty(),
number = parcel.readInt(), number = parcel.readFloat(),
volume = parcel.readInt(),
url = parcel.readString().orEmpty(), url = parcel.readString().orEmpty(),
scanlator = parcel.readString(), scanlator = parcel.readString(),
uploadDate = parcel.readLong(), uploadDate = parcel.readLong(),
@@ -31,7 +32,8 @@ data class ParcelableChapter(
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) { override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
parcel.writeLong(id) parcel.writeLong(id)
parcel.writeString(name) parcel.writeString(name)
parcel.writeInt(number) parcel.writeFloat(number)
parcel.writeInt(volume)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(scanlator) parcel.writeString(scanlator)
parcel.writeLong(uploadDate) parcel.writeLong(uploadDate)

View File

@@ -347,7 +347,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
var historySortOrder: ListSortOrder var historySortOrder: ListSortOrder
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.UPDATED) get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.LAST_READ)
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) } set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
var allFavoritesSortOrder: ListSortOrder var allFavoritesSortOrder: ListSortOrder

View File

@@ -17,7 +17,16 @@ class FastScrollRecyclerView @JvmOverloads constructor(
) : RecyclerView(context, attrs, defStyleAttr) { ) : RecyclerView(context, attrs, defStyleAttr) {
val fastScroller = FastScroller(context, attrs) val fastScroller = FastScroller(context, attrs)
private var applyViewPager2Fix = false var isVP2BugWorkaroundEnabled = false
set(value) {
field = value
if (value && isAttachedToWindow) {
checkIfInVP2()
} else if (!value) {
applyVP2Workaround = false
}
}
private var applyVP2Workaround = false
var isFastScrollerEnabled: Boolean = true var isFastScrollerEnabled: Boolean = true
set(value) { set(value) {
@@ -46,23 +55,29 @@ class FastScrollRecyclerView @JvmOverloads constructor(
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
fastScroller.attachRecyclerView(this) fastScroller.attachRecyclerView(this)
applyViewPager2Fix = ancestors.any { it is ViewPager2 } == true if (isVP2BugWorkaroundEnabled) {
checkIfInVP2()
}
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
fastScroller.detachRecyclerView() fastScroller.detachRecyclerView()
super.onDetachedFromWindow() super.onDetachedFromWindow()
applyViewPager2Fix = false applyVP2Workaround = false
} }
override fun isLayoutRequested(): Boolean { override fun isLayoutRequested(): Boolean {
return if (applyViewPager2Fix) false else super.isLayoutRequested() return if (applyVP2Workaround) false else super.isLayoutRequested()
} }
override fun requestLayout() { override fun requestLayout() {
super.requestLayout() super.requestLayout()
if (applyViewPager2Fix && parent?.isLayoutRequested == true) { if (applyVP2Workaround && parent?.isLayoutRequested == true) {
parent?.requestLayout() parent?.requestLayout()
} }
} }
private fun checkIfInVP2() {
applyVP2Workaround = ancestors.any { it is ViewPager2 } == true
}
} }

View File

@@ -8,9 +8,11 @@ import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.annotation.Px import androidx.annotation.Px
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import com.google.android.material.R as materialR
fun Context.getThemeDrawable( fun Context.getThemeDrawable(
@AttrRes resId: Int, @AttrRes resId: Int,
@@ -75,3 +77,7 @@ fun TypedArray.getDrawableCompat(context: Context, index: Int): Drawable? {
val resId = getResourceId(index, 0) val resId = getResourceId(index, 0)
return if (resId != 0) ContextCompat.getDrawable(context, resId) else null return if (resId != 0) ContextCompat.getDrawable(context, resId) else null
} }
@get:StyleRes
val DIALOG_THEME_CENTERED: Int
inline get() = materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered

View File

@@ -363,8 +363,8 @@ class DetailsActivity :
} }
private fun initPager() { private fun initPager() {
viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false
val adapter = DetailsPagerAdapter(this) val adapter = DetailsPagerAdapter(this)
viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false
viewBinding.pager.offscreenPageLimit = 1 viewBinding.pager.offscreenPageLimit = 1
viewBinding.pager.adapter = adapter viewBinding.pager.adapter = adapter
TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach() TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach()

View File

@@ -59,6 +59,7 @@ class ChaptersFragment :
with(binding.recyclerViewChapters) { with(binding.recyclerViewChapters) {
checkNotNull(selectionController).attachToRecyclerView(this) checkNotNull(selectionController).attachToRecyclerView(this)
setHasFixedSize(true) setHasFixedSize(true)
isNestedScrollingEnabled = false
adapter = chaptersAdapter adapter = chaptersAdapter
} }
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
@@ -83,6 +84,17 @@ class ChaptersFragment :
super.onDestroyView() super.onDestroyView()
} }
override fun onPause() {
// required for BottomSheetBehavior
requireViewBinding().recyclerViewChapters.isNestedScrollingEnabled = false
super.onPause()
}
override fun onResume() {
requireViewBinding().recyclerViewChapters.isNestedScrollingEnabled = true
super.onResume()
}
override fun onItemClick(item: ChapterListItem, view: View) { override fun onItemClick(item: ChapterListItem, view: View) {
if (selectionController?.onItemClick(item.chapter.id) == true) { if (selectionController?.onItemClick(item.chapter.id) == true) {
return return

View File

@@ -88,6 +88,7 @@ class PagesFragment :
addItemDecoration(TypedListSpacingDecoration(context, false)) addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = thumbnailsAdapter adapter = thumbnailsAdapter
setHasFixedSize(true) setHasFixedSize(true)
isNestedScrollingEnabled = false
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(settings.gridSize / 100f, this) spanResolver?.setGridSize(settings.gridSize / 100f, this)
addOnScrollListener(ScrollListener().also { scrollListener = it }) addOnScrollListener(ScrollListener().also { scrollListener = it })
@@ -112,6 +113,17 @@ class PagesFragment :
super.onDestroyView() super.onDestroyView()
} }
override fun onPause() {
// required for BottomSheetBehavior
requireViewBinding().recyclerView.isNestedScrollingEnabled = false
super.onPause()
}
override fun onResume() {
requireViewBinding().recyclerView.isNestedScrollingEnabled = true
super.onResume()
}
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onItemClick(item: PageThumbnail, view: View) { override fun onItemClick(item: PageThumbnail, view: View) {

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
class DownloadsMenuProvider( class DownloadsMenuProvider(
@@ -41,10 +42,8 @@ class DownloadsMenuProvider(
} }
private fun confirmCancelAll() { private fun confirmCancelAll() {
MaterialAlertDialogBuilder( MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
context, .setTitle(R.string.cancel_all)
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
).setTitle(R.string.cancel_all)
.setMessage(R.string.cancel_all_downloads_confirm) .setMessage(R.string.cancel_all_downloads_confirm)
.setIcon(R.drawable.ic_cancel_multiple) .setIcon(R.drawable.ic_cancel_multiple)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
@@ -54,10 +53,8 @@ class DownloadsMenuProvider(
} }
private fun confirmRemoveCompleted() { private fun confirmRemoveCompleted() {
MaterialAlertDialogBuilder( MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
context, .setTitle(R.string.remove_completed)
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
).setTitle(R.string.remove_completed)
.setMessage(R.string.remove_completed_downloads_confirm) .setMessage(R.string.remove_completed_downloads_confirm)
.setIcon(R.drawable.ic_clear_all) .setIcon(R.drawable.ic_clear_all)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -306,7 +306,7 @@ class DownloadsViewModel @Inject constructor(
return chapters.mapNotNullTo(ArrayList(size)) { return chapters.mapNotNullTo(ArrayList(size)) {
if (chapterIds == null || it.id in chapterIds) { if (chapterIds == null || it.id in chapterIds) {
DownloadChapter( DownloadChapter(
number = it.number, number = it.number.toInt(),
name = it.name, name = it.name,
isDownloaded = it.id in localChapters, isDownloaded = it.id in localChapters,
) )

View File

@@ -95,6 +95,22 @@ abstract class FavouritesDao {
return findCoversImpl(query) return findCoversImpl(query)
} }
suspend fun findCovers(order: ListSortOrder, limit: Int): List<Cover> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT manga.cover_url AS url, manga.source AS source FROM favourites " +
"LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE deleted_at = 0 GROUP BY manga.manga_id ORDER BY $orderBy LIMIT ?",
arrayOf<Any>(limit),
)
return findCoversImpl(query)
}
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
abstract fun observeMangaCount(): Flow<Int>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)") @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
abstract suspend fun findAllManga(): List<MangaEntity> abstract suspend fun findAllManga(): List<MangaEntity>
@@ -177,8 +193,8 @@ abstract class FavouritesDao {
ListSortOrder.ALPHABETIC -> "manga.title ASC" ListSortOrder.ALPHABETIC -> "manga.title ASC"
ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC" ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC"
ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
ListSortOrder.UPDATED, // for legacy support
ListSortOrder.PROGRESS -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) DESC" ListSortOrder.PROGRESS -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
ListSortOrder.LAST_READ -> "IFNULL((SELECT updated_at FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
} }

View File

@@ -60,6 +60,11 @@ class FavouritesRepository @Inject constructor(
.flatMapLatest { order -> observeAll(categoryId, order) } .flatMapLatest { order -> observeAll(categoryId, order) }
} }
fun observeMangaCount(): Flow<Int> {
return db.getFavouritesDao().observeMangaCount()
.distinctUntilChanged()
}
fun observeCategories(): Flow<List<FavouriteCategory>> { fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.getFavouriteCategoriesDao().observeAll().mapItems { return db.getFavouriteCategoriesDao().observeAll().mapItems {
it.toFavouriteCategory() it.toFavouriteCategory()
@@ -89,6 +94,10 @@ class FavouritesRepository @Inject constructor(
} }
} }
suspend fun getAllFavoritesCovers(order: ListSortOrder, limit: Int): List<Cover> {
return db.getFavouritesDao().findCovers(order, limit)
}
fun observeCategory(id: Long): Flow<FavouriteCategory?> { fun observeCategory(id: Long): Flow<FavouriteCategory?> {
return db.getFavouriteCategoriesDao().observe(id) return db.getFavouriteCategoriesDao().observe(id)
.map { it?.toFavouriteCategory() } .map { it?.toFavouriteCategory() }

View File

@@ -7,7 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import com.google.android.material.R as materialR import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
class CategoriesSelectionCallback( class CategoriesSelectionCallback(
private val recyclerView: RecyclerView, private val recyclerView: RecyclerView,
@@ -75,7 +75,7 @@ class CategoriesSelectionCallback(
private fun confirmDeleteCategories(ids: Set<Long>, mode: ActionMode) { private fun confirmDeleteCategories(ids: Set<Long>, mode: ActionMode) {
val context = recyclerView.context val context = recyclerView.context
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
.setMessage(R.string.categories_delete_confirm) .setMessage(R.string.categories_delete_confirm)
.setTitle(R.string.remove_category) .setTitle(R.string.remove_category)
.setIcon(R.drawable.ic_delete) .setIcon(R.drawable.ic_delete)

View File

@@ -76,7 +76,13 @@ class FavouriteCategoriesActivity :
} }
} }
override fun onItemClick(item: FavouriteCategory, view: View) { override fun onItemClick(item: FavouriteCategory?, view: View) {
if (item == null) {
if (selectionController.count == 0) {
startActivity(FavouritesActivity.newIntent(view.context))
}
return
}
if (selectionController.onItemClick(item.id)) { if (selectionController.onItemClick(item.id)) {
return return
} }
@@ -92,8 +98,12 @@ class FavouriteCategoriesActivity :
startActivity(intent) startActivity(intent)
} }
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean { override fun onItemLongClick(item: FavouriteCategory?, view: View): Boolean {
return selectionController.onItemLongClick(item.id) return item != null && selectionController.onItemLongClick(item.id)
}
override fun onShowAllClick(isChecked: Boolean) {
viewModel.setAllCategoriesVisible(isChecked)
} }
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean { override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean {

View File

@@ -5,9 +5,11 @@ import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory> { interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory?> {
fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean
fun onEditClick(item: FavouriteCategory, view: View) fun onEditClick(item: FavouriteCategory, view: View)
fun onShowAllClick(isChecked: Boolean)
} }

View File

@@ -5,17 +5,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.domain.model.Cover import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.favourites.ui.categories.adapter.AllCategoriesListModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -30,9 +34,13 @@ class FavouritesCategoriesViewModel @Inject constructor(
private var commitJob: Job? = null private var commitJob: Job? = null
val content = repository.observeCategoriesWithCovers() val content = combine(
.map { it.toUiList() } repository.observeCategoriesWithCovers(),
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) observeAllCategories(),
settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) { isAllFavouritesVisible },
) { cats, all, showAll ->
cats.toUiList(all, showAll)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun deleteCategories(ids: Set<Long>) { fun deleteCategories(ids: Set<Long>) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
@@ -74,21 +82,46 @@ class FavouritesCategoriesViewModel @Inject constructor(
} }
} }
private fun Map<FavouriteCategory, List<Cover>>.toUiList(): List<ListModel> = map { (category, covers) -> private fun Map<FavouriteCategory, List<Cover>>.toUiList(
CategoryListModel( allFavorites: Pair<Int, List<Cover>>,
mangaCount = covers.size, showAll: Boolean
covers = covers.take(3), ): List<ListModel> {
category = category, if (isEmpty()) {
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources, return listOf(
) EmptyState(
}.ifEmpty { icon = R.drawable.ic_empty_favourites,
listOf( textPrimary = R.string.text_empty_holder_primary,
EmptyState( textSecondary = R.string.empty_favourite_categories,
icon = R.drawable.ic_empty_favourites, actionStringRes = 0,
textPrimary = R.string.text_empty_holder_primary, ),
textSecondary = R.string.empty_favourite_categories, )
actionStringRes = 0, }
val result = ArrayList<ListModel>(size + 1)
result.add(
AllCategoriesListModel(
mangaCount = allFavorites.first,
covers = allFavorites.second,
isVisible = showAll,
), ),
) )
mapTo(result) { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
)
}
return result
}
private fun observeAllCategories(): Flow<Pair<Int, List<Cover>>> {
return settings.observeAsFlow(AppSettings.KEY_FAVORITES_ORDER) {
allFavoritesSortOrder
}.mapLatest { order ->
repository.getAllFavoritesCovers(order, limit = 3)
}.combine(repository.observeMangaCount()) { covers, count ->
count to covers
}
} }
} }

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class AllCategoriesListModel(
val mangaCount: Int,
val covers: List<Cover>,
val isVisible: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is AllCategoriesListModel
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is AllCategoriesListModel && previousState.isVisible != isVisible) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}

View File

@@ -19,6 +19,7 @@ class CategoriesAdapter(
init { init {
addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener)) addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener))
addDelegate(ListItemType.NAV_ITEM, allCategoriesAD(coil, lifecycleOwner, onItemClickListener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener)) addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
} }

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding
import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -92,3 +93,68 @@ fun categoryAD(
} }
} }
} }
fun allCategoriesAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<AllCategoriesListModel, ListModel, ItemCategoriesAllBinding>(
{ inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) },
) {
val eventListener = OnClickListener { v ->
if (v.id == R.id.imageView_visible) {
clickListener.onShowAllClick(!item.isVisible)
} else {
clickListener.onItemClick(null, v)
}
}
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
binding.imageViewCover3.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
itemView.setOnClickListener(eventListener)
binding.imageViewVisible.setOnClickListener(eventListener)
bind {
binding.textViewSubtitle.text = if (item.mangaCount == 0) {
getString(R.string.empty)
} else {
context.resources.getQuantityString(
R.plurals.items,
item.mangaCount,
item.mangaCount,
)
}
binding.imageViewVisible.setImageResource(
if (item.isVisible) {
R.drawable.ic_eye
} else {
R.drawable.ic_eye_off
},
)
repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
source(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
}
}

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
@@ -40,10 +41,8 @@ class FavouriteTabPopupMenuProvider(
} }
private fun confirmDelete() { private fun confirmDelete() {
MaterialAlertDialogBuilder( MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
context, .setMessage(R.string.categories_delete_confirm)
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
).setMessage(R.string.categories_delete_confirm)
.setTitle(R.string.remove_category) .setTitle(R.string.remove_category)
.setIcon(R.drawable.ic_delete) .setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.list package org.koitharu.kotatsu.favourites.ui.list
import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -12,6 +13,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -26,6 +28,11 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
val categoryId val categoryId
get() = viewModel.categoryId get() = viewModel.categoryId
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.recyclerView.isVP2BugWorkaroundEnabled = true
}
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {

View File

@@ -35,7 +35,7 @@ abstract class HistoryDao {
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> { fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
val orderBy = when (order) { val orderBy = when (order) {
ListSortOrder.UPDATED -> "history.updated_at DESC" ListSortOrder.LAST_READ -> "history.updated_at DESC"
ListSortOrder.NEWEST -> "history.created_at DESC" ListSortOrder.NEWEST -> "history.created_at DESC"
ListSortOrder.PROGRESS -> "history.percent DESC" ListSortOrder.PROGRESS -> "history.percent DESC"
ListSortOrder.ALPHABETIC -> "manga.title" ListSortOrder.ALPHABETIC -> "manga.title"

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
@@ -161,7 +162,7 @@ class HistoryRepository @Inject constructor(
} }
fun shouldSkip(manga: Manga): Boolean { fun shouldSkip(manga: Manga): Boolean {
return manga.isNsfw && settings.isHistoryExcludeNsfw || settings.isIncognitoModeEnabled return ((manga.source.isNsfw() || manga.isNsfw) && settings.isHistoryExcludeNsfw) || settings.isIncognitoModeEnabled
} }
fun observeShouldSkip(manga: Manga): Flow<Boolean> { fun observeShouldSkip(manga: Manga): Flow<Boolean> {

View File

@@ -8,6 +8,7 @@ import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
@@ -36,7 +37,7 @@ class HistoryListMenuProvider(
private fun showClearHistoryDialog() { private fun showClearHistoryDialog() {
val selectionListener = RememberSelectionDialogListener(2) val selectionListener = RememberSelectionDialogListener(2)
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
.setTitle(R.string.clear_history) .setTitle(R.string.clear_history)
.setSingleChoiceItems( .setSingleChoiceItems(
arrayOf( arrayOf(

View File

@@ -172,7 +172,7 @@ class HistoryListViewModel @Inject constructor(
} }
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) { private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
ListSortOrder.UPDATED -> ListHeader(calculateTimeAgo(updatedAt)) ListSortOrder.LAST_READ -> ListHeader(calculateTimeAgo(updatedAt))
ListSortOrder.NEWEST -> ListHeader(calculateTimeAgo(createdAt)) ListSortOrder.NEWEST -> ListHeader(calculateTimeAgo(createdAt))
ListSortOrder.PROGRESS -> ListHeader( ListSortOrder.PROGRESS -> ListHeader(
when (percent) { when (percent) {

View File

@@ -9,7 +9,6 @@ enum class ListSortOrder(
@StringRes val titleResId: Int, @StringRes val titleResId: Int,
) { ) {
UPDATED(R.string.updated),
NEWEST(R.string.order_added), NEWEST(R.string.order_added),
PROGRESS(R.string.progress), PROGRESS(R.string.progress),
ALPHABETIC(R.string.by_name), ALPHABETIC(R.string.by_name),
@@ -17,14 +16,15 @@ enum class ListSortOrder(
RATING(R.string.by_rating), RATING(R.string.by_rating),
RELEVANCE(R.string.by_relevance), RELEVANCE(R.string.by_relevance),
NEW_CHAPTERS(R.string.new_chapters), NEW_CHAPTERS(R.string.new_chapters),
LAST_READ(R.string.last_read),
; ;
fun isGroupingSupported() = this == UPDATED || this == NEWEST || this == PROGRESS fun isGroupingSupported() = this == LAST_READ || this == NEWEST || this == PROGRESS
companion object { companion object {
val HISTORY: Set<ListSortOrder> = EnumSet.of(UPDATED, NEWEST, PROGRESS, ALPHABETIC, ALPHABETIC_REVERSE, NEW_CHAPTERS) val HISTORY: Set<ListSortOrder> = EnumSet.of(LAST_READ, NEWEST, PROGRESS, ALPHABETIC, ALPHABETIC_REVERSE, NEW_CHAPTERS)
val FAVORITES: Set<ListSortOrder> = EnumSet.of(ALPHABETIC, ALPHABETIC_REVERSE, NEWEST, RATING, NEW_CHAPTERS, PROGRESS) val FAVORITES: Set<ListSortOrder> = EnumSet.of(ALPHABETIC, ALPHABETIC_REVERSE, NEWEST, RATING, NEW_CHAPTERS, PROGRESS, LAST_READ)
val SUGGESTIONS: Set<ListSortOrder> = EnumSet.of(RELEVANCE) val SUGGESTIONS: Set<ListSortOrder> = EnumSet.of(RELEVANCE)
operator fun invoke(value: String, fallback: ListSortOrder) = entries.find(value) ?: fallback operator fun invoke(value: String, fallback: ListSortOrder) = entries.find(value) ?: fallback

View File

@@ -100,6 +100,7 @@ sealed class LocalMangaInput(
id = id, id = id,
name = name, name = name,
number = number, number = number,
volume = volume,
url = url, url = url,
scanlator = scanlator, scanlator = scanlator,
uploadDate = uploadDate, uploadDate = uploadDate,

View File

@@ -9,6 +9,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
@@ -16,6 +17,7 @@ import org.koitharu.kotatsu.databinding.DialogImportBinding
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint
class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener { class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener {
@Inject @Inject

View File

@@ -10,7 +10,6 @@ import android.transition.TransitionManager
import android.transition.TransitionSet import android.transition.TransitionSet
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -40,6 +39,7 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.GridTouchHelper import org.koitharu.kotatsu.core.util.GridTouchHelper
import org.koitharu.kotatsu.core.util.IdlingDetector import org.koitharu.kotatsu.core.util.IdlingDetector
@@ -140,6 +140,7 @@ class ReaderActivity :
viewModel.content.observe(this) { viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value) onLoadingStateChanged(viewModel.isLoading.value)
} }
viewModel.incognitoMode.observe(this, MenuInvalidator(this))
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure) viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn) viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged) viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
@@ -152,6 +153,7 @@ class ReaderActivity :
viewModel.isZoomControlsEnabled.observe(this) { viewModel.isZoomControlsEnabled.observe(this) {
viewBinding.zoomControl.isVisible = it viewBinding.zoomControl.isVisible = it
} }
addMenuProvider(ReaderTopMenuProvider(this, viewModel))
} }
override fun getParentActivityIntent(): Intent? { override fun getParentActivityIntent(): Intent? {
@@ -190,21 +192,12 @@ class ReaderActivity :
viewBinding.slider.isRtl = mode == ReaderMode.REVERSED viewBinding.slider.isRtl = mode == ReaderMode.REVERSED
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_reader_top, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_settings -> { R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this)) startActivity(SettingsActivity.newReaderSettingsIntent(this))
} }
R.id.action_chapters -> {
ChaptersSheet.show(supportFragmentManager)
}
R.id.action_pages_thumbs -> { R.id.action_pages_thumbs -> {
val state = viewModel.getCurrentState() ?: return false val state = viewModel.getCurrentState() ?: return false
PagesThumbnailsSheet.show( PagesThumbnailsSheet.show(

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.reader.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
class ReaderTopMenuProvider(
private val activity: ReaderActivity,
private val viewModel: ReaderViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader_top, menu)
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_incognito)?.isVisible = viewModel.incognitoMode.value
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_chapters -> {
ChaptersSheet.show(activity.supportFragmentManager)
true
}
R.id.action_incognito -> {
showIncognitoModeDialog()
true
}
else -> false
}
}
private fun showIncognitoModeDialog() {
MaterialAlertDialogBuilder(activity, DIALOG_THEME_CENTERED)
.setIcon(R.drawable.ic_incognito)
.setTitle(R.string.incognito_mode)
.setMessage(R.string.incognito_mode_hint)
.setPositiveButton(R.string.got_it, null)
.show()
}
}

View File

@@ -96,6 +96,12 @@ class ReaderViewModel @Inject constructor(
val onShowToast = MutableEventFlow<Int>() val onShowToast = MutableEventFlow<Int>()
val uiState = MutableStateFlow<ReaderUiState?>(null) val uiState = MutableStateFlow<ReaderUiState?>(null)
val incognitoMode = if (isIncognito) {
MutableStateFlow(true)
} else mangaFlow.map {
it != null && historyRepository.shouldSkip(it)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val content = MutableStateFlow(ReaderContent(emptyList(), null)) val content = MutableStateFlow(ReaderContent(emptyList(), null))
val manga: MangaDetails? val manga: MangaDetails?
get() = mangaData.value get() = mangaData.value
@@ -256,7 +262,7 @@ class ReaderViewModel @Inject constructor(
} }
@MainThread @MainThread
fun onCurrentPageChanged(position: Int) { fun onCurrentPageChanged(lowerPos: Int, upperPos: Int) {
val prevJob = stateChangeJob val prevJob = stateChangeJob
val pages = content.value.pages // capture immediately val pages = content.value.pages // capture immediately
stateChangeJob = launchJob(Dispatchers.Default) { stateChangeJob = launchJob(Dispatchers.Default) {
@@ -265,7 +271,8 @@ class ReaderViewModel @Inject constructor(
if (pages.size != content.value.pages.size) { if (pages.size != content.value.pages.size) {
return@launchJob // TODO return@launchJob // TODO
} }
pages.getOrNull(position)?.let { page -> val centerPos = (lowerPos + upperPos) / 2
pages.getOrNull(centerPos)?.let { page ->
currentState.update { cs -> currentState.update { cs ->
cs?.copy(chapterId = page.chapterId, page = page.index) cs?.copy(chapterId = page.chapterId, page = page.index)
} }
@@ -275,14 +282,14 @@ class ReaderViewModel @Inject constructor(
return@launchJob return@launchJob
} }
ensureActive() ensureActive()
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) { if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.last().chapterId, isNext = true) loadPrevNextChapter(pages.last().chapterId, isNext = true)
} }
if (position <= BOUNDS_PAGE_OFFSET) { if (lowerPos <= BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.first().chapterId, isNext = false) loadPrevNextChapter(pages.first().chapterId, isNext = false)
} }
if (pageLoader.isPrefetchApplicable()) { if (pageLoader.isPrefetchApplicable()) {
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT)) pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
} }
} }
} }
@@ -381,7 +388,7 @@ class ReaderViewModel @Inject constructor(
mangaName = manga?.toManga()?.title, mangaName = manga?.toManga()?.title,
branch = chapter?.branch, branch = chapter?.branch,
chapterName = chapter?.name, chapterName = chapter?.name,
chapterNumber = chapter?.number ?: 0, chapterNumber = chapter?.number?.toInt() ?: 0,
chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0, chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0,
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0, totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
currentPage = state?.page ?: 0, currentPage = state?.page ?: 0,

View File

@@ -172,7 +172,8 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
} }
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(reversed(page)) val pos = reversed(page)
viewModel.onCurrentPageChanged(pos, pos)
} }
private fun reversed(position: Int): Int { private fun reversed(position: Int): Int {

View File

@@ -42,7 +42,6 @@ open class PageHolder(
bindingInfo.buttonRetry.setOnClickListener(this) bindingInfo.buttonRetry.setOnClickListener(this)
@Suppress("LeakingThis") @Suppress("LeakingThis")
bindingInfo.buttonErrorDetails.setOnClickListener(this) bindingInfo.buttonErrorDetails.setOnClickListener(this)
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
} }
override fun onResume() { override fun onResume() {
@@ -61,6 +60,7 @@ open class PageHolder(
delegate.reload() delegate.reload()
} }
binding.ssiv.applyDownsampling(isResumed()) binding.ssiv.applyDownsampling(isResumed())
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

View File

@@ -170,7 +170,7 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
} }
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page) viewModel.onCurrentPageChanged(page, page)
} }
companion object { companion object {

View File

@@ -24,7 +24,8 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>() { class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>(),
WebtoonRecyclerView.OnWebtoonScrollListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@@ -46,7 +47,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = readerAdapter adapter = readerAdapter
addOnPageScrollListener(PageScrollListener()) addOnPageScrollListener(this@WebtoonReaderFragment)
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also { recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
addOnScrollListener(it) addOnScrollListener(it)
} }
@@ -70,6 +71,15 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
exceptionResolver = exceptionResolver, exceptionResolver = exceptionResolver,
) )
override fun onScrollChanged(
recyclerView: WebtoonRecyclerView,
dy: Int,
firstVisiblePosition: Int,
lastVisiblePosition: Int,
) {
viewModel.onCurrentPageChanged(firstVisiblePosition, lastVisiblePosition)
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope { override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val setItems = launch { val setItems = launch {
requireAdapter().setItems(pages) requireAdapter().setItems(pages)
@@ -91,7 +101,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
?.restoreScroll(pendingState.scroll) ?.restoreScroll(pendingState.scroll)
} }
} }
notifyPageChanged(position) viewModel.onCurrentPageChanged(position, position)
} else { } else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show() .show()
@@ -121,10 +131,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
viewBinding?.frame?.onZoomOut() viewBinding?.frame?.onZoomOut()
} }
private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page)
}
override fun switchPageBy(delta: Int) { override fun switchPageBy(delta: Int) {
with(requireViewBinding().recyclerView) { with(requireViewBinding().recyclerView) {
if (isAnimationEnabled()) { if (isAnimationEnabled()) {
@@ -147,12 +153,4 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
} }
return true return true
} }
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {
super.onPageChanged(recyclerView, index)
notifyPageChanged(index)
}
}
} }

View File

@@ -6,8 +6,8 @@ import android.view.View
import androidx.core.view.ViewCompat.TYPE_TOUCH import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import java.util.LinkedList import java.util.LinkedList
import java.util.WeakHashMap import java.util.WeakHashMap
@@ -15,7 +15,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) { ) : RecyclerView(context, attrs, defStyleAttr) {
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>()
private val scrollDispatcher = WebtoonScrollDispatcher()
private val detachedViews = WeakHashMap<View, Unit>() private val detachedViews = WeakHashMap<View, Unit>()
private var isFixingScroll: Boolean = false private var isFixingScroll: Boolean = false
@@ -103,22 +104,20 @@ class WebtoonRecyclerView @JvmOverloads constructor(
return 0 return 0
} }
fun addOnPageScrollListener(listener: OnPageScrollListener) { fun addOnPageScrollListener(listener: OnWebtoonScrollListener) {
val list = onPageScrollListeners ?: LinkedList<OnPageScrollListener>().also { onPageScrollListeners = it } onPageScrollListeners.add(listener)
list.add(listener)
} }
fun removeOnPageScrollListener(listener: OnPageScrollListener) { fun removeOnPageScrollListener(listener: OnWebtoonScrollListener) {
onPageScrollListeners?.remove(listener) onPageScrollListeners.remove(listener)
} }
private fun notifyScrollChanged(dy: Int) { private fun notifyScrollChanged(dy: Int) {
val listeners = onPageScrollListeners val listeners = onPageScrollListeners
if (listeners.isNullOrEmpty()) { if (listeners.isEmpty()) {
return return
} }
val centerPosition = findCenterViewPosition() scrollDispatcher.dispatchScroll(this, dy)
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
} }
fun relayoutChildren() { fun relayoutChildren() {
@@ -162,20 +161,30 @@ class WebtoonRecyclerView @JvmOverloads constructor(
else -> false else -> false
} }
abstract class OnPageScrollListener { private class WebtoonScrollDispatcher {
private var lastPosition = NO_POSITION private var firstPos = NO_POSITION
private var lastPos = NO_POSITION
fun dispatchScroll(recyclerView: WebtoonRecyclerView, dy: Int, centerPosition: Int) { fun dispatchScroll(rv: WebtoonRecyclerView, dy: Int) {
onScroll(recyclerView, dy) val lm = rv.layoutManager as? LinearLayoutManager
if (centerPosition != NO_POSITION && centerPosition != lastPosition) { if (lm == null) {
lastPosition = centerPosition firstPos = NO_POSITION
onPageChanged(recyclerView, centerPosition) lastPos = NO_POSITION
return
}
val newFirstPos = lm.findFirstVisibleItemPosition()
val newLastPos = lm.findLastVisibleItemPosition()
if (newFirstPos != firstPos || newLastPos != lastPos) {
firstPos = newFirstPos
lastPos = newLastPos
rv.onPageScrollListeners.forEach { it.onScrollChanged(rv, dy, newFirstPos, newLastPos) }
} }
} }
}
open fun onScroll(recyclerView: WebtoonRecyclerView, dy: Int) = Unit interface OnWebtoonScrollListener {
open fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) = Unit fun onScrollChanged(recyclerView: WebtoonRecyclerView, dy: Int, firstVisiblePosition: Int, lastVisiblePosition: Int)
} }
} }

View File

@@ -113,10 +113,14 @@ class MultiSearchViewModel @Inject constructor(
} }
val semaphore = Semaphore(MAX_PARALLELISM) val semaphore = Semaphore(MAX_PARALLELISM)
for (source in sources) { for (source in sources) {
val repository = mangaRepositoryFactory.create(source)
if (!repository.isSearchSupported) {
continue
}
launch { launch {
val item = runCatchingCancellable { val item = runCatchingCancellable {
semaphore.withPermit { semaphore.withPermit {
mangaRepositoryFactory.create(source).getList(offset = 0, filter = MangaListFilter.Search(q)) repository.getList(offset = 0, filter = MangaListFilter.Search(q))
.toUi(ListMode.GRID, extraProvider) .toUi(ListMode.GRID, extraProvider)
} }
}.fold( }.fold(

View File

@@ -8,6 +8,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.resolve import org.koitharu.kotatsu.core.util.ext.resolve
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -43,7 +44,7 @@ class SearchSuggestionMenuProvider(
} }
private fun clearSearchHistory() { private fun clearSearchHistory() {
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) MaterialAlertDialogBuilder(context, DIALOG_THEME_CENTERED)
.setTitle(R.string.clear_search_history) .setTitle(R.string.clear_search_history)
.setIcon(R.drawable.ic_clear_all) .setIcon(R.drawable.ic_clear_all)
.setMessage(R.string.text_clear_search_history_prompt) .setMessage(R.string.text_clear_search_history_prompt)

View File

@@ -16,8 +16,8 @@ import io.noties.markwon.Markwon
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import com.google.android.material.R as materialR
class AppUpdateDialog(private val activity: AppCompatActivity) { class AppUpdateDialog(private val activity: AppCompatActivity) {
@@ -43,10 +43,7 @@ class AppUpdateDialog(private val activity: AppCompatActivity) {
appendLine() appendLine()
append(Markwon.create(activity).toMarkdown(version.description)) append(Markwon.create(activity).toMarkdown(version.description))
} }
MaterialAlertDialogBuilder( MaterialAlertDialogBuilder(activity, DIALOG_THEME_CENTERED)
activity,
materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
)
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(message) .setMessage(message)
.setIcon(R.drawable.ic_app_update) .setIcon(R.drawable.ic_app_update)

View File

@@ -52,6 +52,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword" android:inputType="textPassword"
android:maxLength="24" android:maxLength="24"
android:singleLine="true" android:singleLine="true"
@@ -80,4 +81,4 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -53,6 +53,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword" android:inputType="textPassword"
android:maxLength="24" android:maxLength="24"
android:singleLine="true" android:singleLine="true"
@@ -94,4 +95,4 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,111 @@
<?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="wrap_content"
android:background="@drawable/list_selector"
android:minHeight="98dp"
android:paddingStart="?android:listPreferredItemPaddingStart"
tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover3"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#99FFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover2"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#4DFFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover1"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginTop="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/all_favourites"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_default="wrap"
tools:text="@tools:sample/lorem[1]" />
<ImageView
android:id="@+id/imageView_visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/show_all"
android:padding="@dimen/margin_normal"
android:src="@drawable/ic_eye"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,13 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item <item
android:id="@+id/action_chapters" android:id="@+id/action_chapters"
android:icon="@drawable/ic_expand_more" android:icon="@drawable/ic_expand_more"
android:title="@string/chapters"
android:orderInCategory="0" android:orderInCategory="0"
android:title="@string/chapters"
app:showAsAction="always" /> app:showAsAction="always" />
</menu> <item
android:id="@+id/action_incognito"
android:icon="@drawable/ic_incognito"
android:title="@string/incognito_mode"
android:visible="false"
app:showAsAction="always" />
</menu>

View File

@@ -559,4 +559,6 @@
<string name="mark_as_completed">Mark as completed</string> <string name="mark_as_completed">Mark as completed</string>
<string name="mark_as_completed_prompt">Mark selected manga as completely read?\n\nWarning: current reading progress will be lost.</string> <string name="mark_as_completed_prompt">Mark selected manga as completely read?\n\nWarning: current reading progress will be lost.</string>
<string name="category_hidden_done">This category was hidden from the main screen and is accessible through Menu → Manage categories</string> <string name="category_hidden_done">This category was hidden from the main screen and is accessible through Menu → Manage categories</string>
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
<string name="last_read">Last read</string>
</resources> </resources>