Improve updated manga screen

master
Koitharu 2 years ago
parent 32eba77639
commit ff4eac8269
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -172,6 +172,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true) get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) } set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
var isUpdatedGroupingEnabled: Boolean
get() = prefs.getBoolean(KEY_UPDATED_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_UPDATED_GROUPING, value) }
var isFeedHeaderVisible: Boolean
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
val isReadingIndicatorsEnabled: Boolean val isReadingIndicatorsEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_INDICATORS, true) get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
@ -575,6 +583,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output" const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last" const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_UPDATED_GROUPING = "updated_grouping"
const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_READING_INDICATORS = "reading_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters" const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
@ -652,5 +661,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_STATS_ENABLED = "stats_on" const val KEY_STATS_ENABLED = "stats_on"
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation" const val KEY_APP_TRANSLATION = "about_app_translation"
const val KEY_FEED_HEADER = "feed_header"
} }
} }

@ -65,6 +65,7 @@ import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.File import java.io.File
import kotlin.math.roundToLong import kotlin.math.roundToLong
import com.google.android.material.R as materialR
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
@ -141,7 +142,7 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
} else { } else {
// Set navbar scrim 70% of navigationBarColor // Set navbar scrim 70% of navigationBarColor
ElevationOverlayProvider(context).compositeOverlayIfNeeded( ElevationOverlayProvider(context).compositeOverlayIfNeeded(
context.getThemeColor(com.google.android.material.R.attr.colorSurfaceContainer, alphaFactor), context.getThemeColor(materialR.attr.colorSurfaceContainer, alphaFactor),
elevation, elevation,
) )
} }

@ -11,6 +11,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
import org.koitharu.kotatsu.list.ui.config.ListConfigSection import org.koitharu.kotatsu.list.ui.config.ListConfigSection
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesFragment
class MangaListMenuProvider( class MangaListMenuProvider(
private val fragment: Fragment, private val fragment: Fragment,
@ -26,6 +27,7 @@ class MangaListMenuProvider(
is HistoryListFragment -> ListConfigSection.History is HistoryListFragment -> ListConfigSection.History
is SuggestionsFragment -> ListConfigSection.Suggestions is SuggestionsFragment -> ListConfigSection.Suggestions
is FavouritesListFragment -> ListConfigSection.Favorites(fragment.categoryId) is FavouritesListFragment -> ListConfigSection.Favorites(fragment.categoryId)
is UpdatesFragment -> ListConfigSection.Updated
else -> ListConfigSection.General else -> ListConfigSection.General
} }
ListConfigBottomSheet.show(fragment.childFragmentManager, section) ListConfigBottomSheet.show(fragment.childFragmentManager, section)

@ -14,7 +14,6 @@ import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.ext.setValueRounded
@ -22,7 +21,6 @@ import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.databinding.SheetListModeBinding import org.koitharu.kotatsu.databinding.SheetListModeBinding
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ListConfigBottomSheet : class ListConfigBottomSheet :
@ -31,10 +29,6 @@ class ListConfigBottomSheet :
MaterialButtonToggleGroup.OnButtonCheckedListener, CompoundButton.OnCheckedChangeListener, MaterialButtonToggleGroup.OnButtonCheckedListener, CompoundButton.OnCheckedChangeListener,
AdapterView.OnItemSelectedListener { AdapterView.OnItemSelectedListener {
@Inject
@Deprecated("")
lateinit var settings: AppSettings
private val viewModel by viewModels<ListConfigViewModel>() private val viewModel by viewModels<ListConfigViewModel>()
override fun onCreateViewBinding( override fun onCreateViewBinding(
@ -57,11 +51,11 @@ class ListConfigBottomSheet :
binding.checkableGroup.addOnButtonCheckedListener(this) binding.checkableGroup.addOnButtonCheckedListener(this)
binding.switchGrouping.isVisible = viewModel.isGroupingAvailable binding.switchGrouping.isVisible = viewModel.isGroupingSupported
if (viewModel.isGroupingAvailable) { if (viewModel.isGroupingSupported) {
binding.switchGrouping.isEnabled = settings.historySortOrder.isGroupingSupported() binding.switchGrouping.isEnabled = viewModel.isGroupingAvailable
} }
binding.switchGrouping.isChecked = settings.isHistoryGroupingEnabled binding.switchGrouping.isChecked = viewModel.isGroupingEnabled
binding.switchGrouping.setOnCheckedChangeListener(this) binding.switchGrouping.setOnCheckedChangeListener(this)
val sortOrders = viewModel.getSortOrders() val sortOrders = viewModel.getSortOrders()
@ -99,7 +93,7 @@ class ListConfigBottomSheet :
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
when (buttonView.id) { when (buttonView.id) {
R.id.switch_grouping -> settings.isHistoryGroupingEnabled = isChecked R.id.switch_grouping -> viewModel.isGroupingEnabled = isChecked
} }
} }
@ -113,7 +107,7 @@ class ListConfigBottomSheet :
when (parent.id) { when (parent.id) {
R.id.spinner_order -> { R.id.spinner_order -> {
viewModel.setSortOrder(position) viewModel.setSortOrder(position)
viewBinding?.switchGrouping?.isEnabled = settings.historySortOrder.isGroupingSupported() viewBinding?.switchGrouping?.isEnabled = viewModel.isGroupingAvailable
} }
} }
} }

@ -18,4 +18,7 @@ sealed interface ListConfigSection : Parcelable {
@Parcelize @Parcelize
data object Suggestions : ListConfigSection data object Suggestions : ListConfigSection
@Parcelize
data object Updated : ListConfigSection
} }

