Merge branch 'devel' into Instant_history

# Conflicts:
#	app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Date.kt
#	app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt
pull/332/head
Isira Seneviratne 3 years ago
commit fd3d6b98ee

@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 543 versionCode 545
versionName '5.1-b2' versionName '5.1.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -42,6 +42,7 @@ android {
} }
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
main.java.srcDirs += 'src/main/kotlin/'
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
@ -60,7 +61,7 @@ android {
} }
lint { lint {
abortOnError true abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged' disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
} }
testOptions { testOptions {
unitTests.includeAndroidResources true unitTests.includeAndroidResources true
@ -81,15 +82,15 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:96b9ac36f3') { implementation('com.github.KotatsuApp:kotatsu-parsers:cae7073f87') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.1' implementation 'androidx.activity:activity-ktx:1.7.1'
implementation 'androidx.fragment:fragment-ktx:1.5.7' implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
@ -125,8 +126,8 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.45' implementation 'com.google.dagger:hilt-android:2.46.1'
kapt 'com.google.dagger:hilt-compiler:2.45' kapt 'com.google.dagger:hilt-compiler:2.46.1'
implementation 'androidx.hilt:hilt-work:1.0.0' implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0'
@ -143,18 +144,18 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227' testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'androidx.room:room-testing:2.5.1' androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.45' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.45' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
} }

@ -8,7 +8,7 @@
public static void checkParameterIsNotNull(...); public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...); public static void checkNotNullParameter(...);
} }
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment -keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; } -keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.** -dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import junit.framework.TestCase.* import junit.framework.TestCase.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
@ -11,8 +10,9 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@HiltAndroidTest @HiltAndroidTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)

@ -0,0 +1,37 @@
package org.koitharu.kotatsu.util
import android.util.Log
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
class LoggingAdapterDataObserver(
private val tag: String,
) : AdapterDataObserver() {
override fun onChanged() {
Log.d(tag, "onChanged()")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
}
override fun onStateRestorationPolicyChanged() {
Log.d(tag, "onStateRestorationPolicyChanged()")
}
}

@ -0,0 +1,3 @@
package org.koitharu.kotatsu.util.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

@ -1,3 +0,0 @@
package org.koitharu.kotatsu.utils.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

@ -1,5 +0,0 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LifecycleService
abstract class BaseService : LifecycleService()

@ -1,5 +0,0 @@
package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize)

@ -1,6 +0,0 @@
package org.koitharu.kotatsu.details.domain
class BranchComparator : Comparator<String?> {
override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2)
}

@ -1,162 +0,0 @@
package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@ViewModelScoped
class MangaDetailsDelegate @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val intent = MangaIntent(savedStateHandle)
private val mangaData = MutableStateFlow(intent.manga)
val selectedBranch = MutableStateFlow<String?>(null)
// Remote manga for saved and saved for remote
val relatedManga = MutableStateFlow<Manga?>(null)
val manga: StateFlow<Manga?>
get() = mangaData
val mangaId = intent.manga?.id ?: intent.mangaId
suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
mangaData.value = manga
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga
relatedManga.value = runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
mangaRepositoryFactory.create(m.source).getDetails(m)
} else {
localMangaRepository.findSavedManga(manga)?.manga
}
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
}
fun mapChapters(
manga: Manga?,
related: Manga?,
history: MangaHistory?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val chapters = manga?.chapters ?: return emptyList()
val relatedChapters = related?.chapters
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
} else {
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
}
}
private fun mapChapters(
chapters: List<MangaChapter>,
downloadedChapters: List<MangaChapter>?,
currentId: Long?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val result = ArrayList<ChapterListItem>(chapters.size)
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
for (i in chapters.indices) {
val chapter = chapters[i]
if (chapter.branch != branch) {
continue
}
result += chapter.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = downloadedIds?.contains(chapter.id) == true,
)
}
if (result.size < chapters.size / 2) {
result.trimToSize()
}
return result
}
private fun mapChaptersWithSource(
chapters: List<MangaChapter>,
sourceChapters: List<MangaChapter>,
currentId: Long?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
val result = ArrayList<ChapterListItem>(sourceChapters.size)
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
val firstNewIndex = sourceChapters.size - newCount
for (i in sourceChapters.indices) {
val chapter = sourceChapters[i]
val localChapter = chaptersMap.remove(chapter.id)
if (chapter.branch != branch) {
continue
}
result += localChapter?.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = false,
) ?: chapter.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = true,
isDownloaded = false,
)
}
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
result.ensureCapacity(result.size + chaptersMap.size)
chaptersMap.values.mapNotNullTo(result) {
if (it.branch == branch) {
it.toListItem(
isCurrent = false,
isUnread = true,
isNew = false,
isMissing = false,
isDownloaded = false,
)
} else {
null
}
}
result.sortBy { it.chapter.number }
}
if (result.size < sourceChapters.size / 2) {
result.trimToSize()
}
return result
}
}

