Permormance improvements
This commit is contained in:
@@ -48,7 +48,7 @@ class BookmarksViewModel @Inject constructor(
|
||||
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = repository.removeBookmarks(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ElementsIntoSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -40,6 +43,8 @@ import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
@@ -205,5 +210,16 @@ interface AppModule {
|
||||
MemoryContentCache(application)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@LocalStorageChanges
|
||||
fun provideMutableLocalStorageChangesFlow(): MutableSharedFlow<LocalManga?> = MutableSharedFlow()
|
||||
|
||||
@Provides
|
||||
@LocalStorageChanges
|
||||
fun provideLocalStorageChangesFlow(
|
||||
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
|
||||
): SharedFlow<LocalManga?> = flow.asSharedFlow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.transition.Slide
|
||||
import android.transition.TransitionManager
|
||||
@@ -66,13 +64,6 @@ class DetailsActivity :
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
|
||||
|
||||
private val downloadReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val downloadedManga = DownloadService.getDownloadedManga(intent) ?: return
|
||||
viewModel.onDownloadComplete(downloadedManga)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
|
||||
@@ -130,7 +121,6 @@ class DetailsActivity :
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
|
||||
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
||||
addMenuProvider(
|
||||
DetailsMenuProvider(
|
||||
activity = this,
|
||||
@@ -142,11 +132,6 @@ class DetailsActivity :
|
||||
binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(downloadReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
when (v.id) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -19,7 +18,6 @@ import coil.request.ImageRequest
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
@@ -46,7 +44,6 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||
import org.koitharu.kotatsu.utils.ext.crossfade
|
||||
import org.koitharu.kotatsu.utils.ext.drawableTop
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
@@ -55,8 +52,6 @@ import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.utils.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -94,6 +89,7 @@ class DetailsFragment :
|
||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
|
||||
viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
@@ -150,20 +146,9 @@ class DetailsFragment :
|
||||
}
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
infoLayout.textViewSource.isVisible = false
|
||||
val file = manga.url.toUri().toFileOrNull()
|
||||
if (file != null) {
|
||||
viewLifecycleScope.launch {
|
||||
val size = file.computeSize()
|
||||
infoLayout.textViewSize.text = FileSize.BYTES.format(requireContext(), size)
|
||||
infoLayout.textViewSize.isVisible = true
|
||||
}
|
||||
} else {
|
||||
infoLayout.textViewSize.isVisible = false
|
||||
}
|
||||
} else {
|
||||
infoLayout.textViewSource.text = manga.source.title
|
||||
infoLayout.textViewSource.isVisible = true
|
||||
infoLayout.textViewSize.isVisible = false
|
||||
}
|
||||
|
||||
infoLayout.textViewNsfw.isVisible = manga.isNsfw
|
||||
@@ -192,6 +177,16 @@ class DetailsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val textView = binding.infoLayout.textViewSize
|
||||
if (size == 0L) {
|
||||
textView.isVisible = false
|
||||
} else {
|
||||
textView.text = FileSize.BYTES.format(textView.context, size)
|
||||
textView.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(history: HistoryInfo) {
|
||||
binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.text.Html
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.LiveData
|
||||
@@ -14,6 +15,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
@@ -36,6 +38,8 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -46,8 +50,10 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.utils.ext.toFileOrNull
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -62,6 +68,7 @@ class DetailsViewModel @Inject constructor(
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
private val delegate: MangaDetailsDelegate,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job
|
||||
@@ -109,6 +116,23 @@ class DetailsViewModel @Inject constructor(
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
val localSize = combine(
|
||||
delegate.manga,
|
||||
delegate.relatedManga,
|
||||
) { m1, m2 ->
|
||||
val url = when {
|
||||
m1?.source == MangaSource.LOCAL -> m1.url
|
||||
m2?.source == MangaSource.LOCAL -> m2.url
|
||||
else -> null
|
||||
}
|
||||
if (url != null) {
|
||||
val file = url.toUri().toFileOrNull()
|
||||
file?.computeSize() ?: 0L
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
|
||||
|
||||
val description = delegate.manga
|
||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
||||
.transformLatest {
|
||||
@@ -174,6 +198,10 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
loadingJob = doLoad()
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
@@ -195,7 +223,7 @@ class DetailsViewModel @Inject constructor(
|
||||
runCatchingCancellable {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
onMangaRemoved.postCall(manga)
|
||||
onMangaRemoved.emitCall(manga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,26 +250,6 @@ class DetailsViewModel @Inject constructor(
|
||||
chaptersQuery.value = query?.trim().orEmpty()
|
||||
}
|
||||
|
||||
fun onDownloadComplete(downloadedManga: Manga) {
|
||||
val currentManga = delegate.manga.value ?: return
|
||||
if (currentManga.id != downloadedManga.id) {
|
||||
return
|
||||
}
|
||||
if (currentManga.source == MangaSource.LOCAL) {
|
||||
reload()
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
runCatchingCancellable {
|
||||
localMangaRepository.getDetails(downloadedManga)
|
||||
}.onSuccess {
|
||||
delegate.relatedManga.value = it
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
|
||||
val scrobbler = getScrobbler(index) ?: return
|
||||
launchJob(Dispatchers.Default) {
|
||||
@@ -287,6 +295,27 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
||||
downloadedManga ?: return
|
||||
val currentManga = delegate.manga.value ?: return
|
||||
if (currentManga.id != downloadedManga.manga.id) {
|
||||
return
|
||||
}
|
||||
if (currentManga.source == MangaSource.LOCAL) {
|
||||
reload()
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
runCatchingCancellable {
|
||||
localMangaRepository.getDetails(downloadedManga.manga)
|
||||
}.onSuccess {
|
||||
delegate.relatedManga.value = it
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Spanned.filterSpans(): CharSequence {
|
||||
val spannable = SpannableString.valueOf(this)
|
||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
@@ -29,6 +30,8 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.ui.service.PausingHandle
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
||||
@@ -58,6 +61,7 @@ class DownloadManager @Inject constructor(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
) {
|
||||
|
||||
private val coverWidth = context.resources.getDimensionPixelSize(
|
||||
@@ -165,13 +169,18 @@ class DownloadManager @Inject constructor(
|
||||
delay(SLOWDOWN_DELAY)
|
||||
}
|
||||
}
|
||||
output.flushChapter(chapter)
|
||||
if (output.flushChapter(chapter)) {
|
||||
runCatchingCancellable {
|
||||
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
|
||||
}.onFailure(Throwable::printStackTraceDebug)
|
||||
}
|
||||
}
|
||||
outState.value = DownloadState.PostProcessing(startId, data, cover)
|
||||
output.mergeWithExisting()
|
||||
output.finish()
|
||||
val localManga = LocalMangaInput.of(output.rootFile).getManga().manga
|
||||
outState.value = DownloadState.Done(startId, data, cover, localManga)
|
||||
val localManga = LocalMangaInput.of(output.rootFile).getManga()
|
||||
localStorageChanges.emit(localManga)
|
||||
outState.value = DownloadState.Done(startId, data, cover, localManga.manga)
|
||||
} catch (e: CancellationException) {
|
||||
outState.value = DownloadState.Cancelled(startId, manga, cover)
|
||||
throw e
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.download.ui.service
|
||||
|
||||
import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -56,7 +57,6 @@ class DownloadService : BaseService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isRunning = true
|
||||
downloadNotification = DownloadNotification(this)
|
||||
wakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
|
||||
@@ -93,7 +93,6 @@ class DownloadService : BaseService() {
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
isRunning = false
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -205,12 +204,6 @@ class DownloadService : BaseService() {
|
||||
|
||||
companion object {
|
||||
|
||||
var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
@Deprecated("Use LocalMangaRepository.watchReadableDirs instead")
|
||||
const val ACTION_DOWNLOAD_COMPLETE = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE"
|
||||
|
||||
private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL"
|
||||
private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME"
|
||||
|
||||
@@ -259,13 +252,6 @@ class DownloadService : BaseService() {
|
||||
fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME)
|
||||
.putExtra(EXTRA_CANCEL_ID, startId)
|
||||
|
||||
fun getDownloadedManga(intent: Intent?): Manga? {
|
||||
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
|
||||
return intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showStartedSnackbar(view: View) {
|
||||
Snackbar.make(view, R.string.download_started, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.details) {
|
||||
|
||||
@@ -54,7 +54,7 @@ class ExploreViewModel @Inject constructor(
|
||||
fun openRandom() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
|
||||
onOpenManga.postCall(manga)
|
||||
onOpenManga.emitCall(manga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ExploreViewModel @Inject constructor(
|
||||
val rollback = ReversibleHandle {
|
||||
settings.hiddenSources -= source.name
|
||||
}
|
||||
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEdit
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_ID
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -34,7 +35,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
category.postValue(
|
||||
category.emitValue(
|
||||
if (categoryId != NO_ID) {
|
||||
repository.getCategory(categoryId)
|
||||
} else {
|
||||
@@ -57,7 +58,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
|
||||
} else {
|
||||
repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled, isVisibleOnShelf)
|
||||
}
|
||||
onSaved.postCall(Unit)
|
||||
onSaved.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
@@ -28,7 +27,6 @@ import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -43,9 +41,6 @@ class FavouritesListViewModel @Inject constructor(
|
||||
|
||||
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
||||
|
||||
var categoryName: String? = null
|
||||
private set
|
||||
|
||||
val sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
|
||||
MutableLiveData(null)
|
||||
} else {
|
||||
@@ -82,18 +77,6 @@ class FavouritesListViewModel @Inject constructor(
|
||||
emit(listOf(it.toErrorState(canRetry = false)))
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
if (categoryId != NO_ID) {
|
||||
launchJob {
|
||||
categoryName = withContext(Dispatchers.Default) {
|
||||
runCatchingCancellable {
|
||||
repository.getCategory(categoryId).title
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
override fun onRetry() = Unit
|
||||
@@ -108,7 +91,7 @@ class FavouritesListViewModel @Inject constructor(
|
||||
} else {
|
||||
repository.removeFromCategory(categoryId, ids)
|
||||
}
|
||||
onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -45,7 +46,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
val isGroupingEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
|
||||
.onEach { isGroupingEnabled.postValue(it) }
|
||||
.onEach { isGroupingEnabled.emitValue(it) }
|
||||
|
||||
override val content = combine(
|
||||
repository.observeAllWithHistory(),
|
||||
@@ -77,7 +78,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
override fun onRetry() = Unit
|
||||
|
||||
fun clearHistory() {
|
||||
launchLoadingJob {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = repository.delete(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,9 @@ import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
|
||||
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
|
||||
@@ -30,7 +28,7 @@ import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -46,7 +44,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
@@ -127,7 +124,7 @@ abstract class MangaListFragment :
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
||||
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -173,17 +170,6 @@ abstract class MangaListFragment :
|
||||
listAdapter?.setItems(list, listCommitCallback)
|
||||
}
|
||||
|
||||
private fun onActionDone(action: ReversibleAction) {
|
||||
val handle = action.handle
|
||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
||||
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
|
||||
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
|
||||
if (handle != null) {
|
||||
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
||||
}
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun resolveException(e: Throwable) {
|
||||
if (ExceptionResolver.canResolve(e)) {
|
||||
viewLifecycleScope.launch {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:androidx.annotation.OptIn(ExperimentalBadgeUtils::class)
|
||||
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
@@ -5,6 +7,7 @@ import androidx.annotation.CheckResult
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.badge.BadgeUtils
|
||||
import com.google.android.material.badge.ExperimentalBadgeUtils
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
@CheckResult
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
|
||||
fun emptyHintAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: ListStateHolderListener,
|
||||
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
|
||||
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
|
||||
@@ -15,9 +22,13 @@ fun emptyHintAD(
|
||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||
|
||||
bind {
|
||||
binding.icon.setImageResource(item.icon)
|
||||
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
|
||||
binding.textPrimary.setText(item.textPrimary)
|
||||
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
binding.icon.disposeImageRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package org.koitharu.kotatsu.local
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import java.io.File
|
||||
@@ -9,6 +11,8 @@ class LocalManga(
|
||||
val manga: Manga,
|
||||
) {
|
||||
|
||||
constructor(manga: Manga) : this(manga.url.toUri().toFile(), manga)
|
||||
|
||||
var createdAt: Long = -1L
|
||||
private set
|
||||
get() {
|
||||
|
||||
@@ -7,15 +7,10 @@ import androidx.annotation.WorkerThread
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.util.observe
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
@@ -36,6 +31,7 @@ class LocalStorageManager @Inject constructor(
|
||||
val contentResolver: ContentResolver
|
||||
get() = context.contentResolver
|
||||
|
||||
@WorkerThread
|
||||
fun createHttpCache(): Cache {
|
||||
val directory = File(context.externalCacheDir ?: context.cacheDir, "http")
|
||||
directory.mkdirs()
|
||||
@@ -80,14 +76,6 @@ class LocalStorageManager @Inject constructor(
|
||||
|
||||
fun getStorageDisplayName(file: File) = file.getStorageName(context)
|
||||
|
||||
fun observe(files: List<File>): Flow<File> {
|
||||
if (files.isEmpty()) {
|
||||
return emptyFlow()
|
||||
}
|
||||
return files.asFlow()
|
||||
.flatMapMerge(files.size) { it.observe() }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getConfiguredStorageDirs(): MutableSet<File> {
|
||||
val set = getAvailableStorageDirs()
|
||||
|
||||
@@ -6,9 +6,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.utils.ext.subdir
|
||||
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.utils.ext.takeIfWriteable
|
||||
@@ -20,47 +23,41 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val cacheDir = checkNotNull(findSuitableDir(context)) {
|
||||
val dirs = (context.externalCacheDirs + context.cacheDir).joinToString(";") {
|
||||
it?.absolutePath.toString()
|
||||
private val cacheDir = SuspendLazy {
|
||||
val dirs = context.externalCacheDirs + context.cacheDir
|
||||
dirs.firstNotNullOf {
|
||||
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
|
||||
}
|
||||
"Cannot find any suitable directory for PagesCache: [$dirs]"
|
||||
}
|
||||
private val lruCache = createDiskLruCacheSafe(
|
||||
dir = cacheDir,
|
||||
size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||
)
|
||||
private val lruCache = SuspendLazy {
|
||||
val dir = cacheDir.get()
|
||||
val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
||||
runCatchingCancellable {
|
||||
DiskLruCache.create(dir, size)
|
||||
}.recoverCatching { error ->
|
||||
error.printStackTraceDebug()
|
||||
dir.deleteRecursively()
|
||||
dir.mkdir()
|
||||
DiskLruCache.create(dir, size)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend fun get(url: String): File? = runInterruptible(Dispatchers.IO) {
|
||||
lruCache.get(url)?.takeIfReadable()
|
||||
suspend fun get(url: String): File? {
|
||||
val cache = lruCache.get()
|
||||
return runInterruptible(Dispatchers.IO) {
|
||||
cache.get(url)?.takeIfReadable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
|
||||
val file = File(cacheDir.parentFile, url.longHashCode().toString())
|
||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||
try {
|
||||
file.outputStream().use { out ->
|
||||
inputStream.copyToSuspending(out)
|
||||
}
|
||||
lruCache.put(url, file)
|
||||
lruCache.get().put(url, file)
|
||||
} finally {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSuitableDir(context: Context): File? {
|
||||
val dirs = context.externalCacheDirs + context.cacheDir
|
||||
return dirs.firstNotNullOfOrNull {
|
||||
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class LocalStorageChanges
|
||||
@@ -6,11 +6,13 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||
@@ -23,16 +25,19 @@ import javax.inject.Inject
|
||||
class SingleMangaImporter @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val storageManager: LocalStorageManager,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
) {
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
|
||||
suspend fun import(uri: Uri, progressState: MutableStateFlow<Float>?): LocalManga {
|
||||
return if (isDirectory(uri)) {
|
||||
val result = if (isDirectory(uri)) {
|
||||
importDirectory(uri, progressState)
|
||||
} else {
|
||||
importFile(uri, progressState)
|
||||
}
|
||||
localStorageChanges.emit(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun importFile(uri: Uri, progressState: MutableStateFlow<Float>?): LocalManga {
|
||||
|
||||
@@ -57,10 +57,11 @@ class LocalMangaDirOutput(
|
||||
index.addChapter(chapter)
|
||||
}
|
||||
|
||||
override suspend fun flushChapter(chapter: MangaChapter) {
|
||||
val output = chaptersOutput.remove(chapter) ?: return
|
||||
override suspend fun flushChapter(chapter: MangaChapter): Boolean {
|
||||
val output = chaptersOutput.remove(chapter) ?: return false
|
||||
output.flushAndFinish()
|
||||
flushIndex()
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun finish() {
|
||||
|
||||
@@ -16,7 +16,7 @@ sealed class LocalMangaOutput(
|
||||
|
||||
abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String)
|
||||
|
||||
abstract suspend fun flushChapter(chapter: MangaChapter)
|
||||
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean
|
||||
|
||||
abstract suspend fun finish()
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class LocalMangaZipOutput(
|
||||
index.addChapter(chapter)
|
||||
}
|
||||
|
||||
override suspend fun flushChapter(chapter: MangaChapter) = Unit
|
||||
override suspend fun flushChapter(chapter: MangaChapter): Boolean = false
|
||||
|
||||
override suspend fun finish() {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.koitharu.kotatsu.local.data.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.FileObserver
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.io.File
|
||||
|
||||
fun File.observe() = callbackFlow {
|
||||
val observer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FlowFileObserverQ(this, this@observe)
|
||||
} else {
|
||||
FlowFileObserver(this, this@observe)
|
||||
}
|
||||
observer.startWatching()
|
||||
awaitClose { observer.stopWatching() }
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private class FlowFileObserverQ(
|
||||
private val producerScope: ProducerScope<File>,
|
||||
private val file: File,
|
||||
) : FileObserver(file, CREATE or DELETE or CLOSE_WRITE) {
|
||||
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
producerScope.trySendBlocking(
|
||||
if (path == null) file else file.resolve(path),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private class FlowFileObserver(
|
||||
private val producerScope: ProducerScope<File>,
|
||||
private val file: File,
|
||||
) : FileObserver(file.absolutePath, CREATE or DELETE or CLOSE_WRITE) {
|
||||
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
producerScope.trySendBlocking(
|
||||
if (path == null) file else file.resolve(path),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
@@ -106,21 +107,24 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
||||
|
||||
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
|
||||
val files = getAllFiles()
|
||||
val input = files.firstNotNullOfOrNull { file ->
|
||||
LocalMangaInput.of(file).takeIf {
|
||||
runCatchingCancellable {
|
||||
it.getMangaInfo()
|
||||
}.getOrNull()?.id == remoteManga.id
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return input?.getManga()
|
||||
}
|
||||
|
||||
suspend fun watchReadableDirs(): Flow<File> {
|
||||
val filter = TempFileFilter()
|
||||
val dirs = storageManager.getReadableDirs()
|
||||
return storageManager.observe(dirs)
|
||||
.filterNot { filter.accept(it, it.name) }
|
||||
return channelFlow {
|
||||
for (file in files) {
|
||||
launch {
|
||||
val mangaInput = LocalMangaInput.of(file)
|
||||
runCatchingCancellable {
|
||||
val mangaInfo = mangaInput.getMangaInfo()
|
||||
if (mangaInfo != null && mangaInfo.id == remoteManga.id) {
|
||||
send(mangaInput)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.firstOrNull()?.getManga()
|
||||
}
|
||||
|
||||
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
|
||||
@@ -149,7 +153,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
||||
dirs.flatMap { dir ->
|
||||
dir.listFiles(TempFileFilter())?.toList().orEmpty()
|
||||
}.forEach { file ->
|
||||
file.delete()
|
||||
file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -9,10 +9,12 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
@@ -25,6 +27,10 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||
@Inject
|
||||
lateinit var localMangaRepository: LocalMangaRepository
|
||||
|
||||
@Inject
|
||||
@LocalStorageChanges
|
||||
lateinit var localStorageChanges: MutableSharedFlow<LocalManga?>
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isRunning = true
|
||||
@@ -41,10 +47,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||
startForeground()
|
||||
val mangaWithChapters = localMangaRepository.getDetails(manga)
|
||||
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
|
||||
sendBroadcast(
|
||||
Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE)
|
||||
.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)),
|
||||
)
|
||||
localStorageChanges.emit(LocalManga(manga))
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
@@ -27,6 +26,8 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -46,6 +47,7 @@ class LocalListViewModel @Inject constructor(
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
private val tagHighlighter: MangaTagHighlighter,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings), ListExtraProvider {
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Unit>()
|
||||
@@ -83,7 +85,14 @@ class LocalListViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
watchDirectories()
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
.collectLatest {
|
||||
if (refreshJob?.isActive != true) {
|
||||
doRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateFilter(tags: Set<MangaTag>) {
|
||||
@@ -108,21 +117,19 @@ class LocalListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun delete(ids: Set<Long>) {
|
||||
launchLoadingJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
||||
for (manga in itemsToRemove) {
|
||||
val original = repository.getRemoteManga(manga)
|
||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
||||
runCatchingCancellable {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
mangaList.update { list ->
|
||||
list?.filterNot { it.id == manga.id }
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
||||
for (manga in itemsToRemove) {
|
||||
val original = repository.getRemoteManga(manga)
|
||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
||||
runCatchingCancellable {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
mangaList.update { list ->
|
||||
list?.filterNot { it.id == manga.id }
|
||||
}
|
||||
}
|
||||
onMangaRemoved.call(Unit)
|
||||
onMangaRemoved.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,15 +144,6 @@ class LocalListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchDirectories() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
repository.watchReadableDirs()
|
||||
.collectLatest {
|
||||
doRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
|
||||
val tags = HashMap<MangaTag, Int>()
|
||||
for (item in mangaList) {
|
||||
|
||||
@@ -60,9 +60,9 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun openLastReader() {
|
||||
launchLoadingJob {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
|
||||
onOpenReader.call(manga)
|
||||
onOpenReader.emitCall(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.requireValue
|
||||
@@ -202,12 +203,12 @@ class ReaderViewModel @Inject constructor(
|
||||
prevJob?.cancelAndJoin()
|
||||
try {
|
||||
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
|
||||
onPageSaved.postCall(dest)
|
||||
onPageSaved.emitCall(dest)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
onPageSaved.postCall(null)
|
||||
onPageSaved.emitCall(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,7 +286,7 @@ class ReaderViewModel @Inject constructor(
|
||||
percent = computePercent(state.chapterId, state.page),
|
||||
)
|
||||
bookmarksRepository.addBookmark(bookmark)
|
||||
onShowToast.postCall(R.string.bookmark_added)
|
||||
onShowToast.emitCall(R.string.bookmark_added)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +323,7 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch
|
||||
mangaData.value = manga.filterChapters(branch)
|
||||
readerMode.postValue(mode)
|
||||
readerMode.emitValue(mode)
|
||||
|
||||
chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId)
|
||||
// save state
|
||||
@@ -333,7 +334,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
notifyStateChanged()
|
||||
content.postValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
|
||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +342,7 @@ class ReaderViewModel @Inject constructor(
|
||||
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||
content.postValue(ReaderContent(chaptersLoader.snapshot(), null))
|
||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -43,7 +44,7 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val repository = mangaRepositoryFactory.create(page.source)
|
||||
val url = repository.getPageUrl(page)
|
||||
preview.postValue(
|
||||
preview.emitValue(
|
||||
MangaPage(
|
||||
id = page.id,
|
||||
url = url,
|
||||
@@ -71,7 +72,7 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
fun save() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
||||
onDismiss.postCall(Unit)
|
||||
onDismiss.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class RemoteListViewModel @Inject constructor(
|
||||
e.printStackTraceDebug()
|
||||
listError.value = e
|
||||
if (!mangaList.value.isNullOrEmpty()) {
|
||||
errorEvent.postCall(e)
|
||||
errorEvent.emitCall(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import org.koitharu.kotatsu.utils.ext.require
|
||||
import javax.inject.Inject
|
||||
@@ -51,22 +52,22 @@ class ScrobblerConfigViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
scrobbler.user
|
||||
.onEach { user.postValue(it) }
|
||||
.onEach { user.emitValue(it) }
|
||||
.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
|
||||
fun onAuthCodeReceived(authCode: String) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val newUser = scrobbler.authorize(authCode)
|
||||
user.postValue(newUser)
|
||||
user.emitValue(newUser)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
scrobbler.logout()
|
||||
user.postValue(null)
|
||||
onLoggedOut.postCall(Unit)
|
||||
user.emitValue(null)
|
||||
onLoggedOut.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.require
|
||||
import org.koitharu.kotatsu.utils.ext.requireValue
|
||||
@@ -135,7 +136,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
||||
}
|
||||
doneJob = launchJob(Dispatchers.Default) {
|
||||
currentScrobbler.linkManga(manga.id, targetId)
|
||||
onClose.postCall(Unit)
|
||||
onClose.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
||||
try {
|
||||
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
|
||||
if (info != null) {
|
||||
selectedItemId.postValue(info.targetId)
|
||||
selectedItemId.emitValue(info.targetId)
|
||||
}
|
||||
} finally {
|
||||
loadList(append = false)
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
@@ -96,7 +97,7 @@ class MultiSearchViewModel @Inject constructor(
|
||||
listError.value = null
|
||||
listData.value = emptyList()
|
||||
loadingData.value = true
|
||||
query.postValue(q)
|
||||
query.emitValue(q)
|
||||
searchImpl(q)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEBOUNCE_TIMEOUT = 500L
|
||||
@@ -97,7 +98,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
buildSearchSuggestion(searchQuery, hiddenSources)
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
suggestion.postValue(it)
|
||||
suggestion.emitValue(it)
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
@@ -66,13 +66,13 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onProgressChanged(progress: Progress?) {
|
||||
private fun onProgressChanged(value: Float) {
|
||||
with(binding.progressBar) {
|
||||
isIndeterminate = progress == null
|
||||
isVisible = true
|
||||
if (progress != null) {
|
||||
this.max = progress.total
|
||||
this.progress = progress.value
|
||||
val wasIndeterminate = isIndeterminate
|
||||
isIndeterminate = value < 0
|
||||
if (value >= 0) {
|
||||
setProgressCompat((value * max).roundToInt(), !wasIndeterminate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BackupViewModel @Inject constructor(
|
||||
@@ -18,7 +17,7 @@ class BackupViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val progress = MutableLiveData<Progress?>(null)
|
||||
val progress = MutableLiveData(-1f)
|
||||
val onBackupDone = SingleLiveEvent<File>()
|
||||
|
||||
init {
|
||||
@@ -26,18 +25,18 @@ class BackupViewModel @Inject constructor(
|
||||
val file = BackupZipOutput(context).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
|
||||
progress.value = Progress(0, 3)
|
||||
progress.value = 0f
|
||||
backup.put(repository.dumpHistory())
|
||||
|
||||
progress.value = Progress(1, 3)
|
||||
progress.value = 0.3f
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
progress.value = Progress(2, 3)
|
||||
progress.value = 0.6f
|
||||
backup.put(repository.dumpFavourites())
|
||||
|
||||
progress.value = Progress(3, 3)
|
||||
progress.value = 0.9f
|
||||
backup.finish()
|
||||
progress.value = null
|
||||
progress.value = 1f
|
||||
backup.close()
|
||||
backup.file
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
@@ -51,13 +51,13 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onProgressChanged(progress: Progress?) {
|
||||
private fun onProgressChanged(value: Float) {
|
||||
with(binding.progressBar) {
|
||||
isVisible = true
|
||||
isIndeterminate = progress == null
|
||||
if (progress != null) {
|
||||
this.max = progress.total
|
||||
this.progress = progress.value
|
||||
val wasIndeterminate = isIndeterminate
|
||||
isIndeterminate = value < 0
|
||||
if (value >= 0) {
|
||||
setProgressCompat((value * max).roundToInt(), !wasIndeterminate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import javax.inject.Inject
|
||||
@@ -26,7 +25,7 @@ class RestoreViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val progress = MutableLiveData<Progress?>(null)
|
||||
val progress = MutableLiveData(-1f)
|
||||
val onRestoreDone = SingleLiveEvent<CompositeResult>()
|
||||
|
||||
init {
|
||||
@@ -47,16 +46,16 @@ class RestoreViewModel @Inject constructor(
|
||||
try {
|
||||
val result = CompositeResult()
|
||||
|
||||
progress.value = Progress(0, 3)
|
||||
progress.value = 0f
|
||||
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
|
||||
progress.value = Progress(1, 3)
|
||||
progress.value = 0.3f
|
||||
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
|
||||
progress.value = Progress(2, 3)
|
||||
progress.value = 0.6f
|
||||
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
|
||||
progress.value = Progress(3, 3)
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
} finally {
|
||||
backup.close()
|
||||
|
||||
@@ -82,7 +82,7 @@ class SourcesListViewModel @Inject constructor(
|
||||
val rollback = ReversibleHandle {
|
||||
setEnabled(source, true)
|
||||
}
|
||||
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||
}
|
||||
buildList()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -31,7 +32,7 @@ class TrackerSettingsViewModel @Inject constructor(
|
||||
|
||||
private fun updateCategoriesCount() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
categoriesCount.postValue(repository.getCategoriesCount())
|
||||
categoriesCount.emitValue(repository.getCategoriesCount())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
@@ -19,6 +20,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
@@ -32,6 +35,7 @@ class ShelfRepository @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val db: MangaDatabase,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) {
|
||||
|
||||
fun observeShelfContent(): Flow<ShelfContent> = combine(
|
||||
@@ -43,16 +47,15 @@ class ShelfRepository @Inject constructor(
|
||||
ShelfContent(history, favorites, updated, local)
|
||||
}
|
||||
|
||||
fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
|
||||
return flow {
|
||||
emit(null)
|
||||
emitAll(localMangaRepository.watchReadableDirs())
|
||||
}.mapLatest {
|
||||
localMangaRepository.getList(0, null, sortOrder)
|
||||
}
|
||||
private fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
|
||||
return localStorageChanges
|
||||
.onStart { emit(null) }
|
||||
.mapLatest {
|
||||
localMangaRepository.getList(0, null, sortOrder)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
||||
private fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
||||
return db.favouriteCategoriesDao.observeAll()
|
||||
.flatMapLatest { categories ->
|
||||
val cats = categories.filter { it.isVisibleInLibrary }
|
||||
|
||||
@@ -7,7 +7,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
@@ -60,10 +59,9 @@ class ShelfViewModel @Inject constructor(
|
||||
repository.observeShelfContent(),
|
||||
) { sections, isTrackerEnabled, isConnected, content ->
|
||||
mapList(content, isTrackerEnabled, sections, isConnected)
|
||||
}.debounce(500)
|
||||
.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
}.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
@@ -93,7 +91,7 @@ class ShelfViewModel @Inject constructor(
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = favouritesRepository.removeFromCategory(category.id, ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,14 +101,14 @@ class ShelfViewModel @Inject constructor(
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = historyRepository.delete(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocal(ids: Set<Long>) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
repository.deleteLocalManga(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.removal_completed, null))
|
||||
onActionDone.emitCall(ReversibleAction(R.string.removal_completed, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +121,7 @@ class ShelfViewModel @Inject constructor(
|
||||
historyRepository.deleteAfter(minDate)
|
||||
R.string.removed_from_history
|
||||
}
|
||||
onActionDone.postCall(ReversibleAction(stringRes, null))
|
||||
onActionDone.emitCall(ReversibleAction(stringRes, null))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class ShelfAdapter(
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyHintAD(listener))
|
||||
.addDelegate(emptyHintAD(coil, lifecycleOwner, listener))
|
||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SyncAuthViewModel @Inject constructor(
|
||||
@@ -19,7 +19,7 @@ class SyncAuthViewModel @Inject constructor(
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val token = api.authenticate(email, password)
|
||||
val result = SyncAuthResult(email, password, token)
|
||||
onTokenObtained.postCall(result)
|
||||
onTokenObtained.emitCall(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class FeedViewModel @Inject constructor(
|
||||
if (clearCounters) {
|
||||
repository.clearCounters()
|
||||
}
|
||||
onFeedCleared.postCall(Unit)
|
||||
onFeedCleared.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.LinkedList
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CompositeMutex<T : Any> : Set<T> {
|
||||
|
||||
private val data = ArrayMap<T, MutableList<CancellableContinuation<Unit>>>()
|
||||
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
override val size: Int
|
||||
get() = data.size
|
||||
get() = state.size
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return data.containsKey(element)
|
||||
return state.containsKey(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return elements.all { x -> data.containsKey(x) }
|
||||
return elements.all { x -> state.containsKey(x) }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return data.isEmpty
|
||||
return state.isEmpty
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return data.keys.iterator()
|
||||
return state.keys.iterator()
|
||||
}
|
||||
|
||||
suspend fun lock(element: T) {
|
||||
while (coroutineContext.isActive) {
|
||||
waitForRemoval(element)
|
||||
mutex.withLock {
|
||||
if (data[element] == null) {
|
||||
data[element] = LinkedList<CancellableContinuation<Unit>>()
|
||||
if (state[element] == null) {
|
||||
state[element] = MutableStateFlow(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -47,23 +45,13 @@ class CompositeMutex<T : Any> : Set<T> {
|
||||
}
|
||||
|
||||
fun unlock(element: T) {
|
||||
val continuations = checkNotNull(data.remove(element)) {
|
||||
checkNotNull(state.remove(element)) {
|
||||
"CompositeMutex is not locked for $element"
|
||||
}
|
||||
continuations.forEach { c ->
|
||||
if (c.isActive) {
|
||||
c.resume(Unit)
|
||||
}
|
||||
}
|
||||
}.value = true
|
||||
}
|
||||
|
||||
private suspend fun waitForRemoval(element: T) {
|
||||
val list = data[element] ?: return
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
list.add(continuation)
|
||||
continuation.invokeOnCancellation {
|
||||
list.remove(continuation)
|
||||
}
|
||||
}
|
||||
val flow = state[element] ?: return
|
||||
flow.first { it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private const val DEFAULT_TIMEOUT = 5_000L
|
||||
|
||||
@@ -51,11 +57,16 @@ class FlowLiveData<T>(
|
||||
private inner class Collector : FlowCollector<T> {
|
||||
|
||||
private var previousValue: Any? = value
|
||||
private val dispatcher = Dispatchers.Main.immediate
|
||||
|
||||
override suspend fun emit(value: T) {
|
||||
if (previousValue != value) {
|
||||
previousValue = value
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||
withContext(dispatcher) {
|
||||
setValue(value)
|
||||
}
|
||||
} else {
|
||||
setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
class SingleLiveEvent<T> : LiveData<T>() {
|
||||
|
||||
@@ -33,4 +36,15 @@ class SingleLiveEvent<T> : LiveData<T>() {
|
||||
fun postCall(newValue: T) {
|
||||
postValue(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun emitCall(newValue: T) {
|
||||
val dispatcher = Dispatchers.Main.immediate
|
||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||
withContext(dispatcher) {
|
||||
setValue(newValue)
|
||||
}
|
||||
} else {
|
||||
setValue(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
|
||||
"LiveData value is null"
|
||||
@@ -15,3 +19,14 @@ fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: Buffere
|
||||
previous = it
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> MutableLiveData<T>.emitValue(newValue: T) {
|
||||
val dispatcher = Dispatchers.Main.immediate
|
||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||
withContext(dispatcher) {
|
||||
value = newValue
|
||||
}
|
||||
} else {
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.icu.lang.UCharacter.GraphemeClusterBreak.T
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||
if (obj == null || !isInstance(obj)) {
|
||||
return null
|
||||
}
|
||||
return obj as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.image
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Html
|
||||
import androidx.annotation.WorkerThread
|
||||
import coil.ImageLoader
|
||||
import coil.executeBlocking
|
||||
import coil.request.ImageRequest
|
||||
@@ -14,6 +15,7 @@ class CoilImageGetter @Inject constructor(
|
||||
private val coil: ImageLoader,
|
||||
) : Html.ImageGetter {
|
||||
|
||||
@WorkerThread
|
||||
override fun getDrawable(source: String?): Drawable? {
|
||||
return coil.executeBlocking(
|
||||
ImageRequest.Builder(context)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Deprecated("Should be replaced with Float")
|
||||
@Parcelize
|
||||
data class Progress(
|
||||
val value: Int,
|
||||
val total: Int,
|
||||
) : Parcelable, Comparable<Progress> {
|
||||
|
||||
override fun compareTo(other: Progress): Int {
|
||||
return if (this.total == other.total) {
|
||||
this.value.compareTo(other.value)
|
||||
} else {
|
||||
this.part().compareTo(other.part())
|
||||
}
|
||||
}
|
||||
|
||||
val isIndeterminate: Boolean
|
||||
get() = total <= 0
|
||||
|
||||
private fun part() = if (isIndeterminate) -1.0 else value / total.toDouble()
|
||||
}
|
||||
@@ -20,7 +20,9 @@
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:indeterminate="true"
|
||||
android:max="100" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.internal.headersContentLength
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
|
||||
class AppUpdateRepositoryTest {
|
||||
|
||||
private val okHttpClient = OkHttpClient()
|
||||
private val repository = AppUpdateRepository(okHttpClient)
|
||||
|
||||
@Test
|
||||
fun getLatestVersion() = runTest {
|
||||
val version = repository.getLatestVersion()
|
||||
val versionId = VersionId(version.name)
|
||||
|
||||
val apkHead = okHttpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(version.apkUrl)
|
||||
.head()
|
||||
.build(),
|
||||
).await()
|
||||
|
||||
Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME))
|
||||
Assert.assertTrue(apkHead.isSuccessful)
|
||||
Assert.assertEquals(version.apkSize, apkHead.headersContentLength())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
@@ -27,6 +33,7 @@ class CompositeMutexTest {
|
||||
}
|
||||
}
|
||||
yield()
|
||||
assertEquals(1, mutex.size)
|
||||
mutex.unlock(1)
|
||||
val tryLock = withTimeoutOrNull(1000) {
|
||||
mutex.lock(1)
|
||||
@@ -49,4 +56,4 @@ class CompositeMutexTest {
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user