@ -26,16 +26,18 @@ class ListConfigViewModel @Inject constructor(
var listMode: ListMode var listMode: ListMode
get() = when (section) { get() = when (section) {
is ListConfigSection.Favorites -> settings.favoritesListMode is ListConfigSection.Favorites -> settings.favoritesListMode
ListConfigSection.General -> settings.listMode
ListConfigSection.History -> settings.historyListMode ListConfigSection.History -> settings.historyListMode
ListConfigSection.Suggestions -> settings.suggestionsListMode ListConfigSection.Suggestions -> settings.suggestionsListMode
ListConfigSection.General,
ListConfigSection.Updated -> settings.listMode
} }
set(value) { set(value) {
when (section) { when (section) {
is ListConfigSection.Favorites -> settings.favoritesListMode = value is ListConfigSection.Favorites -> settings.favoritesListMode = value
ListConfigSection.General -> settings.listMode = value
ListConfigSection.History -> settings.historyListMode = value ListConfigSection.History -> settings.historyListMode = value
ListConfigSection.Suggestions -> settings.suggestionsListMode = value ListConfigSection.Suggestions -> settings.suggestionsListMode = value
ListConfigSection.Updated,
ListConfigSection.General -> settings.listMode = value
} }
} }
@ -45,19 +47,40 @@ class ListConfigViewModel @Inject constructor(
settings.gridSize = value settings.gridSize = value
} }
val isGroupingSupported: Boolean
get() = section == ListConfigSection.History || section == ListConfigSection.Updated
val isGroupingAvailable: Boolean val isGroupingAvailable: Boolean
get() = section == ListConfigSection.History get() = when (section) {
ListConfigSection.History -> settings.historySortOrder.isGroupingSupported()
ListConfigSection.Updated -> true
else -> false
}
var isGroupingEnabled: Boolean
get() = when (section) {
ListConfigSection.History -> settings.isHistoryGroupingEnabled
ListConfigSection.Updated -> settings.isUpdatedGroupingEnabled
else -> false
}
set(value) = when (section) {
ListConfigSection.History -> settings.isHistoryGroupingEnabled = value
ListConfigSection.Updated -> settings.isUpdatedGroupingEnabled = value
else -> Unit
}
fun getSortOrders(): List<ListSortOrder>? = when (section) { fun getSortOrders(): List<ListSortOrder>? = when (section) {
is ListConfigSection.Favorites -> ListSortOrder.FAVORITES is ListConfigSection.Favorites -> ListSortOrder.FAVORITES
ListConfigSection.General -> null ListConfigSection.General -> null
ListConfigSection.History -> ListSortOrder.HISTORY ListConfigSection.History -> ListSortOrder.HISTORY
ListConfigSection.Suggestions -> ListSortOrder.SUGGESTIONS ListConfigSection.Suggestions -> ListSortOrder.SUGGESTIONS
ListConfigSection.Updated -> null
}?.sortedByOrdinal() }?.sortedByOrdinal()
fun getSelectedSortOrder(): ListSortOrder? = when (section) { fun getSelectedSortOrder(): ListSortOrder? = when (section) {
is ListConfigSection.Favorites -> getCategorySortOrder(section.categoryId) is ListConfigSection.Favorites -> getCategorySortOrder(section.categoryId)
ListConfigSection.General -> null ListConfigSection.General -> null
ListConfigSection.Updated -> null
ListConfigSection.History -> settings.historySortOrder ListConfigSection.History -> settings.historySortOrder
ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE // TODO ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE // TODO
} }
@ -77,6 +100,7 @@ class ListConfigViewModel @Inject constructor(
ListConfigSection.History -> settings.historySortOrder = value ListConfigSection.History -> settings.historySortOrder = value
ListConfigSection.Suggestions -> Unit ListConfigSection.Suggestions -> Unit
ListConfigSection.Updated -> Unit
} }
} }

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.tracker.data
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
class MangaWithTrack(
@Embedded val track: TrackEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id",
)
val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class),
)
val tags: List<TagEntity>,
)