@ -1,45 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.replaceWith
class BranchesAdapter : BaseAdapter() {
private val dataSet = ArrayList<String?>()
override fun getCount(): Int {
return dataSet.size
}
override fun getItem(position: Int): Any? {
return dataSet[position]
}
override fun getItemId(position: Int): Long {
return dataSet[position].hashCode().toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch, parent, false)
(view as TextView).text = dataSet[position]
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch_dropdown, parent, false)
(view as TextView).text = dataSet[position]
return view
}
fun setItems(items: Collection<String?>) {
dataSet.replaceWith(items)
notifyDataSetChanged()
}
}

@ -1,56 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description()
}
when (item.status) {
FLAG_UNREAD -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
}
FLAG_CURRENT -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
else -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
}
}
val isMissing = item.hasFlag(FLAG_MISSING)
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
}
}

@ -1,6 +0,0 @@
package org.koitharu.kotatsu.list.ui.model
object LoadingFooter : ListModel {
override fun equals(other: Any?): Boolean = other === LoadingFooter
}

@ -1,8 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import org.koitharu.kotatsu.parsers.model.MangaPage
fun interface OnPageSelectListener {
fun onPageSelected(page: MangaPage)
}

@ -1,11 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
data class PageThumbnail(
val number: Int,
val isCurrent: Boolean,
val repository: MangaRepository,
val page: MangaPage
)

@ -1,146 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class PagesThumbnailsSheet :
BaseBottomSheet<SheetPagesBinding>(),
OnListItemClickListener<MangaPage>,
BottomSheetHeaderBar.OnExpansionChangeListener {
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var pageLoader: PageLoader
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private lateinit var thumbnails: List<PageThumbnail>
private var spanResolver: MangaListSpanResolver? = null
private var currentPageIndex = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
if (pages.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
}
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
val repository = mangaRepositoryFactory.create(pages.first().source)
thumbnails = pages.mapIndexed { i, x ->
PageThumbnail(
number = i + 1,
isCurrent = i == currentPageIndex,
repository = repository,
page = x,
)
}
}
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
return SheetPagesBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
spanResolver = MangaListSpanResolver(view.resources)
with(binding.headerBar) {
title = arguments?.getString(ARG_TITLE)
subtitle = null
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
}
with(binding.recyclerView) {
addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
)
adapter = PageThumbnailAdapter(
dataSet = thumbnails,
coil = coil,
scope = viewLifecycleScope,
loader = pageLoader,
clickListener = this@PagesThumbnailsSheet,
)
addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(settings.gridSize / 100f, this)
if (currentPageIndex > 0) {
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
spanResolver = null
}
override fun onItemClick(item: MangaPage, view: View) {
(
(parentFragment as? OnPageSelectListener)
?: (activity as? OnPageSelectListener)
)?.run {
onPageSelected(item)
dismiss()
}
}
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
if (isExpanded) {
headerBar.subtitle = resources.getQuantityString(
R.plurals.pages,
thumbnails.size,
thumbnails.size,
)
} else {
headerBar.subtitle = null
}
}
companion object {
private const val ARG_PAGES = "pages"
private const val ARG_TITLE = "title"
private const val ARG_CURRENT = "current"
private const val TAG = "PagesThumbnailsSheet"
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
PagesThumbnailsSheet().withArgs(3) {
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
putString(ARG_TITLE, title)
putInt(ARG_CURRENT, currentPage)
}.show(fm, TAG)
}
}

