Merge branch 'devel' into feature/nextgen
This commit is contained in:
@@ -14,8 +14,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 32
|
||||
versionCode 411
|
||||
versionName '3.3.2'
|
||||
versionCode 412
|
||||
versionName '3.4'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -77,19 +77,19 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
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'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.5.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc02'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-rc02'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.0-rc02'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.0-rc02'
|
||||
implementation 'androidx.activity:activity-ktx:1.5.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
@@ -99,7 +99,7 @@ dependencies {
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha02'
|
||||
//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-ktx:2.4.2'
|
||||
|
||||
@@ -3,14 +3,13 @@ package org.koitharu.kotatsu.core.model.parcelable
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.utils.ext.createList
|
||||
|
||||
class ParcelableMangaChapters(
|
||||
val chapters: List<MangaChapter>,
|
||||
) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
createList(parcel.readInt()) { parcel.readMangaChapter() }
|
||||
List(parcel.readInt()) { parcel.readMangaChapter() }
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
|
||||
@@ -3,14 +3,13 @@ package org.koitharu.kotatsu.core.model.parcelable
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.utils.ext.createList
|
||||
|
||||
class ParcelableMangaPages(
|
||||
val pages: List<MangaPage>,
|
||||
) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
createList(parcel.readInt()) { parcel.readMangaPage() }
|
||||
List(parcel.readInt()) { parcel.readMangaPage() }
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
|
||||
@@ -3,14 +3,14 @@ package org.koitharu.kotatsu.core.model.parcelable
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.ext.createSet
|
||||
import org.koitharu.kotatsu.utils.ext.Set
|
||||
|
||||
class ParcelableMangaTags(
|
||||
val tags: Set<MangaTag>,
|
||||
) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
createSet(parcel.readInt()) { parcel.readMangaTag() }
|
||||
Set(parcel.readInt()) { parcel.readMangaTag() }
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.text.Html
|
||||
import coil.ComponentRegistry
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
@@ -10,6 +11,7 @@ import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.parser.FaviconMapper
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
||||
|
||||
val uiModule
|
||||
get() = module {
|
||||
@@ -40,4 +42,5 @@ val uiModule
|
||||
.build()
|
||||
).build()
|
||||
}
|
||||
factory<Html.ImageGetter> { CoilImageGetter(androidContext(), get()) }
|
||||
}
|
||||
@@ -8,6 +8,6 @@ val detailsModule
|
||||
get() = module {
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import coil.size.Scale
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -80,6 +81,7 @@ class DetailsFragment :
|
||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
addMenuProvider(DetailsMenuProvider())
|
||||
}
|
||||
|
||||
@@ -108,8 +110,6 @@ class DetailsFragment :
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
textViewAuthor.textAndVisible = manga.author
|
||||
textViewDescription.text = manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank)
|
||||
?: getString(R.string.no_description)
|
||||
when (manga.state) {
|
||||
MangaState.FINISHED -> {
|
||||
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?) {
|
||||
with(binding.buttonRead) {
|
||||
if (history == null) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.text.Html
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
@@ -43,6 +45,7 @@ class DetailsViewModel(
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
private val scrobbler: Scrobbler,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val delegate = MangaDetailsDelegate(
|
||||
@@ -79,7 +82,19 @@ class DetailsViewModel(
|
||||
|
||||
val bookmarks = delegate.manga.flatMapLatest {
|
||||
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 isScrobblingAvailable: Boolean
|
||||
|
||||
@@ -13,9 +13,9 @@ import java.io.InputStream
|
||||
class PagesCache(context: Context) {
|
||||
|
||||
private val cacheDir = context.externalCacheDir ?: context.cacheDir
|
||||
private val lruCache = DiskLruCache.create(
|
||||
cacheDir.subdir(CacheDir.PAGES.dir),
|
||||
FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||
private val lruCache = createDiskLruCacheSafe(
|
||||
dir = cacheDir.subdir(CacheDir.PAGES.dir),
|
||||
size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||
)
|
||||
|
||||
operator fun get(url: String): File? {
|
||||
@@ -60,4 +60,14 @@ class PagesCache(context: Context) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
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
|
||||
|
||||
abstract class Scrobbler(
|
||||
@@ -59,7 +59,7 @@ abstract class Scrobbler(
|
||||
scrobbler = scrobblerService,
|
||||
mangaId = mangaId,
|
||||
targetId = targetId,
|
||||
status = statuses.findKey(status),
|
||||
status = statuses.findKeyByValue(status),
|
||||
chapter = chapter,
|
||||
comment = comment,
|
||||
rating = rating,
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -23,9 +23,6 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
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.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -74,7 +71,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
.setTitle(R.string.app_update_available)
|
||||
.setMessage(message)
|
||||
.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)
|
||||
.setCancelable(false)
|
||||
@@ -88,42 +86,23 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
private val PERIOD = TimeUnit.HOURS.toMillis(6)
|
||||
|
||||
fun isUpdateSupported(context: Context): Boolean {
|
||||
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
|
||||
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getCertificateSHA1Fingerprint(context: Context): String? {
|
||||
val packageInfo = try {
|
||||
context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.GET_SIGNATURES
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTraceDebug()
|
||||
return null
|
||||
}
|
||||
val signatures = packageInfo?.signatures
|
||||
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
|
||||
private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val signatures = requireNotNull(packageInfo?.signatures)
|
||||
val cert: ByteArray = signatures.first().toByteArray()
|
||||
val input: InputStream = ByteArrayInputStream(cert)
|
||||
val c = try {
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
cf.generateCertificate(input) as X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
e.printStackTraceDebug()
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
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
|
||||
}
|
||||
}
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
val c = cf.generateCertificate(input) as X509Certificate
|
||||
val md: MessageDigest = MessageDigest.getInstance("SHA1")
|
||||
val publicKey: ByteArray = md.digest(c.encoded)
|
||||
return publicKey.byte2HexFormatted()
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,25 @@ package org.koitharu.kotatsu.settings
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
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.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||
|
||||
private val source by serializableArgument<MangaSource>(EXTRA_SOURCE)
|
||||
private var repository: RemoteMangaRepository? = null
|
||||
private val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -63,6 +63,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||
|
||||
private fun loadUsername(preference: Preference) = viewLifecycleScope.launch {
|
||||
runCatching {
|
||||
preference.summary = null
|
||||
withContext(Dispatchers.Default) {
|
||||
requireNotNull(repository?.getAuthProvider()?.getUsername())
|
||||
}
|
||||
@@ -70,10 +71,28 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||
preference.title = getString(R.string.logged_in_as, username)
|
||||
}.onFailure { error ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveError(error: Throwable): Unit {
|
||||
viewLifecycleScope.launch {
|
||||
if (exceptionResolver.resolve(error)) {
|
||||
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch
|
||||
loadUsername(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_AUTH = "auth"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
class NewSourcesViewModel(
|
||||
private val settings: AppSettings,
|
||||
@@ -30,12 +32,14 @@ class NewSourcesViewModel(
|
||||
}
|
||||
|
||||
private fun buildList() {
|
||||
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
|
||||
val hidden = settings.hiddenSources
|
||||
sources.value = initialList.map {
|
||||
val locale = it.locale
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = it.name !in hidden,
|
||||
isEnabled = it.name !in hidden && (locale == null || locale in locales),
|
||||
isDraggable = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,25 +18,20 @@ inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
|
||||
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()
|
||||
1 -> Collections.singleton(init(0))
|
||||
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<*>) {
|
||||
this as ArrayList<T>
|
||||
} else {
|
||||
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) {
|
||||
if (v == value) {
|
||||
return k
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,4 +302,16 @@
|
||||
<string name="use_fingerprint">Fingerabdruck verwenden, falls vorhanden</string>
|
||||
<string name="appwidget_shelf_description">Manga aus Ihren Favoriten</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>
|
||||
@@ -301,4 +301,5 @@
|
||||
<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_recent_description">Sus mangas recientemente leídos</string>
|
||||
<string name="logout">Cerrar sesión</string>
|
||||
</resources>
|
||||
@@ -302,4 +302,10 @@
|
||||
<string name="use_fingerprint">Käytä sormenjälkeä, jos käytettävissä</string>
|
||||
<string name="appwidget_shelf_description">Manga suosikeistasi</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>
|
||||
@@ -302,4 +302,19 @@
|
||||
<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_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>
|
||||
@@ -302,4 +302,19 @@
|
||||
<string name="use_fingerprint">Usa le impronte digitali se disponibili</string>
|
||||
<string name="appwidget_shelf_description">Manga dai preferiti</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>
|
||||
@@ -303,4 +303,18 @@
|
||||
<string name="use_fingerprint">指紋がある場合は、指紋を使用する</string>
|
||||
<string name="appwidget_shelf_description">お気に入りの漫画</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>
|
||||
@@ -303,4 +303,18 @@
|
||||
<string name="appwidget_shelf_description">Favorilerinizden mangalar</string>
|
||||
<string name="appwidget_recent_description">Son okuduğunuz mangalar</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>
|
||||
@@ -56,9 +56,4 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<Preference
|
||||
android:key="cookies_clear"
|
||||
android:persistent="false"
|
||||
android:title="@string/clear_cookies" />
|
||||
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user