@ -6,7 +6,6 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao @Dao
abstract class TracksDao { abstract class TracksDao {
@ -47,12 +46,12 @@ abstract class TracksDao {
abstract fun observeNewChapters(mangaId: Long): Flow<Int?> abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
@Transaction @Transaction
@Query("SELECT manga.* FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY last_chapter_date DESC") @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC")
abstract fun observeUpdatedManga(): Flow<List<MangaWithTags>> abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>>
@Transaction @Transaction
@Query("SELECT manga.* FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY last_chapter_date DESC LIMIT :limit") @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC LIMIT :limit")
abstract fun observeUpdatedManga(limit: Int): Flow<List<MangaWithTags>> abstract fun observeUpdatedManga(limit: Int): Flow<List<MangaWithTrack>>
@Query("DELETE FROM tracks") @Query("DELETE FROM tracks")
abstract suspend fun clear() abstract suspend fun clear()

@ -12,6 +12,7 @@ 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.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
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.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.core.util.ext.ifZero
@ -59,13 +60,20 @@ class TrackingRepository @Inject constructor(
.onStart { gcIfNotCalled() } .onStart { gcIfNotCalled() }
} }
fun observeUpdatedManga(limit: Int = 0): Flow<List<Manga>> { fun observeUpdatedManga(limit: Int = 0): Flow<List<MangaTracking>> {
return if (limit == 0) { return if (limit == 0) {
db.getTracksDao().observeUpdatedManga() db.getTracksDao().observeUpdatedManga()
} else { } else {
db.getTracksDao().observeUpdatedManga(limit) db.getTracksDao().observeUpdatedManga(limit)
}.mapItems { it.toManga() } }.mapItems {
.distinctUntilChanged() MangaTracking(
manga = it.manga.toManga(it.tags.toMangaTags()),
lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
newChapters = it.track.newChapters,
)
}.distinctUntilChanged()
.onStart { gcIfNotCalled() } .onStart { gcIfNotCalled() }
} }
@ -79,6 +87,8 @@ class TrackingRepository @Inject constructor(
manga = it.manga.toManga(emptySet()), manga = it.manga.toManga(emptySet()),
lastChapterId = it.track.lastChapterId, lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(), lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
newChapters = it.track.newChapters,
) )
} }
} }
@ -90,6 +100,8 @@ class TrackingRepository @Inject constructor(
manga = manga, manga = manga,
lastChapterId = track?.lastChapterId ?: NO_ID, lastChapterId = track?.lastChapterId ?: NO_ID,
lastCheck = track?.lastCheckTime?.toInstantOrNull(), lastCheck = track?.lastCheckTime?.toInstantOrNull(),
lastChapterDate = track?.lastChapterDate?.toInstantOrNull(),
newChapters = track?.newChapters ?: 0,
) )
} }

