Merge branch 'devel' into feature/shikimori
This commit is contained in:
@@ -10,14 +10,13 @@ open class BottomSheetToolbarController(
|
||||
) : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && bottomSheet.top <= 0
|
||||
if (isExpanded) {
|
||||
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
|
||||
} else {
|
||||
toolbar.navigationIcon = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
|
||||
}
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CompositeMutex<T : Any> : Set<T> {
|
||||
|
||||
private val data = HashMap<T, MutableList<CancellableContinuation<Unit>>>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
override val size: Int
|
||||
get() = data.size
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return data.containsKey(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return elements.all { x -> data.containsKey(x) }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return data.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return data.keys.iterator()
|
||||
}
|
||||
|
||||
suspend fun lock(element: T) {
|
||||
waitForRemoval(element)
|
||||
mutex.withLock {
|
||||
val lastValue = data.put(element, LinkedList<CancellableContinuation<Unit>>())
|
||||
check(lastValue == null) {
|
||||
"CompositeMutex is double-locked for $element"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unlock(element: T) {
|
||||
val continuations = mutex.withLock {
|
||||
checkNotNull(data.remove(element)) {
|
||||
"CompositeMutex is not locked for $element"
|
||||
}
|
||||
}
|
||||
continuations.forEach { c ->
|
||||
if (c.isActive) {
|
||||
c.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForRemoval(element: T) {
|
||||
val list = data[element] ?: return
|
||||
suspendCancellableCoroutine<Unit> { continuation ->
|
||||
list.add(continuation)
|
||||
continuation.invokeOnCancellation {
|
||||
list.remove(continuation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
|
||||
class ExternalStorageHelper(context: Context) {
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
|
||||
suspend fun savePage(page: MangaPage, destination: Uri) {
|
||||
val pageLoader = PageLoader()
|
||||
val pageFile = pageLoader.loadPage(page, force = false)
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(destination)?.use { output ->
|
||||
pageFile.inputStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} ?: throw IOException("Output stream is null")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,4 @@ enum class FileSize(private val multiplier: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class GridTouchHelper(context: Context, private val listener: OnGridTouchListener) :
|
||||
GestureDetector.SimpleOnGestureListener() {
|
||||
class GridTouchHelper(
|
||||
context: Context,
|
||||
private val listener: OnGridTouchListener
|
||||
) : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
private val detector = GestureDetector(context, this)
|
||||
private val width = context.resources.displayMetrics.widthPixels
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
open class MutableZipFile(val file: File) {
|
||||
|
||||
protected val dir = File(file.parentFile, file.nameWithoutExtension)
|
||||
|
||||
suspend fun unpack(): Unit = runInterruptible(Dispatchers.IO) {
|
||||
check(dir.list().isNullOrEmpty()) {
|
||||
"Dir ${dir.name} is not empty"
|
||||
}
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir()
|
||||
}
|
||||
if (!file.exists()) {
|
||||
return@runInterruptible
|
||||
}
|
||||
ZipInputStream(FileInputStream(file)).use { zip ->
|
||||
var entry = zip.nextEntry
|
||||
while (entry != null) {
|
||||
val target = File(dir.path + File.separator + entry.name)
|
||||
target.parentFile?.mkdirs()
|
||||
target.outputStream().use { out ->
|
||||
zip.copyTo(out)
|
||||
}
|
||||
zip.closeEntry()
|
||||
entry = zip.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cleanup() = withContext(Dispatchers.IO) {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
|
||||
suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) {
|
||||
val tempFile = File(file.path + ".tmp")
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete()
|
||||
}
|
||||
try {
|
||||
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
|
||||
dir.listFiles()?.forEach {
|
||||
zipFile(it, it.name, zip)
|
||||
}
|
||||
zip.flush()
|
||||
}
|
||||
tempFile.renameTo(file)
|
||||
} finally {
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(name: String) = File(dir, name)
|
||||
|
||||
suspend fun put(name: String, file: File): Unit = withContext(Dispatchers.IO) {
|
||||
file.copyTo(this@MutableZipFile[name], overwrite = true)
|
||||
}
|
||||
|
||||
suspend fun put(name: String, data: String): Unit = withContext(Dispatchers.IO) {
|
||||
this@MutableZipFile[name].writeText(data)
|
||||
}
|
||||
|
||||
suspend fun getContent(name: String): String = withContext(Dispatchers.IO) {
|
||||
get(name).readText()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@WorkerThread
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
FileInputStream(fileToZip).use { fis ->
|
||||
val zipEntry = ZipEntry(fileName)
|
||||
zipOut.putNextEntry(zipEntry)
|
||||
fis.copyTo(zipOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Runnable
|
||||
|
||||
class PausingDispatcher(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
) : CoroutineDispatcher() {
|
||||
|
||||
@Volatile
|
||||
private var isPaused = false
|
||||
private val queue = ConcurrentLinkedQueue<Task>()
|
||||
|
||||
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
|
||||
return isPaused || super.isDispatchNeeded(context)
|
||||
}
|
||||
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
if (isPaused) {
|
||||
queue.add(Task(context, block))
|
||||
} else {
|
||||
dispatcher.dispatch(context, block)
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun pause() {
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun resume() {
|
||||
if (!isPaused) {
|
||||
return
|
||||
}
|
||||
isPaused = false
|
||||
while (true) {
|
||||
val task = queue.poll() ?: break
|
||||
dispatcher.dispatch(task.context, task.block)
|
||||
}
|
||||
}
|
||||
|
||||
private class Task(
|
||||
val context: CoroutineContext,
|
||||
val block: Runnable,
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class SelectionController {
|
||||
|
||||
private val state = MutableStateFlow(emptySet<Int>())
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.speech.RecognizerIntent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class VoiceInputContract : ActivityResultContract<String?, String?>() {
|
||||
|
||||
override fun createIntent(context: Context, input: String?): Intent {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, input)
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): String? {
|
||||
return if (resultCode == Activity.RESULT_OK && intent != null) {
|
||||
val matches = intent.getStringArrayExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||
matches?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
class WordSet(private vararg val words: String) {
|
||||
|
||||
fun anyWordIn(dateString: String): Boolean = words.any {
|
||||
dateString.contains(it, ignoreCase = true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
val Context.connectivityManager: ConnectivityManager
|
||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
suspend fun ConnectivityManager.waitForNetwork(): Network {
|
||||
val request = NetworkRequest.Builder().build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// fast path
|
||||
activeNetwork?.let { return it }
|
||||
}
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
unregisterNetworkCallback(this)
|
||||
if (cont.isActive) {
|
||||
cont.resume(network)
|
||||
}
|
||||
@@ -34,4 +43,16 @@ fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
||||
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching {
|
||||
val info = getForegroundInfo()
|
||||
setForeground(info)
|
||||
}.isSuccess
|
||||
}.isSuccess
|
||||
|
||||
fun <I> ActivityResultLauncher<I>.resolve(context: Context, input: I): ResolveInfo? {
|
||||
val pm = context.packageManager
|
||||
val intent = contract.createIntent(context, input)
|
||||
return pm.resolveActivity(intent, 0)
|
||||
}
|
||||
|
||||
fun <I> ActivityResultLauncher<I>.tryLaunch(input: I, options: ActivityOptionsCompat? = null): Boolean {
|
||||
return runCatching {
|
||||
launch(input, options)
|
||||
}.isSuccess
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.collection.ArraySet
|
||||
import java.util.*
|
||||
|
||||
fun LongArray.toArraySet(): Set<Long> = createSet(size) { i -> this[i] }
|
||||
|
||||
fun <T : Enum<T>> Array<T>.names() = Array(size) { i ->
|
||||
this[i].name
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.SocketTimeoutException
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
|
||||
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
||||
@@ -22,12 +20,4 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
|
||||
is SocketTimeoutException -> resources.getString(R.string.network_error)
|
||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||
else -> localizedMessage ?: resources.getString(R.string.error_occurred)
|
||||
}
|
||||
|
||||
inline fun <T> measured(tag: String, block: () -> T): T {
|
||||
val time = System.currentTimeMillis()
|
||||
val res = block()
|
||||
val spent = System.currentTimeMillis() - time
|
||||
Log.d("measured", "$tag ${spent.format(1)} ms")
|
||||
return res
|
||||
}
|
||||
@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
val IgnoreErrors
|
||||
get() = CoroutineExceptionHandler { _, e ->
|
||||
|
||||
@@ -15,6 +15,6 @@ fun Date.formatRelative(minResolution: Long): CharSequence = DateUtils.getRelati
|
||||
|
||||
fun Date.daysDiff(other: Long): Int {
|
||||
val thisDay = time / TimeUnit.DAYS.toMillis(1L)
|
||||
val otherDay = other/ TimeUnit.DAYS.toMillis(1L)
|
||||
val otherDay = other / TimeUnit.DAYS.toMillis(1L)
|
||||
return (thisDay - otherDay).toInt()
|
||||
}
|
||||
@@ -26,11 +26,12 @@ fun <T : Parcelable> Fragment.parcelableArgument(name: String): Lazy<T> {
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Fragment.serializableArgument(name: String): Lazy<T> {
|
||||
fun <T : Serializable> Fragment.serializableArgument(name: String): Lazy<T> {
|
||||
return lazy(LazyThreadSafetyMode.NONE) {
|
||||
requireNotNull(arguments?.getSerializable(name) as? T) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
requireNotNull(arguments?.getSerializable(name)) {
|
||||
"No argument $name passed into ${javaClass.simpleName}"
|
||||
}
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
||||
|
||||
fun <T> LiveData<T?>.observeNotNull(owner: LifecycleOwner, observer: Observer<T>) {
|
||||
this.observe(owner) {
|
||||
|
||||
@@ -9,17 +9,17 @@ fun ListPreference.setDefaultValueCompat(defaultValue: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <E: Enum<E>> SharedPreferences.getEnumValue(key: String, enumClass: Class<E>): E? {
|
||||
fun <E : Enum<E>> SharedPreferences.getEnumValue(key: String, enumClass: Class<E>): E? {
|
||||
val stringValue = getString(key, null) ?: return null
|
||||
return enumClass.enumConstants?.find {
|
||||
it.name == stringValue
|
||||
}
|
||||
}
|
||||
|
||||
fun <E: Enum<E>> SharedPreferences.getEnumValue(key: String, defaultValue: E): E {
|
||||
fun <E : Enum<E>> SharedPreferences.getEnumValue(key: String, defaultValue: E): E {
|
||||
return getEnumValue(key, defaultValue.javaClass) ?: defaultValue
|
||||
}
|
||||
|
||||
fun <E: Enum<E>> SharedPreferences.Editor.putEnumValue(key: String, value: E?) {
|
||||
fun <E : Enum<E>> SharedPreferences.Editor.putEnumValue(key: String, value: E?) {
|
||||
putString(key, value?.name)
|
||||
}
|
||||
@@ -5,12 +5,11 @@ import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.isGone
|
||||
|
||||
var TextView.textAndVisible: CharSequence?
|
||||
inline get() = text?.takeIf { visibility == View.VISIBLE }
|
||||
inline set(value) {
|
||||
get() = text?.takeIf { visibility == View.VISIBLE }
|
||||
set(value) {
|
||||
text = value
|
||||
isGone = value.isNullOrEmpty()
|
||||
}
|
||||
@@ -40,8 +39,5 @@ fun TextView.setTextAndVisible(@StringRes textResId: Int) {
|
||||
}
|
||||
|
||||
fun TextView.setTextColorAttr(@AttrRes attrResId: Int) {
|
||||
val colors = context.obtainStyledAttributes(intArrayOf(attrResId)).use {
|
||||
it.getColorStateList(0)
|
||||
}
|
||||
setTextColor(colors)
|
||||
setTextColor(context.getThemeColorStateList(attrResId))
|
||||
}
|
||||
@@ -4,20 +4,24 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.res.use
|
||||
|
||||
@Px
|
||||
fun Context.getThemeDimen(@AttrRes resId: Int) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDimension(0, 0f)
|
||||
}
|
||||
|
||||
fun Context.getThemeDrawable(@AttrRes resId: Int) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
fun Context.getThemeDrawable(
|
||||
@AttrRes resId: Int,
|
||||
) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDrawable(0)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getThemeColor(@AttrRes resId: Int, @ColorInt default: Int = Color.TRANSPARENT) =
|
||||
obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getColor(0, default)
|
||||
}
|
||||
fun Context.getThemeColor(
|
||||
@AttrRes resId: Int,
|
||||
@ColorInt default: Int = Color.TRANSPARENT
|
||||
) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getColor(0, default)
|
||||
}
|
||||
|
||||
fun Context.getThemeColorStateList(
|
||||
@AttrRes resId: Int,
|
||||
) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getColorStateList(0)
|
||||
}
|
||||
@@ -2,20 +2,15 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewHolder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
@@ -28,19 +23,15 @@ fun View.showKeyboard() {
|
||||
imm.showSoftInput(this, 0)
|
||||
}
|
||||
|
||||
inline fun <reified T : View> ViewGroup.inflate(@LayoutRes resId: Int) =
|
||||
LayoutInflater.from(context).inflate(resId, this, false) as T
|
||||
|
||||
val RecyclerView.hasItems: Boolean
|
||||
get() = (adapter?.itemCount ?: 0) > 0
|
||||
|
||||
fun RecyclerView.clearItemDecorations() {
|
||||
suppressLayout(true)
|
||||
while (itemDecorationCount > 0) {
|
||||
removeItemDecorationAt(0)
|
||||
}
|
||||
suppressLayout(false)
|
||||
}
|
||||
|
||||
var RecyclerView.firstItem: Int
|
||||
var RecyclerView.firstVisibleItemPosition: Int
|
||||
get() = (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition()
|
||||
?: RecyclerView.NO_POSITION
|
||||
set(value) {
|
||||
@@ -49,18 +40,6 @@ var RecyclerView.firstItem: Int
|
||||
}
|
||||
}
|
||||
|
||||
inline fun View.showPopupMenu(
|
||||
@MenuRes menuRes: Int,
|
||||
onPrepare: (Menu) -> Unit = {},
|
||||
onItemClick: PopupMenu.OnMenuItemClickListener,
|
||||
) {
|
||||
val menu = PopupMenu(context, this)
|
||||
menu.inflate(menuRes)
|
||||
menu.setOnMenuItemClickListener(onItemClick)
|
||||
onPrepare(menu.menu)
|
||||
menu.show()
|
||||
}
|
||||
|
||||
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
||||
if (visibility != View.VISIBLE) {
|
||||
return false
|
||||
@@ -97,7 +76,7 @@ inline fun ViewPager2.doOnPageChanged(crossinline callback: (Int) -> Unit) {
|
||||
}
|
||||
|
||||
val ViewPager2.recyclerView: RecyclerView?
|
||||
inline get() = children.find { it is RecyclerView } as? RecyclerView
|
||||
get() = children.firstNotNullOfOrNull { it as? RecyclerView }
|
||||
|
||||
fun View.resetTransformations() {
|
||||
alpha = 1f
|
||||
@@ -106,6 +85,7 @@ fun View.resetTransformations() {
|
||||
translationZ = 0f
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
rotation = 0f
|
||||
rotationX = 0f
|
||||
rotationY = 0f
|
||||
}
|
||||
@@ -133,8 +113,17 @@ fun RecyclerView.findCenterViewPosition(): Int {
|
||||
return getChildAdapterPosition(view)
|
||||
}
|
||||
|
||||
inline fun <reified T> RecyclerView.ViewHolder.getItem(): T? {
|
||||
return ((this as? AdapterDelegateViewBindingViewHolder<*, *>)?.item as? T)
|
||||
fun <T> RecyclerView.ViewHolder.getItem(clazz: Class<T>): T? {
|
||||
val rawItem = when (this) {
|
||||
is AdapterDelegateViewBindingViewHolder<*, *> -> item
|
||||
is AdapterDelegateViewHolder<*> -> item
|
||||
else -> null
|
||||
} ?: return null
|
||||
return if (clazz.isAssignableFrom(rawItem.javaClass)) {
|
||||
clazz.cast(rawItem)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Slider.setValueRounded(newValue: Float) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.ImageResult
|
||||
import coil.request.SuccessResult
|
||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
|
||||
class ImageRequestIndicatorListener(
|
||||
@@ -10,9 +11,9 @@ class ImageRequestIndicatorListener(
|
||||
|
||||
override fun onCancel(request: ImageRequest) = indicator.hide()
|
||||
|
||||
override fun onError(request: ImageRequest, throwable: Throwable) = indicator.hide()
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) = indicator.hide()
|
||||
|
||||
override fun onStart(request: ImageRequest) = indicator.show()
|
||||
|
||||
override fun onSuccess(request: ImageRequest, metadata: ImageResult.Metadata) = indicator.hide()
|
||||
override fun onSuccess(request: ImageRequest, result: SuccessResult) = indicator.hide()
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.slider.LabelFormatter
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class IntPercentLabelFormatter : LabelFormatter {
|
||||
override fun getFormattedValue(value: Float) = "%d%%".format(value.toInt())
|
||||
class IntPercentLabelFormatter(context: Context) : LabelFormatter {
|
||||
|
||||
private val pattern = context.getString(R.string.percent_string_pattern)
|
||||
|
||||
override fun getFormattedValue(value: Float) = pattern.format(value.toInt().toString())
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
data class Progress(
|
||||
val value: Int,
|
||||
val total: Int
|
||||
val total: Int,
|
||||
) : Parcelable, Comparable<Progress> {
|
||||
|
||||
override fun compareTo(other: Progress): Int {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import android.os.SystemClock
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
private const val MIN_ESTIMATE_TICKS = 4
|
||||
private const val NO_TIME = -1L
|
||||
|
||||
class TimeLeftEstimator {
|
||||
|
||||
private var times = ArrayList<Int>()
|
||||
private var lastTick: Tick? = null
|
||||
private val tooLargeTime = TimeUnit.DAYS.toMillis(1)
|
||||
|
||||
fun tick(value: Int, total: Int) {
|
||||
if (total < 0) {
|
||||
emptyTick()
|
||||
return
|
||||
}
|
||||
val tick = Tick(value, total, SystemClock.elapsedRealtime())
|
||||
lastTick?.let {
|
||||
val ticksCount = value - it.value
|
||||
times.add(((tick.time - it.time) / ticksCount.toDouble()).roundToInt())
|
||||
}
|
||||
lastTick = tick
|
||||
}
|
||||
|
||||
fun emptyTick() {
|
||||
lastTick = null
|
||||
}
|
||||
|
||||
fun getEstimatedTimeLeft(): Long {
|
||||
val progress = lastTick ?: return NO_TIME
|
||||
if (times.size < MIN_ESTIMATE_TICKS) {
|
||||
return NO_TIME
|
||||
}
|
||||
val timePerTick = times.average()
|
||||
val ticksLeft = progress.total - progress.value
|
||||
val eta = (ticksLeft * timePerTick).roundToLong()
|
||||
return if (eta < tooLargeTime) eta else NO_TIME
|
||||
}
|
||||
|
||||
private class Tick(
|
||||
val value: Int,
|
||||
val total: Int,
|
||||
val time: Long,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user