mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-23 17:09:49 -07:00
* Keep screens loaded while they're hidden and rebuild the library faster * Count chapters in SQL for the migration list * Show the extension list sooner and check trust against loaded stores * Cancel the previous tracker search when searching again * Write reader progress less often and keep its work off the main thread * Stop rebuilding the settings search index on every key * Watch updates for widgets only while one is placed The query re-ran on every chapter or manga write even with no widget on the home screen. * Observe download pages and limit how often their progress updates * Throttle the library update notification and write fetch data together
203 lines
6.8 KiB
Kotlin
203 lines
6.8 KiB
Kotlin
package mihon.feature.upcoming
|
|
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.compose.ui.util.fastMap
|
|
import androidx.compose.ui.util.fastMapIndexedNotNull
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import dev.zacsweers.metro.AppScope
|
|
import dev.zacsweers.metro.ContributesIntoMap
|
|
import dev.zacsweers.metro.Inject
|
|
import dev.zacsweers.metro.binding
|
|
import dev.zacsweers.metrox.viewmodel.ViewModelKey
|
|
import eu.kanade.core.util.insertSeparatorsReversed
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.combine
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.flatMapLatest
|
|
import kotlinx.coroutines.flow.flowOn
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.coroutines.flow.stateIn
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.plus
|
|
import kotlinx.datetime.LocalDate
|
|
import kotlinx.datetime.TimeZone
|
|
import kotlinx.datetime.YearMonth
|
|
import kotlinx.datetime.toLocalDateTime
|
|
import kotlinx.datetime.yearMonth
|
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
|
import tachiyomi.core.common.preference.getAndSet
|
|
import tachiyomi.core.common.util.lang.SubscriberGate
|
|
import tachiyomi.domain.category.interactor.GetCategories
|
|
import tachiyomi.domain.category.model.Category
|
|
import tachiyomi.domain.manga.model.Manga
|
|
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
|
import kotlin.time.Clock
|
|
import kotlin.time.Duration.Companion.seconds
|
|
|
|
@Inject
|
|
@ViewModelKey
|
|
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
|
|
class UpcomingViewModel(
|
|
private val getUpcomingManga: GetUpcomingManga,
|
|
val getCategories: GetCategories,
|
|
val upcomingPreferences: UpcomingPreferences,
|
|
) : ViewModel() {
|
|
|
|
private val subscriberGate = SubscriberGate()
|
|
|
|
val excludedCategories = upcomingPreferences.filterExcludedCategories
|
|
val includedCategories = upcomingPreferences.filterIncludedCategories
|
|
|
|
private val selectedYearMonth = MutableStateFlow(
|
|
value = Clock.System.now()
|
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
|
.date
|
|
.yearMonth,
|
|
)
|
|
|
|
private val dialog = MutableStateFlow<Dialog?>(null)
|
|
|
|
private val hasActiveFilters = getUpcomingItemPreferenceFlow()
|
|
.map { prefs ->
|
|
listOf(
|
|
prefs.filterIncludedCategories,
|
|
prefs.filterExcludedCategories,
|
|
)
|
|
.any { it.isNotEmpty() }
|
|
}
|
|
.distinctUntilChanged()
|
|
|
|
private val upcoming = getUpcomingItemPreferenceFlow()
|
|
.distinctUntilChanged()
|
|
.flatMapLatest {
|
|
getUpcomingManga.subscribe(
|
|
excludedCategories = it.filterExcludedCategories,
|
|
includedCategories = it.filterIncludedCategories,
|
|
)
|
|
}
|
|
.distinctUntilChanged()
|
|
.map { it.toUpcomingUIModels() }
|
|
.flowOn(Dispatchers.Default)
|
|
.stateIn(viewModelScope + subscriberGate, SharingStarted.Eagerly, emptyList())
|
|
|
|
val state: StateFlow<State> = combine(
|
|
upcoming,
|
|
selectedYearMonth,
|
|
dialog,
|
|
hasActiveFilters,
|
|
) { upcoming, selectedYearMonth, dialog, hasActiveFilters ->
|
|
State(
|
|
selectedYearMonth = selectedYearMonth,
|
|
items = upcoming,
|
|
events = upcoming.toEvents(),
|
|
headerIndexes = upcoming.getHeaderIndexes(),
|
|
hasActiveFilters = hasActiveFilters,
|
|
dialog = dialog,
|
|
)
|
|
}
|
|
.stateIn(
|
|
viewModelScope + subscriberGate,
|
|
subscriberGate.whileSubscribed(5.seconds),
|
|
State(selectedYearMonth = selectedYearMonth.value),
|
|
)
|
|
|
|
private fun List<Manga>.toUpcomingUIModels(): List<UpcomingUIModel> {
|
|
var mangaCount = 0
|
|
return fastMap { UpcomingUIModel.Item(it) }
|
|
.insertSeparatorsReversed { before, after ->
|
|
if (after != null) mangaCount++
|
|
|
|
val beforeDate = before?.manga
|
|
?.expectedNextUpdate
|
|
?.toLocalDateTime(TimeZone.currentSystemDefault())
|
|
?.date
|
|
val afterDate = after?.manga
|
|
?.expectedNextUpdate
|
|
?.toLocalDateTime(TimeZone.currentSystemDefault())
|
|
?.date
|
|
|
|
if (beforeDate != afterDate && afterDate != null) {
|
|
UpcomingUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 }
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun List<UpcomingUIModel>.toEvents(): Map<LocalDate, Int> {
|
|
return filterIsInstance<UpcomingUIModel.Header>()
|
|
.associate { it.date to it.mangaCount }
|
|
}
|
|
|
|
private fun List<UpcomingUIModel>.getHeaderIndexes(): Map<LocalDate, Int> {
|
|
return fastMapIndexedNotNull { index, upcomingUIModel ->
|
|
if (upcomingUIModel is UpcomingUIModel.Header) {
|
|
upcomingUIModel.date to index
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
.toMap()
|
|
}
|
|
|
|
fun setSelectedYearMonth(yearMonth: YearMonth) {
|
|
selectedYearMonth.update { yearMonth }
|
|
}
|
|
|
|
private fun getUpcomingItemPreferenceFlow(): Flow<ItemPreferences> {
|
|
return combine(
|
|
upcomingPreferences.filterExcludedCategories.changes(),
|
|
upcomingPreferences.filterIncludedCategories.changes(),
|
|
) { excluded, included ->
|
|
ItemPreferences(
|
|
filterExcludedCategories = excluded,
|
|
filterIncludedCategories = included,
|
|
)
|
|
}
|
|
}
|
|
|
|
fun resetDialog() {
|
|
dialog.update { null }
|
|
}
|
|
|
|
fun showFilterDialog() {
|
|
dialog.update { Dialog.FilterSheet }
|
|
}
|
|
|
|
fun cycleCategory(category: Category) {
|
|
when (category.id) {
|
|
in includedCategories.get() -> {
|
|
includedCategories.getAndSet { it - category.id }
|
|
excludedCategories.getAndSet { it + category.id }
|
|
}
|
|
|
|
in excludedCategories.get() -> excludedCategories.getAndSet { it - category.id }
|
|
else -> includedCategories.getAndSet { it + category.id }
|
|
}
|
|
}
|
|
|
|
@Immutable
|
|
private data class ItemPreferences(
|
|
val filterExcludedCategories: List<Long>,
|
|
val filterIncludedCategories: List<Long>,
|
|
)
|
|
|
|
data class State(
|
|
val selectedYearMonth: YearMonth,
|
|
val items: List<UpcomingUIModel> = listOf(),
|
|
val events: Map<LocalDate, Int> = mapOf(),
|
|
val headerIndexes: Map<LocalDate, Int> = mapOf(),
|
|
val hasActiveFilters: Boolean = false,
|
|
val dialog: Dialog? = null,
|
|
)
|
|
|
|
sealed interface Dialog {
|
|
data object FilterSheet : Dialog
|
|
}
|
|
}
|