@ -7,7 +7,10 @@ data class MangaTracking(
val manga: Manga, val manga: Manga,
val lastChapterId: Long, val lastChapterId: Long,
val lastCheck: Instant?, val lastCheck: Instant?,
val lastChapterDate: Instant?,
val newChapters: Int,
) { ) {
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return lastChapterId == 0L return lastChapterId == 0L
} }

@ -11,11 +11,15 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.drop
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.widgets.TipView import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
@ -60,20 +64,15 @@ class FeedFragment :
setHasFixedSize(true) setHasFixedSize(true)
addOnScrollListener(PaginationScrollListener(4, this@FeedFragment)) addOnScrollListener(PaginationScrollListener(4, this@FeedFragment))
addItemDecoration(TypedListSpacingDecoration(context, true)) addItemDecoration(TypedListSpacingDecoration(context, true))
RecyclerScrollKeeper(this).attach()
} }
binding.swipeRefreshLayout.setOnRefreshListener(this) binding.swipeRefreshLayout.setOnRefreshListener(this)
addMenuProvider( addMenuProvider(FeedMenuProvider(binding.recyclerView, viewModel))
FeedMenuProvider(
binding.recyclerView,
viewModel,
),
)
viewModel.isHeaderEnabled.drop(1).observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
viewModel.content.observe(viewLifecycleOwner, this::onListChanged) viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { onFeedCleared() }
onFeedCleared()
}
viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged) viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
} }

