Merge branch 'devel' into m3

This commit is contained in:
Zakhar Timoshenko
2022-01-26 15:24:52 +03:00
committed by GitHub
43 changed files with 711 additions and 233 deletions

View File

@@ -11,6 +11,7 @@ import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
@@ -28,7 +29,7 @@ object MangaUtils : KoinComponent {
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page)
val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart)

View File

@@ -2,51 +2,38 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koin.core.context.GlobalContext
import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.site.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@Suppress("SpellCheckingInspection")
@Parcelize
enum class MangaSource(
val title: String,
val locale: String?,
val cls: Class<out MangaRepository>,
) : Parcelable {
LOCAL("Local", null, LocalMangaRepository::class.java),
READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java),
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java),
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java),
DESUME("Desu.me", "ru", DesuMeRepository::class.java),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java),
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java),
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
LOCAL("Local", null),
READMANGA_RU("ReadManga", "ru"),
MINTMANGA("MintManga", "ru"),
SELFMANGA("SelfManga", "ru"),
MANGACHAN("Манга-тян", "ru"),
DESUME("Desu.me", "ru"),
HENCHAN("Хентай-тян", "ru"),
YAOICHAN("Яой-тян", "ru"),
MANGATOWN("MangaTown", "en"),
MANGALIB("MangaLib", "ru"),
// NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
MANGAREAD("MangaRead", "en", MangareadRepository::class.java),
REMANGA("Remanga", "ru", RemangaRepository::class.java),
HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java),
ANIBEL("Anibel", "be", AnibelRepository::class.java),
NINEMANGA_EN("NineManga English", "en", NineMangaRepository.English::class.java),
NINEMANGA_ES("NineManga Español", "es", NineMangaRepository.Spanish::class.java),
NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java),
NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java),
NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java),
NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java),
NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java),
EXHENTAI("ExHentai", null, ExHentaiRepository::class.java),
MANGAOWL("MangaOwl", "en", MangaOwlRepository::class.java),
MANGADEX("MangaDex", null, MangaDexRepository::class.java),
MANGAREAD("MangaRead", "en"),
REMANGA("Remanga", "ru"),
HENTAILIB("HentaiLib", "ru"),
ANIBEL("Anibel", "be"),
NINEMANGA_EN("NineManga English", "en"),
NINEMANGA_ES("NineManga Español", "es"),
NINEMANGA_RU("NineManga Русский", "ru"),
NINEMANGA_DE("NineManga Deutsch", "de"),
NINEMANGA_IT("NineManga Italiano", "it"),
NINEMANGA_BR("NineManga Brasil", "pt"),
NINEMANGA_FR("NineManga Français", "fr"),
EXHENTAI("ExHentai", null),
MANGAOWL("MangaOwl", "en"),
MANGADEX("MangaDex", null),
;
@get:Throws(NoBeanDefFoundException::class)
@Deprecated("", ReplaceWith("MangaRepository(this)",
"org.koitharu.kotatsu.core.parser.MangaRepository"))
val repository: MangaRepository
get() = GlobalContext.get().get(named(this))
}

View File

@@ -7,6 +7,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit
@@ -30,4 +31,5 @@ val networkModule
}.build()
}
factory { DownloadManagerHelper(get(), get()) }
single { MangaLoaderContext(get(), get()) }
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import coil.map.Mapper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.model.MangaSource
class FaviconMapper() : Mapper<Uri, HttpUrl> {
override fun map(data: Uri): HttpUrl {
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
return repo.getFaviconUrl().toHttpUrl()
}
override fun handles(data: Uri) = data.scheme == "favicon"
}

View File