@ -1,91 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.graphics.drawable.Drawable
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
import com.google.android.material.R as materialR
fun pageThumbnailAD(
coil: ImageLoader,
scope: CoroutineScope,
loader: PageLoader,
clickListener: OnListItemClickListener<MangaPage>,
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
) {
var job: Job? = null
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
width = gridWidth,
height = (gridWidth / 13f * 18f).toInt(),
)
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
item.page.preview?.let { url ->
coil.execute(
ImageRequest.Builder(context)
.data(url)
.tag(item.page.source)
.size(thumbSize)
.scale(Scale.FILL)
.allowRgb565(true)
.build(),
).drawable
}?.let { drawable ->
return@withContext drawable
}
val file = loader.loadPage(item.page, force = false)
coil.execute(
ImageRequest.Builder(context)
.data(file)
.size(thumbSize)
.decodeRegion(0)
.allowRgb565(isLowRamDevice(context))
.build(),
).drawable
}
binding.root.setOnClickListener {
clickListener.onItemClick(item.page, itemView)
}
bind {
job?.cancel()
binding.imageViewThumb.setImageDrawable(null)
with(binding.textViewNumber) {
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
text = (item.number).toString()
}
job = scope.launch {
val drawable = runCatchingCancellable {
loadPageThumbnail(item)
}.getOrNull()
binding.imageViewThumb.setImageDrawable(drawable)
}
}
onViewRecycled {
job?.cancel()
job = null
binding.imageViewThumb.setImageDrawable(null)
}
}

@ -1,23 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import kotlinx.coroutines.CoroutineScope
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
class PageThumbnailAdapter(
dataSet: List<PageThumbnail>,
coil: ImageLoader,
scope: CoroutineScope,
loader: PageLoader,
clickListener: OnListItemClickListener<MangaPage>
) : ListDelegationAdapter<List<PageThumbnail>>() {
init {
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
setItems(dataSet)
}
}

@ -1,22 +0,0 @@
package org.koitharu.kotatsu.utils
import android.graphics.drawable.Drawable
import androidx.preference.Preference
import coil.target.Target
class PreferenceIconTarget(
private val preference: Preference,
) : Target {
override fun onError(error: Drawable?) {
preference.icon = error
}
override fun onStart(placeholder: Drawable?) {
preference.icon = placeholder
}
override fun onSuccess(result: Drawable) {
preference.icon = result
}
}

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import android.view.View
import androidx.core.graphics.Insets
fun Insets.end(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) left else right
}
fun Insets.start(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) right else left
}

@ -1,19 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
internal val RecyclerView.LayoutManager?.firstVisibleItemPosition
get() = when (this) {
is LinearLayoutManager -> findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0]
else -> 0
}
internal val RecyclerView.LayoutManager?.isLayoutReversed
get() = when (this) {
is LinearLayoutManager -> reverseLayout
is StaggeredGridLayoutManager -> reverseLayout
else -> false
}

@ -20,12 +20,12 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.WorkServiceStopHelper
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp

@ -5,7 +5,6 @@ import androidx.room.withTransaction
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.toBookmark import org.koitharu.kotatsu.bookmarks.data.toBookmark
import org.koitharu.kotatsu.bookmarks.data.toBookmarks import org.koitharu.kotatsu.bookmarks.data.toBookmarks
@ -14,9 +13,10 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
@Reusable @Reusable

