Improve suggestions worker

pull/139/head^2
Koitharu 4 years ago
parent cca6d5fa04
commit cc6b114e4d
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -9,7 +9,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
class MangaDataRepository(private val db: MangaDatabase) { class MangaDataRepository(private val db: MangaDatabase) {
@ -37,7 +37,7 @@ class MangaDataRepository(private val db: MangaDatabase) {
suspend fun resolveIntent(intent: MangaIntent): Manga? = when { suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga intent.manga != null -> intent.manga
intent.mangaId != 0L -> db.mangaDao.find(intent.mangaId)?.toManga() intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri else -> null // TODO resolve uri
} }

@ -14,7 +14,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.medianOrNull import org.koitharu.kotatsu.parsers.util.medianOrNull
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile import java.util.zip.ZipFile

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Junction import androidx.room.Junction
import androidx.room.Relation import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
class MangaWithTags( class MangaWithTags(
@Embedded val manga: MangaEntity, @Embedded val manga: MangaEntity,
@ -15,7 +15,5 @@ class MangaWithTags(
val tags: List<TagEntity> val tags: List<TagEntity>
) { ) {
fun toManga() = manga.toManga(tags.mapToSet { fun toManga() = manga.toManga(tags.mapToSet { it.toMangaTag() })
it.toMangaTag()
})
} }

@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Junction import androidx.room.Junction
import androidx.room.Relation import androidx.room.Relation
import java.util.*
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
class TrackLogWithManga( class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity, @Embedded val trackLog: TrackLogEntity,

@ -24,11 +24,11 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.io.IOException import java.io.IOException
class DetailsViewModel( class DetailsViewModel(

@ -6,7 +6,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.TextView import android.widget.TextView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.replaceWith import org.koitharu.kotatsu.parsers.util.replaceWith
class BranchesAdapter : BaseAdapter() { class BranchesAdapter : BaseAdapter() {

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.ProgressJob

@ -16,8 +16,8 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.format
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import com.google.android.material.R as materialR import com.google.android.material.R as materialR

@ -13,8 +13,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesRepository(private val db: MangaDatabase) { class FavouritesRepository(private val db: MangaDatabase) {

@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao @Dao
abstract class HistoryDao { abstract class HistoryDao {
@ -23,8 +22,15 @@ abstract class HistoryDao {
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List<MangaEntity> abstract suspend fun findAllManga(): List<MangaEntity>
@Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))") @Query(
abstract suspend fun findAllTags(): List<TagEntity> """SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
INNER JOIN history ON history.manga_id = manga_tags.manga_id
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_tags.manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@Query("SELECT * FROM history WHERE manga_id = :id") @Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity? abstract suspend fun find(id: Long): HistoryEntity?
@ -60,5 +66,4 @@ abstract class HistoryDao {
true true
} else false } else false
} }
} }

@ -11,9 +11,9 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class HistoryRepository( class HistoryRepository(
private val db: MangaDatabase, private val db: MangaDatabase,
@ -91,7 +91,7 @@ class HistoryRepository(
} }
} }
suspend fun getAllTags(): Set<MangaTag> { suspend fun getPopularTags(limit: Int): List<MangaTag> {
return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() } return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
} }
} }

@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.areItemsEquals import org.koitharu.kotatsu.parsers.util.areItemsEquals
sealed interface SearchSuggestionItem { sealed interface SearchSuggestionItem {

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.map

@ -6,9 +6,9 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class SuggestionRepository( class SuggestionRepository(
private val db: MangaDatabase, private val db: MangaDatabase,

@ -4,21 +4,28 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.FloatRange
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.* import androidx.work.*
import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers
import kotlin.math.pow import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit
import kotlin.math.pow
class SuggestionsWorker(appContext: Context, params: WorkerParameters) : class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params), KoinComponent { CoroutineWorker(appContext, params), KoinComponent {
@ -27,11 +34,10 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
private val historyRepository by inject<HistoryRepository>() private val historyRepository by inject<HistoryRepository>()
private val appSettings by inject<AppSettings>() private val appSettings by inject<AppSettings>()
override suspend fun doWork(): Result = try { override suspend fun doWork(): Result {
val count = doWorkImpl() val count = doWorkImpl()
Result.success(workDataOf(DATA_COUNT to count)) val outputData = workDataOf(DATA_COUNT to count)
} catch (t: Throwable) { return Result.success(outputData)
Result.failure()
} }
override suspend fun getForegroundInfo(): ForegroundInfo { override suspend fun getForegroundInfo(): ForegroundInfo {
@ -70,21 +76,28 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
suggestionRepository.clear() suggestionRepository.clear()
return 0 return 0
} }
val rawResults = ArrayList<Manga>() val allTags = historyRepository.getPopularTags(TAGS_LIMIT)
val allTags = historyRepository.getAllTags()
if (allTags.isEmpty()) { if (allTags.isEmpty()) {
return 0 return 0
} }
if (TAG in tags) { // not expedited
trySetForeground()
}
val tagsBySources = allTags.groupBy { x -> x.source } val tagsBySources = allTags.groupBy { x -> x.source }
for ((source, tags) in tagsBySources) { val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val repo = MangaRepository(source) val rawResults = coroutineScope {
tags.flatMapTo(rawResults) { tag -> tagsBySources.flatMap { (source, tags) ->
repo.getList( val repo = MangaRepository(source)
offset = 0, tags.map { tag ->
sortOrder = SortOrder.UPDATED, async(dispatcher) {
tags = setOf(tag), repo.getList(
) offset = 0,
} sortOrder = SortOrder.UPDATED,
tags = setOf(tag),
)
}
}
}.awaitAll().flatten().asArrayList()
} }
if (appSettings.isSuggestionsExcludeNsfw) { if (appSettings.isSuggestionsExcludeNsfw) {
rawResults.removeAll { it.isNsfw } rawResults.removeAll { it.isNsfw }
@ -95,21 +108,32 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
val suggestions = rawResults.distinctBy { manga -> val suggestions = rawResults.distinctBy { manga ->
manga.id manga.id
}.map { manga -> }.map { manga ->
val jointTags = manga.tags intersect allTags
MangaSuggestion( MangaSuggestion(
manga = manga, manga = manga,
relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), relevance = computeRelevance(manga.tags, allTags)
) )
}.sortedBy { it.relevance }.take(LIMIT) }.sortedBy { it.relevance }.take(LIMIT)
suggestionRepository.replace(suggestions) suggestionRepository.replace(suggestions)
return suggestions.size return suggestions.size
} }
@FloatRange(from = 0.0, to = 1.0)
private fun computeRelevance(mangaTags: Set<MangaTag>, allTags: List<MangaTag>): Float {
val maxWeight = (allTags.size + allTags.size + 1 - mangaTags.size) * mangaTags.size / 2.0
val weight = mangaTags.sumOf { tag ->
val index = allTags.indexOf(tag)
if (index < 0) 0 else allTags.size - index
}
return (weight / maxWeight).pow(2.0).toFloat()
}
companion object { companion object {
private const val TAG = "suggestions" private const val TAG = "suggestions"
private const val TAG_ONESHOT = "suggestions_oneshot" private const val TAG_ONESHOT = "suggestions_oneshot"
private const val LIMIT = 140 private const val LIMIT = 140
private const val TAGS_LIMIT = 20
private const val MAX_PARALLELISM = 4
private const val DATA_COUNT = "count" private const val DATA_COUNT = "count"
private const val WORKER_CHANNEL_ID = "suggestion_worker" private const val WORKER_CHANNEL_ID = "suggestion_worker"
private const val WORKER_NOTIFICATION_ID = 36 private const val WORKER_NOTIFICATION_ID = 36

@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -53,7 +54,9 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
return Result.success() return Result.success()
} }
setForeground(getForegroundInfo()) if (TAG in tags) { // not expedited
trySetForeground()
}
var success = 0 var success = 0
val workData = Data.Builder() val workData = Data.Builder()
.putInt(DATA_TOTAL, tracks.size) .putInt(DATA_TOTAL, tracks.size)

@ -5,6 +5,7 @@ import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.Uri import android.net.Uri
import androidx.work.CoroutineWorker
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -28,4 +29,9 @@ suspend fun ConnectivityManager.waitForNetwork(): Network {
} }
} }
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching {
val info = getForegroundInfo()
setForeground(info)
}.isSuccess

@ -3,46 +3,12 @@ package org.koitharu.kotatsu.utils.ext
import androidx.collection.ArraySet import androidx.collection.ArraySet
import java.util.* import java.util.*
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
clear()
addAll(subject)
}
fun <T> List<T>.medianOrNull(): T? = when {
isEmpty() -> null
else -> get((size / 2).coerceIn(indices))
}
inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
return mapTo(ArraySet(size), transform)
}
fun LongArray.toArraySet(): Set<Long> = createSet(size) { i -> this[i] } fun LongArray.toArraySet(): Set<Long> = createSet(size) { i -> this[i] }
fun <T : Enum<T>> Array<T>.names() = Array(size) { i -> fun <T : Enum<T>> Array<T>.names() = Array(size) { i ->
this[i].name this[i].name
} }
fun <T> Collection<T>.isDistinct(): Boolean {
val set = HashSet<T>(size)
for (item in this) {
if (!set.add(item)) {
return false
}
}
return set.size == size
}
fun <T, K> Collection<T>.isDistinctBy(selector: (T) -> K): Boolean {
val set = HashSet<K>(size)
for (item in this) {
if (!set.add(selector(item))) {
return false
}
}
return set.size == size
}
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) { fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) { if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
@ -51,20 +17,6 @@ fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
} }
} }
inline fun <T> List<T>.areItemsEquals(other: List<T>, equals: (T, T) -> Boolean): Boolean {
if (size != other.size) {
return false
}
for (i in indices) {
val a = this[i]
val b = other[i]
if (!equals(a, b)) {
return false
}
}
return true
}
@Suppress("FunctionName") @Suppress("FunctionName")
inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> { inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
val set = ArraySet<T>(size) val set = ArraySet<T>(size)
@ -82,4 +34,10 @@ inline fun <T> createList(size: Int, init: (index: Int) -> T): List<T> = when (s
0 -> emptyList() 0 -> emptyList()
1 -> Collections.singletonList(init(0)) 1 -> Collections.singletonList(init(0))
else -> MutableList(size, init) else -> MutableList(size, init)
}
fun <T> List<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
this as ArrayList<T>
} else {
ArrayList(this)
} }

@ -2,14 +2,15 @@ package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources import android.content.res.Resources
import android.util.Log import android.util.Log
import java.io.FileNotFoundException
import java.net.SocketTimeoutException
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import java.io.FileNotFoundException import org.koitharu.kotatsu.parsers.util.format
import java.net.SocketTimeoutException
fun Throwable.getDisplayMessage(resources: Resources) = when (this) { fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is AuthRequiredException -> resources.getString(R.string.auth_required) is AuthRequiredException -> resources.getString(R.string.auth_required)

@ -1,27 +1,3 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String {
val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat
val symbols = formatter.decimalFormatSymbols
if (thousandsSep != null) {
symbols.groupingSeparator = thousandsSep
formatter.isGroupingUsed = true
} else {
formatter.isGroupingUsed = false
}
symbols.decimalSeparator = decPoint
formatter.decimalFormatSymbols = symbols
formatter.minimumFractionDigits = decimals
formatter.maximumFractionDigits = decimals
return when (this) {
is Float,
is Double -> formatter.format(this.toDouble())
else -> formatter.format(this.toLong())
}
}
inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else this inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else this
Loading…
Cancel
Save