@@ -7,6 +7,8 @@ import org.koitharu.kotatsu.core.model.*
interface MangaRepository {
val source: MangaSource
val sortOrders: Set<SortOrder>
suspend fun getList2(

View File

@@ -2,15 +2,12 @@ package org.koitharu.kotatsu.core.parser
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.site.*
val parserModule
get() = module {
single { MangaLoaderContext(get(), get()) }
factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.SourceSettings
@@ -12,8 +11,6 @@ abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext
) : MangaRepository {
protected abstract val source: MangaSource
protected abstract val defaultDomain: String
private val conf by lazy {
@@ -29,6 +26,8 @@ abstract class RemoteMangaRepository(
override suspend fun getTags(): Set<MangaTag> = emptySet()
open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico"
open fun onCreatePreferences(map: MutableMap<String, Any>) {
map[SourceSettings.KEY_DOMAIN] = defaultDomain
}
@@ -53,8 +52,10 @@ abstract class RemoteMangaRepository(
if (subdomain != null) {
append(subdomain)
append('.')
append(conf.getDomain(defaultDomain).removePrefix("www."))
} else {
append(conf.getDomain(defaultDomain))
}
append(conf.getDomain(defaultDomain))
append(this@withDomain)
}
else -> this

View File

@@ -21,6 +21,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
SortOrder.NEWEST
)
override fun getFaviconUrl(): String {
return "https://cdn.${getDomain()}/favicons/favicon.png"
}
override suspend fun getList2(
offset: Int,
query: String?,

View File

@@ -128,7 +128,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
scanlator = null,
branch = null,
)
}
} ?: bypassLicensedChapters(manga)
)
}
@@ -191,6 +191,32 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
map[SourceSettings.KEY_USE_SSL] = true
}
private suspend fun bypassLicensedChapters(manga: Manga): List<MangaChapter> {
val doc = loaderContext.httpGet(manga.url.withDomain("m")).parseHtml()
val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList()
val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
return list.select("li").asReversed().mapIndexedNotNull { i, li ->
val a = li.selectFirst("a") ?: return@mapIndexedNotNull null
val href = a.relUrl("href")
val name = a.selectFirst("span.vol")?.text().orEmpty().ifEmpty {
a.ownText()
}
MangaChapter(
id = generateUid(href),
url = href,
source = MangaSource.MANGATOWN,
number = i + 1,
uploadDate = parseChapterDate(
dateFormat,
li.selectFirst("span.time")?.text()
),
name = name.ifEmpty { "${manga.title} - ${i + 1}" },
scanlator = null,
branch = null,
)
}
}
private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
private companion object {

View File

@@ -151,8 +151,10 @@ class MangareadRepository(
?.selectFirst("div.reading-content")
?: throw ParseException("Root not found")
return root.select("div.page-break").map { div ->
val img = div.selectFirst("img")
val url = img?.relUrl("src") ?: parseFailed("Page image not found")
val img = div.selectFirst("img") ?: parseFailed("Page image not found")
val url = img.relUrl("data-src").ifEmpty {
img.relUrl("src")
}
MangaPage(
id = generateUid(url),
url = url,

View File

@@ -5,6 +5,7 @@ import coil.ImageLoader
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.FaviconMapper
import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
@@ -15,6 +16,7 @@ val uiModule
.componentRegistry(
ComponentRegistry.Builder()
.add(CbzFetcher())
.add(FaviconMapper())
.build()
).build()
}

View File

@@ -123,7 +123,7 @@ class DetailsViewModel(
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
manga = manga.source.repository.getDetails(manga)
manga = MangaRepository(manga.source).getDetails(manga)
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = if (hist != null) {

View File

@@ -145,7 +145,7 @@ class DownloadManager(
while (true) {
try {
val response = call.clone().await()
withContext(Dispatchers.IO) {
runInterruptible(Dispatchers.IO) {
file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyTo(out)
}

View File

@@ -9,23 +9,24 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.buffer
import okio.source
import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch(
pool: BitmapPool,
data: Uri,
size: Size,
options: Options,
): FetchResult {
): FetchResult = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(data.schemeSpecificPart)
val entry = zip.getEntry(data.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
return SourceResult(
SourceResult(
source = ExtraCloseableBufferedSource(
zip.getInputStream(entry).source().buffer(),
zip,

View File

@@ -31,7 +31,7 @@ class MangaZip(val file: File) {
return writableCbz.flush()
}
fun addCover(file: File, ext: String) {
suspend fun addCover(file: File, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(0, 0))
if (ext.isNotEmpty() && ext.length <= 4) {
@@ -39,11 +39,11 @@ class MangaZip(val file: File) {
append(ext)
}
}
writableCbz[name] = file
writableCbz.put(name, file)
index.setCoverEntry(name)
}
fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(chapter.number, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
@@ -51,7 +51,7 @@ class MangaZip(val file: File) {
append(ext)
}
}
writableCbz[name] = file
writableCbz.put(name, file)
index.addChapter(chapter)
}

View File

@@ -1,8 +1,7 @@
package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -27,11 +26,13 @@ class WritableCbzFile(private val file: File) {
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
while (entry != null && currentCoroutineContext().isActive) {
val target = File(dir.path + File.separator + entry.name)
target.parentFile?.mkdirs()
target.outputStream().use { out ->
zip.copyTo(out)
runInterruptible {
target.parentFile?.mkdirs()
target.outputStream().use { out ->
zip.copyTo(out)
}
}
zip.closeEntry()
entry = zip.nextEntry
@@ -51,11 +52,13 @@ class WritableCbzFile(private val file: File) {
tempFile.delete()
}
try {
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
runInterruptible {
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
}
zip.flush()
}
zip.flush()
}
tempFile.renameTo(file)
} finally {
@@ -67,29 +70,26 @@ class WritableCbzFile(private val file: File) {
operator fun get(name: String) = File(dir, name)
operator fun set(name: String, file: File) {
suspend fun put(name: String, file: File) = runInterruptible(Dispatchers.IO) {
file.copyTo(this[name], overwrite = true)
}
companion object {
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}

View File

@@ -23,6 +23,7 @@ import java.util.zip.ZipFile
class LocalMangaRepository(private val context: Context) : MangaRepository {
override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter()
override suspend fun getList2(

View File

@@ -50,7 +50,7 @@ class PageLoader(
private fun loadAsync(page: MangaPage): Deferred<File> {
var repo = repository
if (repo?.javaClass != page.source.cls) {
if (repo?.source != page.source) {
repo = mangaRepositoryOf(page.source)
repository = repo
}

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.reader.ui
import android.content.ContentResolver
import android.net.Uri
import android.util.LongSparseArray
import androidx.lifecycle.MutableLiveData
@@ -77,7 +76,7 @@ class ReaderViewModel(
var manga = dataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
val repo = manga.source.repository
val repo = MangaRepository(manga.source)
manga = repo.getDetails(manga)
manga.chapters?.forEach {
chapters.put(it.id, it)
@@ -206,7 +205,7 @@ class ReaderViewModel(
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
val manga = checkNotNull(mangaData.value) { "Manga is null" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = manga.source.repository
val repo = MangaRepository(manga.source)
return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage.from(page, index, chapterId)
}

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.levenshteinDistance
@@ -29,7 +30,7 @@ class MangaSearchRepository(
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source ->
runCatching {
source.repository.getList2(
MangaRepository(source).getList2(
offset = 0,
query = query,
sortOrder = SortOrder.POPULARITY

View File

@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.settings.sources
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
SourceConfigListener {
SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
private lateinit var reorderHelper: ItemTouchHelper
private val viewModel by viewModel<SourcesSettingsViewModel>()
@@ -42,7 +42,7 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sourcesAdapter = SourceConfigAdapter(this)
val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner)
with(binding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(SourceConfigItemDecoration(view.context))
@@ -59,6 +59,17 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_sources, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
@@ -83,6 +94,20 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
viewModel.expandOrCollapse(header.localeId)
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performSearch(newText)
return true
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
(item.actionView as SearchView).setQuery("", false)
return true
}
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0,

View File

@@ -21,6 +21,7 @@ class SourcesSettingsViewModel(
val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
private val expandedGroups = HashSet<String?>()
private var searchQuery: String? = null
init {
buildList()
@@ -63,9 +64,30 @@ class SourcesSettingsViewModel(
buildList()
}
fun performSearch(query: String?) {
searchQuery = query?.trim()
buildList()
}
private fun buildList() {
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
val hiddenSources = settings.hiddenSources
val query = searchQuery
if (!query.isNullOrEmpty()) {
items.value = sources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
source = it,
isEnabled = it.name !in hiddenSources,
isDraggable = false,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
}
return
}
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it.name !in hiddenSources) {
KEY_ENABLED
@@ -81,6 +103,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem(
source = it,
isEnabled = true,
isDraggable = true,
)
}
}
@@ -102,6 +125,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem(
source = it,
isEnabled = false,
isDraggable = false,
)
}
}

View File

@@ -1,13 +1,19 @@
package org.koitharu.kotatsu.settings.sources.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourceConfigAdapter(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(),
sourceConfigGroupDelegate(listener),
sourceConfigItemDelegate(listener),
sourceConfigItemDelegate(listener, coil, lifecycleOwner),
sourceConfigDraggableItemDelegate(listener),
sourceConfigEmptySearchDelegate(),
)

View File

@@ -4,14 +4,19 @@ import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
@@ -38,11 +43,44 @@ fun sourceConfigGroupDelegate(
}
}
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigItemDelegate(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) }
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
) {
var imageRequest: Disposable? = null
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
imageRequest = ImageRequest.Builder(context)
.data(item.faviconUrl)
.error(R.drawable.ic_favicon_fallback)
.target(binding.imageViewIcon)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
}
}
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigDraggableItemDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigDraggableBinding>(
{ layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
) {
val eventListener = object : View.OnClickListener, View.OnTouchListener,
@@ -70,11 +108,9 @@ fun sourceConfigItemDelegate(
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
binding.imageViewHandle.isVisible = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled
binding.root.updatePaddingRelative(
start = if (item.isEnabled) 0 else binding.imageViewHandle.paddingStart * 2,
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
)
}
}
}
fun sourceConfigEmptySearchDelegate() = adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
R.layout.item_sources_empty
) { }

View File

@@ -2,21 +2,25 @@ package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.*
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> {
oldItem is LocaleGroup && newItem is LocaleGroup -> {
oldItem.localeId == newItem.localeId
}
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
oldItem is SourceItem && newItem is SourceItem -> {
oldItem.source == newItem.source
}
oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> {
oldItem is Header && newItem is Header -> {
oldItem.titleResId == newItem.titleResId
}
oldItem == EmptySearchResult && newItem == EmptySearchResult -> {
true
}
else -> false
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.model
import android.net.Uri
import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.MangaSource
@@ -49,8 +50,12 @@ sealed interface SourceConfigItem {
class SourceItem(
val source: MangaSource,
val isEnabled: Boolean,
val isDraggable: Boolean,
) : SourceConfigItem {
val faviconUrl: Uri
get() = Uri.fromParts("favicon", source.name, null)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -59,6 +64,7 @@ sealed interface SourceConfigItem {
if (source != other.source) return false
if (isEnabled != other.isEnabled) return false
if (isDraggable != other.isDraggable) return false
return true
}
@@ -66,7 +72,10 @@ sealed interface SourceConfigItem {
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isEnabled.hashCode()
result = 31 * result + isDraggable.hashCode()
return result
}
}
object EmptySearchResult : SourceConfigItem
}

View File

@@ -10,4 +10,10 @@ object PendingIntentCompat {
} else {
0
}
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
}

View File

@@ -31,7 +31,7 @@ class RecentWidgetProvider : AppWidgetProvider() {
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
)
)
views.setEmptyView(R.id.stackView, R.id.textView_holder)

View File

@@ -31,7 +31,7 @@ class ShelfWidgetProvider : AppWidgetProvider() {
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
)
)
views.setEmptyView(R.id.gridView, R.id.textView_holder)