@ -3,16 +3,14 @@ package org.koitharu.kotatsu.bookmarks.ui
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@ -24,10 +22,10 @@ class BookmarksActivity :
SnackbarOwner { SnackbarOwner {
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = binding.appbar get() = viewBinding.appbar
override val snackbarHost: CoordinatorLayout override val snackbarHost: CoordinatorLayout
get() = binding.root get() = viewBinding.root
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -43,7 +41,7 @@ class BookmarksActivity :
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding( viewBinding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
) )

@ -16,19 +16,21 @@ import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.data.ids import org.koitharu.kotatsu.bookmarks.data.ids
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@ -37,8 +39,6 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -56,12 +56,12 @@ class BookmarksFragment :
private var adapter: BookmarksGroupAdapter? = null private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null private var selectionController: SectionedSelectionController<Manga>? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
return FragmentListSimpleBinding.inflate(inflater, container, false) return FragmentListSimpleBinding.inflate(inflater, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
selectionController = SectionedSelectionController( selectionController = SectionedSelectionController(
activity = requireActivity(), activity = requireActivity(),
owner = this, owner = this,
@ -77,7 +77,7 @@ class BookmarksFragment :
) )
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration) binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
@ -114,7 +114,7 @@ class BookmarksFragment :
override fun onFastScrollStop(fastScroller: FastScroller) = Unit override fun onFastScrollStop(fastScroller: FastScroller) = Unit
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) { override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
binding.recyclerView.invalidateNestedItemDecorations() requireViewBinding().recyclerView.invalidateNestedItemDecorations()
} }
override fun onCreateActionMode( override fun onCreateActionMode(
@ -149,10 +149,10 @@ class BookmarksFragment :
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext()) ): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding( requireViewBinding().recyclerView.updatePadding(
bottom = insets.bottom, bottom = insets.bottom,
) )
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> { requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom bottomMargin = insets.bottom
} }
} }

@ -4,8 +4,8 @@ import android.content.Context
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.util.ext.getItem
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.utils.ext.getItem
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
@ -14,5 +14,4 @@ class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
return item.pageId return item.pageId
} }
} }

@ -7,17 +7,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
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
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel

@ -4,16 +4,16 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.source
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
fun bookmarkListAD( fun bookmarkListAD(
coil: ImageLoader, coil: ImageLoader,

@ -4,8 +4,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
class BookmarksAdapter( class BookmarksAdapter(
coil: ImageLoader, coil: ImageLoader,
@ -13,7 +13,7 @@ class BookmarksAdapter(
clickListener: OnListItemClickListener<Bookmark>, clickListener: OnListItemClickListener<Bookmark>,
) : AsyncListDifferDelegationAdapter<Bookmark>( ) : AsyncListDifferDelegationAdapter<Bookmark>(
DiffCallback(), DiffCallback(),
bookmarkListAD(coil, lifecycleOwner, clickListener) bookmarkListAD(coil, lifecycleOwner, clickListener),
) { ) {
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() { private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {

@ -6,20 +6,20 @@ import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.source
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
fun bookmarksGroupAD( fun bookmarksGroupAD(
coil: ImageLoader, coil: ImageLoader,

@ -5,16 +5,17 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
@ -54,6 +55,10 @@ class BookmarksGroupAdapter(
oldItem.manga.id == newItem.manga.id oldItem.manga.id == newItem.manga.id
} }
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass else -> oldItem.javaClass == newItem.javaClass
} }
} }

@ -12,8 +12,9 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -24,18 +25,20 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityBrowserBinding.inflate(layoutInflater)) if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
return
}
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
} }
with(binding.webView.settings) { with(viewBinding.webView.settings) {
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome userAgentString = CommonHeadersInterceptor.userAgentChrome
} }
binding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView) onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback) onBackPressedDispatcher.addCallback(onBackPressedCallback)
if (savedInstanceState != null) { if (savedInstanceState != null) {
return return
@ -48,18 +51,18 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
url, url,
) )
binding.webView.loadUrl(url) viewBinding.webView.loadUrl(url)
} }
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
binding.webView.saveState(outState) viewBinding.webView.saveState(outState)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState) super.onRestoreInstanceState(savedInstanceState)
binding.webView.restoreState(savedInstanceState) viewBinding.webView.restoreState(savedInstanceState)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -70,14 +73,14 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
binding.webView.stopLoading() viewBinding.webView.stopLoading()
finishAfterTransition() finishAfterTransition()
true true
} }
R.id.action_browser -> { R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(binding.webView.url) intent.data = Uri.parse(viewBinding.webView.url)
try { try {
startActivity(Intent.createChooser(intent, item.title)) startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
@ -89,22 +92,22 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
override fun onPause() { override fun onPause() {
binding.webView.onPause() viewBinding.webView.onPause()
super.onPause() super.onPause()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.webView.onResume() viewBinding.webView.onResume()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
binding.webView.destroy() viewBinding.webView.destroy()
} }
override fun onLoadingStateChanged(isLoading: Boolean) { override fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading viewBinding.progressBar.isVisible = isLoading
} }
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
@ -117,10 +120,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding( viewBinding.appbar.updatePadding(
top = insets.top, top = insets.top,
) )
binding.root.updatePadding( viewBinding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
bottom = insets.bottom, bottom = insets.bottom,

@ -1,10 +1,8 @@
package org.koitharu.kotatsu.browser.cloudflare package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebSettings import android.webkit.WebSettings
@ -14,13 +12,13 @@ import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import okhttp3.Headers import okhttp3.Headers
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -39,14 +37,13 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
url = requireArguments().getString(ARG_URL).orEmpty() url = requireArguments().getString(ARG_URL).orEmpty()
} }
override fun onInflateView( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
) = FragmentCloudflareBinding.inflate(inflater, container, false) ) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled") override fun onViewBindingCreated(binding: FragmentCloudflareBinding, savedInstanceState: Bundle?) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState)
super.onViewCreated(view, savedInstanceState)
with(binding.webView.settings) { with(binding.webView.settings) {
javaScriptEnabled = true javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT cacheMode = WebSettings.LOAD_DEFAULT
@ -64,8 +61,8 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
} }
override fun onDestroyView() { override fun onDestroyView() {
binding.webView.stopLoading() requireViewBinding().webView.stopLoading()
binding.webView.destroy() requireViewBinding().webView.destroy()
onBackPressedCallback = null onBackPressedCallback = null
super.onDestroyView() super.onDestroyView()
} }
@ -76,18 +73,18 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
override fun onDialogCreated(dialog: AlertDialog) { override fun onDialogCreated(dialog: AlertDialog) {
super.onDialogCreated(dialog) super.onDialogCreated(dialog)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also { onBackPressedCallback = WebViewBackPressedCallback(requireViewBinding().webView).also {
dialog.onBackPressedDispatcher.addCallback(it) dialog.onBackPressedDispatcher.addCallback(it)
} }
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.webView.onResume() requireViewBinding().webView.onResume()
} }
override fun onPause() { override fun onPause() {
binding.webView.onPause() requireViewBinding().webView.onPause()
super.onPause() super.onPause()
} }
@ -97,7 +94,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
} }
override fun onPageLoaded() { override fun onPageLoaded() {
bindingOrNull()?.progressBar?.isInvisible = true viewBinding?.progressBar?.isInvisible = true
} }
override fun onCheckPassed() { override fun onCheckPassed() {

@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
import android.text.Html import android.text.Html
import android.util.AndroidRuntimeException
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import coil.ComponentRegistry import coil.ComponentRegistry
@ -23,50 +22,42 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.CookieJar
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.* import org.koitharu.kotatsu.core.network.*
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.activityManager
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
import org.koitharu.kotatsu.utils.ext.activityManager
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
import org.koitharu.kotatsu.widget.WidgetUpdater import org.koitharu.kotatsu.widget.WidgetUpdater
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface AppModule { interface AppModule {
@Binds
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
@Binds @Binds
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
@ -75,47 +66,6 @@ interface AppModule {
companion object { companion object {
@Provides
@Singleton
fun provideCookieJar(
@ApplicationContext context: Context
): MutableCookieJar = try {
AndroidCookieJar()
} catch (e: AndroidRuntimeException) {
// WebView is not available
PreferencesCookieJar(context)
}
@Provides
@Singleton
fun provideOkHttpClient(
localStorageManager: LocalStorageManager,
commonHeadersInterceptor: CommonHeadersInterceptor,
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
cookieJar: CookieJar,
settings: AppSettings,
): OkHttpClient {
val cache = localStorageManager.createHttpCache()
return OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
dns(DoHManager(cache, settings))
if (settings.isSSLBypassEnabled) {
bypassSSLErrors()
}
cache(cache)
addInterceptor(GZipInterceptor())
addInterceptor(commonHeadersInterceptor)
addInterceptor(CloudFlareInterceptor())
addInterceptor(mirrorSwitchInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}
}.build()
}
@Provides @Provides
@Singleton @Singleton
fun provideNetworkState( fun provideNetworkState(
@ -134,14 +84,10 @@ interface AppModule {
@Singleton @Singleton
fun provideCoil( fun provideCoil(
@ApplicationContext context: Context, @ApplicationContext context: Context,
okHttpClient: OkHttpClient, @MangaHttpClient okHttpClient: OkHttpClient,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
pagesCache: PagesCache,
): ImageLoader { ): ImageLoader {
val httpClientFactory = {
okHttpClient.newBuilder()
.cache(null)
.build()
}
val diskCacheFactory = { val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder() DiskCache.Builder()
@ -149,19 +95,20 @@ interface AppModule {
.build() .build()
} }
return ImageLoader.Builder(context) return ImageLoader.Builder(context)
.okHttpClient(httpClientFactory) .okHttpClient(okHttpClient.newBuilder().cache(null).build())
.interceptorDispatcher(Dispatchers.Default) .interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO) .fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default) .decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default) .transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(isLowRamDevice(context)) .allowRgb565(context.isLowRamDevice())
.components( .components(
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(SvgDecoder.Factory()) .add(SvgDecoder.Factory())
.add(CbzFetcher.Factory()) .add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory)) .add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
.build(), .build(),
).build() ).build()
} }

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10

@ -5,10 +5,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.Closeable import okio.Closeable
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.format
import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.core.zip.ZipOutput
import org.koitharu.kotatsu.utils.ext.format
import java.io.File import java.io.File
import java.util.* import java.util.Date
import java.util.Locale
import java.util.zip.Deflater import java.util.zip.Deflater
class BackupZipOutput(val file: File) : Closeable { class BackupZipOutput(val file: File) : Closeable {

@ -0,0 +1,33 @@
package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
import java.util.concurrent.TimeUnit
class ExpiringLruCache<T>(
val maxSize: Int,
private val lifetime: Long,
private val timeUnit: TimeUnit,
) {
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
operator fun get(key: ContentCache.Key): T? {
val value = cache.get(key) ?: return null
if (value.isExpired) {
cache.remove(key)
}
return value.get()
}
operator fun set(key: ContentCache.Key, value: T) {
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
}
fun clear() {
cache.evictAll()
}
fun trimToSize(size: Int) {
cache.trimToSize(size)
}
}

@ -0,0 +1,34 @@
package org.koitharu.kotatsu.core.cache
import android.os.SystemClock
import java.util.concurrent.TimeUnit
class ExpiringValue<T>(
private val value: T,
lifetime: Long,
timeUnit: TimeUnit,
) {
private val expiresAt = SystemClock.elapsedRealtime() + timeUnit.toMillis(lifetime)
val isExpired: Boolean
get() = SystemClock.elapsedRealtime() >= expiresAt
fun get(): T? = if (isExpired) null else value
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExpiringValue<*>
if (value != other.value) return false
return expiresAt == other.expiresAt
}
override fun hashCode(): Int {
var result = value?.hashCode() ?: 0
result = 31 * result + expiresAt.hashCode()
return result
}
}

@ -6,6 +6,7 @@ import android.content.res.Configuration
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.concurrent.TimeUnit
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 { class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
@ -13,8 +14,8 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
application.registerComponentCallbacks(this) application.registerComponentCallbacks(this)
} }
private val detailsCache = DeferredLruCache<Manga>(4) private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
private val pagesCache = DeferredLruCache<List<MangaPage>>(4) private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
override val isCachingEnabled: Boolean = true override val isCachingEnabled: Boolean = true
@ -23,7 +24,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
} }
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) { override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
detailsCache.put(ContentCache.Key(source, url), details) detailsCache[ContentCache.Key(source, url)] = details
} }
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? { override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
@ -31,7 +32,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
} }
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) { override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
pagesCache.put(ContentCache.Key(source, url), pages) pagesCache[ContentCache.Key(source, url)] = pages
} }
override fun onConfigurationChanged(newConfig: Configuration) = Unit override fun onConfigurationChanged(newConfig: Configuration) = Unit
@ -43,17 +44,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
trimCache(pagesCache, level) trimCache(pagesCache, level)
} }
private fun trimCache(cache: DeferredLruCache<*>, level: Int) { private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
when (level) { when (level) {
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL, ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
ComponentCallbacks2.TRIM_MEMORY_COMPLETE, ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll() ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.clear()
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN, ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1) ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
else -> cache.trimToSize(cache.maxSize() / 2) else -> cache.trimToSize(cache.maxSize / 2)
} }
} }
} }