@ -22,12 +22,22 @@ class FeedMenuProvider(
menuInflater.inflate(R.menu.opt_feed, menu) menuInflater.inflate(R.menu.opt_feed, menu)
} }
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_show_updated)?.isChecked = viewModel.isHeaderEnabled.value
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_update -> { R.id.action_update -> {
viewModel.update() viewModel.update()
true true
} }
R.id.action_show_updated -> {
viewModel.setHeaderEnabled(!menuItem.isChecked)
true
}
R.id.action_clear_feed -> { R.id.action_clear_feed -> {
CheckBoxAlertDialog.Builder(context) CheckBoxAlertDialog.Builder(context)
.setTitle(R.string.clear_updates_feed) .setTitle(R.string.clear_updates_feed)

@ -6,22 +6,25 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
@ -34,6 +37,7 @@ private const val PAGE_SIZE = 20
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor( class FeedViewModel @Inject constructor(
private val settings: AppSettings,
private val repository: TrackingRepository, private val repository: TrackingRepository,
private val scheduler: TrackWorker.Scheduler, private val scheduler: TrackWorker.Scheduler,
private val listExtraProvider: ListExtraProvider, private val listExtraProvider: ListExtraProvider,
@ -45,6 +49,12 @@ class FeedViewModel @Inject constructor(
val isRunning = scheduler.observeIsRunning() val isRunning = scheduler.observeIsRunning()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isHeaderEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_FEED_HEADER,
valueProducer = { isFeedHeaderVisible },
)
val onFeedCleared = MutableEventFlow<Unit>() val onFeedCleared = MutableEventFlow<Unit>()
val content = combine( val content = combine(
observeHeader(), observeHeader(),
@ -94,6 +104,10 @@ class FeedViewModel @Inject constructor(
scheduler.startNow() scheduler.startNow()
} }
fun setHeaderEnabled(value: Boolean) {
settings.isFeedHeaderVisible = value
}
private fun List<TrackingLogItem>.mapListTo(destination: MutableList<ListModel>) { private fun List<TrackingLogItem>.mapListTo(destination: MutableList<ListModel>) {
var prevDate: DateTimeAgo? = null var prevDate: DateTimeAgo? = null
for (item in this) { for (item in this) {
@ -106,11 +120,19 @@ class FeedViewModel @Inject constructor(
} }
} }
private fun observeHeader() = repository.observeUpdatedManga(10).map { mangaList -> private fun observeHeader() = isHeaderEnabled.flatMapLatest { hasHeader ->
if (hasHeader) {
repository.observeUpdatedManga(10).map { mangaList ->
if (mangaList.isEmpty()) { if (mangaList.isEmpty()) {
null null
} else { } else {
UpdatedMangaHeader(mangaList.toUi(ListMode.GRID, listExtraProvider)) UpdatedMangaHeader(
mangaList.map { it.manga.toGridModel(listExtraProvider) },
)
}
}
} else {
flowOf(null)
} }
} }
} }

@ -11,15 +11,24 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
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.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
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.tracker.domain.model.MangaTracking
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -32,8 +41,9 @@ class UpdatesViewModel @Inject constructor(
override val content = combine( override val content = combine(
repository.observeUpdatedManga(), repository.observeUpdatedManga(),
settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled },
listMode, listMode,
) { mangaList, mode -> ) { mangaList, grouping, mode ->
when { when {
mangaList.isEmpty() -> listOf( mangaList.isEmpty() -> listOf(
EmptyState( EmptyState(
@ -44,7 +54,7 @@ class UpdatesViewModel @Inject constructor(
), ),
) )
else -> mangaList.toUi(mode, extraProvider) else -> mangaList.toUi(mode, grouping)
} }
}.onStart { }.onStart {
loadingCounter.increment() loadingCounter.increment()
@ -69,4 +79,26 @@ class UpdatesViewModel @Inject constructor(
repository.clearUpdates(ids) repository.clearUpdates(ids)
} }
} }
private suspend fun List<MangaTracking>.toUi(mode: ListMode, grouped: Boolean): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (size * 1.4).toInt() else size)
var prevHeader: DateTimeAgo? = null
for (item in this) {
if (grouped) {
val header = item.lastChapterDate?.let { calculateTimeAgo(it) }
if (header != prevHeader) {
if (header != null) {
result += ListHeader(header)
}
prevHeader = header
}
}
result += when (mode) {
ListMode.LIST -> item.manga.toListModel(extraProvider)
ListMode.DETAILED_LIST -> item.manga.toListDetailedModel(extraProvider)
ListMode.GRID -> item.manga.toGridModel(extraProvider)
}
}
return result
}
} }

@ -15,6 +15,13 @@
android:title="@string/update" android:title="@string/update"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_show_updated"
android:checkable="true"
android:orderInCategory="50"
android:title="@string/show_updated"
app:showAsAction="never" />
<item <item
android:id="@+id/action_clear_feed" android:id="@+id/action_clear_feed"
android:orderInCategory="50" android:orderInCategory="50"

@ -652,4 +652,5 @@
<string name="fix">Fix</string> <string name="fix">Fix</string>
<string name="missing_storage_permission">There is no permission to access manga on external storage</string> <string name="missing_storage_permission">There is no permission to access manga on external storage</string>
<string name="last_used">Last used</string> <string name="last_used">Last used</string>
<string name="show_updated">Show updated</string>
</resources> </resources>

Loading…
Cancel
Save