Merge branch 'devel' into feature/nextgen

This commit is contained in:
Koitharu
2022-07-06 16:15:37 +03:00
25 changed files with 243 additions and 114 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 32 targetSdkVersion 32
versionCode 411 versionCode 412
versionName '3.3.2' versionName '3.4'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -77,19 +77,19 @@ afterEvaluate {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:c92f89f307') { implementation('com.github.nv95:kotatsu-parsers:da3b0ae0cf') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3'
implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.0-rc01' implementation 'androidx.activity:activity-ktx:1.5.0'
implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01' implementation 'androidx.fragment:fragment-ktx:1.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc02' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-rc02' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-service:2.5.0-rc02' implementation 'androidx.lifecycle:lifecycle-service:2.5.0'
implementation 'androidx.lifecycle:lifecycle-process:2.5.0-rc02' implementation 'androidx.lifecycle:lifecycle-process:2.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
@@ -99,7 +99,7 @@ dependencies {
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
implementation 'com.google.android.material:material:1.7.0-alpha02' implementation 'com.google.android.material:material:1.7.0-alpha02'
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0-rc02' kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0'
implementation 'androidx.room:room-runtime:2.4.2' implementation 'androidx.room:room-runtime:2.4.2'
implementation 'androidx.room:room-ktx:2.4.2' implementation 'androidx.room:room-ktx:2.4.2'

View File

@@ -3,14 +3,13 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.ext.createList
class ParcelableMangaChapters( class ParcelableMangaChapters(
val chapters: List<MangaChapter>, val chapters: List<MangaChapter>,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
createList(parcel.readInt()) { parcel.readMangaChapter() } List(parcel.readInt()) { parcel.readMangaChapter() }
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {

View File

@@ -3,14 +3,13 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.utils.ext.createList
class ParcelableMangaPages( class ParcelableMangaPages(
val pages: List<MangaPage>, val pages: List<MangaPage>,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
createList(parcel.readInt()) { parcel.readMangaPage() } List(parcel.readInt()) { parcel.readMangaPage() }
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {

View File

@@ -3,14 +3,14 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.createSet import org.koitharu.kotatsu.utils.ext.Set
class ParcelableMangaTags( class ParcelableMangaTags(
val tags: Set<MangaTag>, val tags: Set<MangaTag>,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
createSet(parcel.readInt()) { parcel.readMangaTag() } Set(parcel.readInt()) { parcel.readMangaTag() }
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.ui package org.koitharu.kotatsu.core.ui
import android.text.Html
import coil.ComponentRegistry import coil.ComponentRegistry
import coil.ImageLoader import coil.ImageLoader
import coil.disk.DiskCache import coil.disk.DiskCache
@@ -10,6 +11,7 @@ import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.FaviconMapper import org.koitharu.kotatsu.core.parser.FaviconMapper
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.utils.image.CoilImageGetter
val uiModule val uiModule
get() = module { get() = module {
@@ -40,4 +42,5 @@ val uiModule
.build() .build()
).build() ).build()
} }
factory<Html.ImageGetter> { CoilImageGetter(androidContext(), get()) }
} }

View File

@@ -8,6 +8,6 @@ val detailsModule
get() = module { get() = module {
viewModel { intent -> viewModel { intent ->
DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get(), get()) DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
} }
} }

View File

@@ -21,6 +21,7 @@ import coil.size.Scale
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -80,6 +81,7 @@ class DetailsFragment :
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged) viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
addMenuProvider(DetailsMenuProvider()) addMenuProvider(DetailsMenuProvider())
} }
@@ -108,8 +110,6 @@ class DetailsFragment :
textViewTitle.text = manga.title textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle textViewSubtitle.textAndVisible = manga.altTitle
textViewAuthor.textAndVisible = manga.author textViewAuthor.textAndVisible = manga.author
textViewDescription.text = manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank)
?: getString(R.string.no_description)
when (manga.state) { when (manga.state) {
MangaState.FINISHED -> { MangaState.FINISHED -> {
textViewState.apply { textViewState.apply {
@@ -172,6 +172,14 @@ class DetailsFragment :
} }
} }
private fun onDescriptionChanged(description: CharSequence?) {
if (description.isNullOrBlank()) {
binding.textViewDescription.setText(R.string.no_description)
} else {
binding.textViewDescription.text = description
}
}
private fun onHistoryChanged(history: MangaHistory?) { private fun onHistoryChanged(history: MangaHistory?) {
with(binding.buttonRead) { with(binding.buttonRead) {
if (history == null) { if (history == null) {

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.text.Html
import androidx.core.text.parseAsHtml
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
@@ -43,6 +45,7 @@ class DetailsViewModel(
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobbler: Scrobbler, private val scrobbler: Scrobbler,
private val imageGetter: Html.ImageGetter,
) : BaseViewModel() { ) : BaseViewModel() {
private val delegate = MangaDetailsDelegate( private val delegate = MangaDetailsDelegate(
@@ -79,7 +82,19 @@ class DetailsViewModel(
val bookmarks = delegate.manga.flatMapLatest { val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val description = delegate.manga
.distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest {
val description = it?.description
if (description.isNullOrEmpty()) {
emit(null)
} else {
emit(description.parseAsHtml())
emit(description.parseAsHtml(imageGetter = imageGetter))
}
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null)
val onMangaRemoved = SingleLiveEvent<Manga>() val onMangaRemoved = SingleLiveEvent<Manga>()
val isScrobblingAvailable: Boolean val isScrobblingAvailable: Boolean

View File

@@ -13,9 +13,9 @@ import java.io.InputStream
class PagesCache(context: Context) { class PagesCache(context: Context) {
private val cacheDir = context.externalCacheDir ?: context.cacheDir private val cacheDir = context.externalCacheDir ?: context.cacheDir
private val lruCache = DiskLruCache.create( private val lruCache = createDiskLruCacheSafe(
cacheDir.subdir(CacheDir.PAGES.dir), dir = cacheDir.subdir(CacheDir.PAGES.dir),
FileSize.MEGABYTES.convert(200, FileSize.BYTES), size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
) )
operator fun get(url: String): File? { operator fun get(url: String): File? {
@@ -60,4 +60,14 @@ class PagesCache(context: Context) {
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat() progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
} }
} }
}
private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
return try {
DiskLruCache.create(dir, size)
} catch (e: Exception) {
dir.deleteRecursively()
dir.mkdir()
DiskLruCache.create(dir, size)
}
} }

View File

@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.domain.model.* import org.koitharu.kotatsu.scrobbling.domain.model.*
import org.koitharu.kotatsu.utils.ext.findKey import org.koitharu.kotatsu.utils.ext.findKeyByValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class Scrobbler( abstract class Scrobbler(
@@ -59,7 +59,7 @@ abstract class Scrobbler(
scrobbler = scrobblerService, scrobbler = scrobblerService,
mangaId = mangaId, mangaId = mangaId,
targetId = targetId, targetId = targetId,
status = statuses.findKey(status), status = statuses.findKeyByValue(status),
chapter = chapter, chapter = chapter,
comment = comment, comment = comment,
rating = rating, rating = rating,

View File

@@ -4,9 +4,9 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -23,9 +23,6 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -74,7 +71,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.download) { _, _ -> .setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl))) val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
} }
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.setCancelable(false) .setCancelable(false)
@@ -88,42 +86,23 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
private val PERIOD = TimeUnit.HOURS.toMillis(6) private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean { fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1 return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? { private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching {
val packageInfo = try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
context.packageManager.getPackageInfo( val signatures = requireNotNull(packageInfo?.signatures)
context.packageName, val cert: ByteArray = signatures.first().toByteArray()
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTraceDebug()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert) val input: InputStream = ByteArrayInputStream(cert)
val c = try { val cf = CertificateFactory.getInstance("X509")
val cf = CertificateFactory.getInstance("X509") val c = cf.generateCertificate(input) as X509Certificate
cf.generateCertificate(input) as X509Certificate val md: MessageDigest = MessageDigest.getInstance("SHA1")
} catch (e: CertificateException) { val publicKey: ByteArray = md.digest(c.encoded)
e.printStackTraceDebug() return publicKey.byte2HexFormatted()
return null }.onFailure { error ->
} error.printStackTraceDebug()
return try { }.getOrNull()
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTraceDebug()
null
} catch (e: CertificateEncodingException) {
e.printStackTraceDebug()
null
}
}
} }
} }

View File

@@ -3,25 +3,25 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
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.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
class SourceSettingsFragment : BasePreferenceFragment(0) { class SourceSettingsFragment : BasePreferenceFragment(0) {
private val source by serializableArgument<MangaSource>(EXTRA_SOURCE) private val source by serializableArgument<MangaSource>(EXTRA_SOURCE)
private var repository: RemoteMangaRepository? = null private var repository: RemoteMangaRepository? = null
private val exceptionResolver = ExceptionResolver(this)
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -63,6 +63,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
private fun loadUsername(preference: Preference) = viewLifecycleScope.launch { private fun loadUsername(preference: Preference) = viewLifecycleScope.launch {
runCatching { runCatching {
preference.summary = null
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
requireNotNull(repository?.getAuthProvider()?.getUsername()) requireNotNull(repository?.getAuthProvider()?.getUsername())
} }
@@ -70,10 +71,28 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
preference.title = getString(R.string.logged_in_as, username) preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error -> }.onFailure { error ->
preference.isEnabled = error is AuthRequiredException preference.isEnabled = error is AuthRequiredException
when {
error is AuthRequiredException -> Unit
ExceptionResolver.canResolve(error) -> {
Snackbar.make(listView, error.getDisplayMessage(resources), Snackbar.LENGTH_INDEFINITE)
.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show()
}
else -> preference.summary = error.getDisplayMessage(resources)
}
error.printStackTraceDebug() error.printStackTraceDebug()
} }
} }
private fun resolveError(error: Throwable): Unit {
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch
loadUsername(pref)
}
}
}
companion object { companion object {
private const val KEY_AUTH = "auth" private const val KEY_AUTH = "auth"

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.settings.newsources package org.koitharu.kotatsu.settings.newsources
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.mapToSet
class NewSourcesViewModel( class NewSourcesViewModel(
private val settings: AppSettings, private val settings: AppSettings,
@@ -30,12 +32,14 @@ class NewSourcesViewModel(
} }
private fun buildList() { private fun buildList() {
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
val hidden = settings.hiddenSources val hidden = settings.hiddenSources
sources.value = initialList.map { sources.value = initialList.map {
val locale = it.locale
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
summary = it.getLocaleTitle(), summary = it.getLocaleTitle(),
isEnabled = it.name !in hidden, isEnabled = it.name !in hidden && (locale == null || locale in locales),
isDraggable = false, isDraggable = false,
) )
} }

View File

@@ -18,25 +18,20 @@ inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
return set return set
} }
inline fun <T> createSet(size: Int, init: (index: Int) -> T): Set<T> = when (size) { @Suppress("FunctionName")
inline fun <T> Set(size: Int, init: (index: Int) -> T): Set<T> = when (size) {
0 -> emptySet() 0 -> emptySet()
1 -> Collections.singleton(init(0)) 1 -> Collections.singleton(init(0))
else -> MutableSet(size, init) else -> MutableSet(size, init)
} }
inline fun <T> createList(size: Int, init: (index: Int) -> T): List<T> = when (size) {
0 -> emptyList()
1 -> Collections.singletonList(init(0))
else -> MutableList(size, init)
}
fun <T> List<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) { fun <T> List<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
this as ArrayList<T> this as ArrayList<T>
} else { } else {
ArrayList(this) ArrayList(this)
} }
fun <K, V> Map<K, V>.findKey(value: V): K? { fun <K, V> Map<K, V>.findKeyByValue(value: V): K? {
for ((k, v) in entries) { for ((k, v) in entries) {
if (v == value) { if (v == value) {
return k return k

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import androidx.core.os.LocaleListCompat
import java.util.*
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw kotlin.NoSuchElementException()
fun LocaleListCompat.toList(): List<Locale> = createList(size()) { i -> getOrThrow(i) }
operator fun LocaleListCompat.iterator() = object : Iterator<Locale> {
private var index = 0
override fun hasNext(): Boolean = index < size()
override fun next(): Locale = getOrThrow(index++)
}
inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
destination: C,
block: (Locale) -> R,
): C {
val len = size()
for (i in 0 until len) {
val item = get(i) ?: continue
destination.add(block(item))
}
return destination
}
inline fun <T> LocaleListCompat.map(block: (Locale) -> T): List<T> {
return mapTo(ArrayList(size()), block)
}
inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
return mapTo(LinkedHashSet(size()), block)
}

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.utils.ext
import androidx.core.os.LocaleListCompat
import java.util.*
operator fun LocaleListCompat.iterator(): ListIterator<Locale> = LocaleListCompatIterator(this)
fun LocaleListCompat.toList(): List<Locale> = List(size()) { i -> getOrThrow(i) }
inline fun <T> LocaleListCompat.map(block: (Locale) -> T): List<T> {
return List(size()) { i -> block(getOrThrow(i)) }
}
inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
return Set(size()) { i -> block(getOrThrow(i)) }
}
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
private var index = 0
override fun hasNext() = index < list.size()
override fun hasPrevious() = index > 0
override fun next() = list.get(index++) ?: throw NoSuchElementException()
override fun nextIndex() = index
override fun previous() = list.get(--index) ?: throw NoSuchElementException()
override fun previousIndex() = index - 1
}

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.utils.image
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Html
import coil.ImageLoader
import coil.executeBlocking
import coil.request.ImageRequest
class CoilImageGetter(
private val context: Context,
private val coil: ImageLoader,
) : Html.ImageGetter {
override fun getDrawable(source: String?): Drawable? {
return coil.executeBlocking(
ImageRequest.Builder(context)
.data(source)
.allowHardware(false)
.build()
).drawable?.apply {
setBounds(0, 0, intrinsicHeight, intrinsicHeight)
}
}
}

View File

@@ -302,4 +302,16 @@
<string name="use_fingerprint">Fingerabdruck verwenden, falls vorhanden</string> <string name="use_fingerprint">Fingerabdruck verwenden, falls vorhanden</string>
<string name="appwidget_shelf_description">Manga aus Ihren Favoriten</string> <string name="appwidget_shelf_description">Manga aus Ihren Favoriten</string>
<string name="appwidget_recent_description">Ihr kürzlich gelesener Manga</string> <string name="appwidget_recent_description">Ihr kürzlich gelesener Manga</string>
<string name="report">Melden</string>
<string name="tracking">Nachverfolgung</string>
<string name="logout">Abmelden</string>
<string name="status_planned">Geplant</string>
<string name="status_on_hold">In der Warteschleife</string>
<string name="show_reading_indicators">Indikatoren für den Lesefortschritt anzeigen</string>
<string name="show_all">Alle anzeigen</string>
<string name="show_reading_indicators_summary">Gelesenen Prozentsatz in Verlauf und Favoriten anzeigen</string>
<string name="clear_cookies_summary">Kann im Falle einiger Probleme helfen. Alle Berechtigungen werden für ungültig erklärt</string>
<string name="status_completed">Abgeschlossen</string>
<string name="exclude_nsfw_from_history_summary">Manga, die als NSFW markiert sind, werden nicht in den Verlauf aufgenommen und Ihr Fortschritt wird nicht gespeichert.</string>
<string name="data_deletion">Datenlöschung</string>
</resources> </resources>

View File

@@ -301,4 +301,5 @@
<string name="use_fingerprint">Utilizar la huella dactilar si está disponible</string> <string name="use_fingerprint">Utilizar la huella dactilar si está disponible</string>
<string name="appwidget_shelf_description">Mangas de tus favoritos</string> <string name="appwidget_shelf_description">Mangas de tus favoritos</string>
<string name="appwidget_recent_description">Sus mangas recientemente leídos</string> <string name="appwidget_recent_description">Sus mangas recientemente leídos</string>
<string name="logout">Cerrar sesión</string>
</resources> </resources>

View File

@@ -302,4 +302,10 @@
<string name="use_fingerprint">Käytä sormenjälkeä, jos käytettävissä</string> <string name="use_fingerprint">Käytä sormenjälkeä, jos käytettävissä</string>
<string name="appwidget_shelf_description">Manga suosikeistasi</string> <string name="appwidget_shelf_description">Manga suosikeistasi</string>
<string name="appwidget_recent_description">Äskettäin lukemasi manga</string> <string name="appwidget_recent_description">Äskettäin lukemasi manga</string>
<string name="tracking">Seuranta</string>
<string name="logout">Kirjaudu ulos</string>
<string name="status_reading">Lukemassa</string>
<string name="status_re_reading">Lukemassa uudelleen</string>
<string name="data_deletion">Tietojen poistaminen</string>
<string name="show_all">Näytä kaikki</string>
</resources> </resources>

View File

@@ -302,4 +302,19 @@
<string name="use_fingerprint">Utiliser l\'empreinte digitale si elle est disponible</string> <string name="use_fingerprint">Utiliser l\'empreinte digitale si elle est disponible</string>
<string name="appwidget_recent_description">Vos mangas récemment lus</string> <string name="appwidget_recent_description">Vos mangas récemment lus</string>
<string name="appwidget_shelf_description">Les mangas de vos favoris</string> <string name="appwidget_shelf_description">Les mangas de vos favoris</string>
<string name="report">Signaler</string>
<string name="tracking">Suivi</string>
<string name="status_planned">Planifié</string>
<string name="status_reading">Lecture</string>
<string name="show_reading_indicators">Afficher les indicateurs de progression de lecture</string>
<string name="show_reading_indicators_summary">Afficher le pourcentage de lecture dans l\'historique et les favoris</string>
<string name="exclude_nsfw_from_history_summary">Les mangas marqués comme étant pour adultes ne seront jamais ajoutés à l\'historique et votre progression ne sera pas sauvegardée</string>
<string name="clear_cookies_summary">Peut aider en cas de problème. Toutes les autorisations seront invalidées</string>
<string name="show_all">Tout afficher</string>
<string name="status_on_hold">En attente</string>
<string name="status_dropped">Abandonné</string>
<string name="data_deletion">Suppression des données</string>
<string name="logout">Se déconnecter</string>
<string name="status_completed">Terminé</string>
<string name="status_re_reading">Relecture</string>
</resources> </resources>

View File

@@ -302,4 +302,19 @@
<string name="use_fingerprint">Usa le impronte digitali se disponibili</string> <string name="use_fingerprint">Usa le impronte digitali se disponibili</string>
<string name="appwidget_shelf_description">Manga dai preferiti</string> <string name="appwidget_shelf_description">Manga dai preferiti</string>
<string name="appwidget_recent_description">I manga letti di recente</string> <string name="appwidget_recent_description">I manga letti di recente</string>
<string name="report">Segnala</string>
<string name="tracking">Tracciamento</string>
<string name="status_reading">Lettura</string>
<string name="status_re_reading">Rilettura</string>
<string name="status_on_hold">In attesa</string>
<string name="show_reading_indicators">Mostrare gli indicatori di progresso della lettura</string>
<string name="data_deletion">Eliminazione dei dati</string>
<string name="show_reading_indicators_summary">Mostra la percentuale di lettura nella cronologia e nei preferiti</string>
<string name="exclude_nsfw_from_history_summary">I manga contrassegnati come per adulti non verranno mai aggiunti alla cronologia e i vostri progressi non verranno salvati</string>
<string name="clear_cookies_summary">Può aiutare in caso di problemi. Tutte le autorizzazioni saranno invalidate</string>
<string name="show_all">Mostra tutto</string>
<string name="logout">Esci</string>
<string name="status_planned">Pianificato</string>
<string name="status_completed">Finito</string>
<string name="status_dropped">Abbandonato</string>
</resources> </resources>

View File

@@ -303,4 +303,18 @@
<string name="use_fingerprint">指紋がある場合は、指紋を使用する</string> <string name="use_fingerprint">指紋がある場合は、指紋を使用する</string>
<string name="appwidget_shelf_description">お気に入りの漫画</string> <string name="appwidget_shelf_description">お気に入りの漫画</string>
<string name="report">報告</string> <string name="report">報告</string>
<string name="status_reading">読書</string>
<string name="status_re_reading">再読込</string>
<string name="status_completed">完了</string>
<string name="status_on_hold">保留中</string>
<string name="tracking">追跡</string>
<string name="logout">ログアウト</string>
<string name="status_planned">予定</string>
<string name="status_dropped">ドロップ</string>
<string name="data_deletion">データの削除</string>
<string name="show_reading_indicators_summary">履歴とお気に入りに既読率を表示する</string>
<string name="clear_cookies_summary">いくつかの問題の場合に助けることができる。すべての認証が無効になります</string>
<string name="show_reading_indicators">読書の進行状況インジケーターを表示</string>
<string name="exclude_nsfw_from_history_summary">NSFWとマークされたマンガは履歴に追加されず、進行状況も保存されない</string>
<string name="show_all">すべて表示</string>
</resources> </resources>

View File

@@ -303,4 +303,18 @@
<string name="appwidget_shelf_description">Favorilerinizden mangalar</string> <string name="appwidget_shelf_description">Favorilerinizden mangalar</string>
<string name="appwidget_recent_description">Son okuduğunuz mangalar</string> <string name="appwidget_recent_description">Son okuduğunuz mangalar</string>
<string name="report">Bildir</string> <string name="report">Bildir</string>
<string name="tracking">İzleme</string>
<string name="logout">Oturumu kapat</string>
<string name="status_reading">Okunuyor</string>
<string name="status_completed">Tamamlandı</string>
<string name="show_reading_indicators">Okuma ilerleme göstergelerini göster</string>
<string name="data_deletion">Verileri sil</string>
<string name="show_reading_indicators_summary">Geçmişte ve favorilerde okunma yüzdesini göster</string>
<string name="exclude_nsfw_from_history_summary">Uygunsuz olarak işaretlenen mangalar asla geçmişe eklenmeyecek ve ilerlemeniz kaydedilmeyecektir</string>
<string name="clear_cookies_summary">Bazı sorunlarda yardımcı olabilir. Tüm yetkilendirmeler geçersiz kılınacaktır</string>
<string name="status_on_hold">Beklemede</string>
<string name="status_dropped">Bırakıldı</string>
<string name="status_planned">Planlandı</string>
<string name="status_re_reading">Yeniden okunuyor</string>
<string name="show_all">Tümünü göster</string>
</resources> </resources>

View File

@@ -56,9 +56,4 @@
</PreferenceCategory> </PreferenceCategory>
<Preference
android:key="cookies_clear"
android:persistent="false"
android:title="@string/clear_cookies" />
</PreferenceScreen> </PreferenceScreen>