@ -33,6 +33,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration6To7
import org.koitharu.kotatsu.core.db.migrations.Migration7To8 import org.koitharu.kotatsu.core.db.migrations.Migration7To8
import org.koitharu.kotatsu.core.db.migrations.Migration8To9 import org.koitharu.kotatsu.core.db.migrations.Migration8To9
import org.koitharu.kotatsu.core.db.migrations.Migration9To10 import org.koitharu.kotatsu.core.db.migrations.Migration9To10
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
@ -46,7 +47,6 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
const val DATABASE_VERSION = 15 const val DATABASE_VERSION = 15

@ -1,13 +1,13 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.longHashCode
// Entity to model // Entity to model

@ -6,9 +6,9 @@ import androidx.core.util.Consumer
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.ErrorDetailsDialog import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class DialogErrorObserver( class DialogErrorObserver(
host: View, host: View,

@ -11,8 +11,8 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.utils.ext.findActivity import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
abstract class ErrorObserver( abstract class ErrorObserver(
protected val host: View, protected val host: View,

@ -12,13 +12,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.ui.ErrorDetailsDialog import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.isSuccess
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.isSuccess
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine

@ -5,10 +5,10 @@ import androidx.core.util.Consumer
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.ErrorDetailsDialog import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class SnackbarErrorObserver( class SnackbarErrorObserver(
host: View, host: View,

@ -13,14 +13,15 @@ import okhttp3.Request
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
@ -36,7 +37,7 @@ private const val CONTENT_TYPE_APK = "application/vnd.android.package-archive"
class AppUpdateRepository @Inject constructor( class AppUpdateRepository @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val settings: AppSettings, private val settings: AppSettings,
private val okHttp: OkHttpClient, @BaseHttpClient private val okHttp: OkHttpClient,
) { ) {
private val availableUpdate = MutableStateFlow<AppVersion?>(null) private val availableUpdate = MutableStateFlow<AppVersion?>(null)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save