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