From b73fe0398f23daf324c9f3a02fe7571e27549db5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 18 Aug 2022 12:33:17 +0300 Subject: [PATCH] Highlight new records in feed --- .../kotatsu/core/db/dao/TrackLogsDao.kt | 7 +- .../kotatsu/tracker/data/EntityMapping.kt | 29 ++++++-- .../kotatsu/tracker/data/TracksDao.kt | 4 ++ .../tracker/domain/TrackingRepository.kt | 14 +++- .../tracker/domain/model/TrackingLogItem.kt | 5 +- .../kotatsu/tracker/ui/FeedFragment.kt | 2 +- .../kotatsu/tracker/ui/FeedViewModel.kt | 72 ++++++------------- .../kotatsu/tracker/ui/adapter/FeedItemAD.kt | 9 +-- .../kotatsu/tracker/ui/model/FeedItem.kt | 3 +- .../ui/model/ListModelConversionExt.kt | 3 +- .../koitharu/kotatsu/utils/ext/TextViewExt.kt | 13 ++++ .../koitharu/kotatsu/utils/ext/ThemeExt.kt | 9 +++ .../res/layout/preference_toggle_header.xml | 2 +- 13 files changed, 101 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index ade35613b..ee0da6165 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.db.dao import androidx.room.* +import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogWithManga @@ -8,8 +9,8 @@ import org.koitharu.kotatsu.tracker.data.TrackLogWithManga interface TrackLogsDao { @Transaction - @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset") - suspend fun findAll(offset: Int, limit: Int): List + @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0") + fun observeAll(limit: Int): Flow> @Query("DELETE FROM track_logs") suspend fun clear() @@ -25,4 +26,4 @@ interface TrackLogsDao { @Query("SELECT COUNT(*) FROM track_logs") suspend fun count(): Int -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt index 452f60f8c..9f8099612 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt @@ -5,9 +5,26 @@ import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem -fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem( - id = trackLog.id, - chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }, - manga = manga.toManga(tags.toMangaTags()), - createdAt = Date(trackLog.createdAt) -) \ No newline at end of file +fun TrackLogWithManga.toTrackingLogItem(counters: MutableMap): TrackingLogItem { + val chaptersList = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() } + return TrackingLogItem( + id = trackLog.id, + chapters = chaptersList, + manga = manga.toManga(tags.toMangaTags()), + createdAt = Date(trackLog.createdAt), + isNew = counters.decrement(trackLog.mangaId, chaptersList.size), + ) +} + +private fun MutableMap.decrement(key: Long, count: Int): Boolean { + val counter = get(key) + if (counter == null || counter <= 0) { + return false + } + if (counter < count) { + remove(key) + } else { + put(key, counter - count) + } + return true +} diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 118480f75..0eb482202 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -18,6 +18,10 @@ abstract class TracksDao { @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") abstract suspend fun findNewChapters(mangaId: Long): Int? + @MapInfo(keyColumn = "manga_id", valueColumn = "chapters_new") + @Query("SELECT manga_id, chapters_new FROM tracks") + abstract fun observeNewChaptersMap(): Flow> + @Query("SELECT chapters_new FROM tracks") abstract fun observeNewChapters(): Flow> diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index ce3f9cda9..2fdf8b404 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -5,6 +5,8 @@ import androidx.room.withTransaction import java.util.* import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity @@ -73,9 +75,15 @@ class TrackingRepository @Inject constructor( db.tracksDao.delete(mangaId) } - suspend fun getTrackingLog(offset: Int, limit: Int): List { - return db.trackLogsDao.findAll(offset, limit).map { x -> - x.toTrackingLogItem() + fun observeTrackingLog(limit: Flow): Flow> { + return limit.flatMapLatest { limitValue -> + combine( + db.tracksDao.observeNewChaptersMap(), + db.trackLogsDao.observeAll(limitValue), + ) { counters, entities -> + val countersMap = counters.toMutableMap() + entities.map { x -> x.toTrackingLogItem(countersMap) } + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt index c5021eaf3..286a3a5a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt @@ -7,5 +7,6 @@ data class TrackingLogItem( val id: Long, val manga: Manga, val chapters: List, - val createdAt: Date -) \ No newline at end of file + val createdAt: Date, + val isNew: Boolean, +) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index c55eb11ab..ef2022fa0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -137,7 +137,7 @@ class FeedFragment : } override fun onScrolledToEnd() { - viewModel.loadList(append = true) + viewModel.requestMoreItems() } override fun onItemClick(item: Manga, view: View) { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt index 6974e053e..1511cc217 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt @@ -4,44 +4,39 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import java.util.* import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.daysDiff +private const val PAGE_SIZE = 20 + @HiltViewModel class FeedViewModel @Inject constructor( private val repository: TrackingRepository, ) : BaseViewModel() { - private val logList = MutableStateFlow?>(null) - private val hasNextPage = MutableStateFlow(false) - private var loadingJob: Job? = null + private val limit = MutableStateFlow(PAGE_SIZE) + private val isReady = AtomicBoolean(false) val onFeedCleared = SingleLiveEvent() - val content = combine( - logList.filterNotNull(), - hasNextPage, - ) { list, isHasNextPage -> - buildList(list.size + 2) { + val content = repository.observeTrackingLog(limit) + .map { list -> if (list.isEmpty()) { - add( + listOf( EmptyState( icon = R.drawable.ic_empty_feed, textPrimary = R.string.text_empty_holder_primary, @@ -50,48 +45,26 @@ class FeedViewModel @Inject constructor( ), ) } else { - list.mapListTo(this) - if (isHasNextPage) { - add(LoadingFooter) - } + isReady.set(true) + list.mapList() } - } - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) - - init { - loadList(append = false) - } - - fun loadList(append: Boolean) { - if (loadingJob?.isActive == true) { - return - } - if (append && !hasNextPage.value) { - return - } - loadingJob = launchLoadingJob(Dispatchers.Default) { - val offset = if (append) logList.value?.size ?: 0 else 0 - val list = repository.getTrackingLog(offset, 20) - if (!append) { - logList.value = list - } else if (list.isNotEmpty()) { - logList.value = logList.value?.plus(list) ?: list - } - hasNextPage.value = list.isNotEmpty() - } - } + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) fun clearFeed() { - val lastJob = loadingJob - loadingJob = launchLoadingJob(Dispatchers.Default) { - lastJob?.cancelAndJoin() + launchLoadingJob(Dispatchers.Default) { repository.clearLogs() - logList.value = emptyList() onFeedCleared.postCall(Unit) } } - private fun List.mapListTo(destination: MutableList) { + fun requestMoreItems() { + if (isReady.compareAndSet(true, false)) { + limit.value += PAGE_SIZE + } + } + + private fun List.mapList(): List { + val destination = ArrayList((size * 1.4).toInt()) var prevDate: DateTimeAgo? = null for (item in this) { val date = timeAgo(item.createdAt) @@ -101,6 +74,7 @@ class FeedViewModel @Inject constructor( prevDate = date destination += item.toFeedItem() } + return destination } private fun timeAgo(date: Date): DateTimeAgo { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt index 522dad2f8..96375fe47 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt @@ -11,22 +11,23 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.ui.model.FeedItem import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.isLowRamDevice +import org.koitharu.kotatsu.utils.ext.isBold import org.koitharu.kotatsu.utils.ext.newImageRequest fun feedItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) }, ) { - itemView.setOnClickListener { clickListener.onItemClick(item.manga, it) } bind { + binding.textViewTitle.isBold = item.isNew + binding.textViewSummary.isBold = item.isNew binding.imageViewCover.newImageRequest(item.imageUrl)?.run { placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt index 1cdce1869..4d1db25f9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt @@ -9,4 +9,5 @@ data class FeedItem( val title: String, val manga: Manga, val count: Int, -) : ListModel \ No newline at end of file + val isNew: Boolean, +) : ListModel diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt index b12c4ecff..4e2b91233 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt @@ -8,4 +8,5 @@ fun TrackingLogItem.toFeedItem() = FeedItem( title = manga.title, count = chapters.size, manga = manga, -) \ No newline at end of file + isNew = isNew, +) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt index e053cd5a2..2197c4348 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.utils.ext +import android.graphics.Typeface import android.graphics.drawable.Drawable import android.view.View import android.widget.TextView @@ -48,3 +49,15 @@ fun TextView.setTextAndVisible(@StringRes textResId: Int) { fun TextView.setTextColorAttr(@AttrRes attrResId: Int) { setTextColor(context.getThemeColorStateList(attrResId)) } + +var TextView.isBold: Boolean + get() = typeface.isBold + set(value) { + var style = typeface.style + style = if (value) { + style or Typeface.BOLD + } else { + style and Typeface.BOLD.inv() + } + setTypeface(typeface, style) + } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt index 7896da2e5..6f034b7e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt @@ -2,11 +2,14 @@ package org.koitharu.kotatsu.utils.ext import android.content.Context import android.graphics.Color +import android.widget.TextView import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import androidx.annotation.StyleRes import androidx.core.content.res.use import androidx.core.graphics.ColorUtils +import androidx.core.widget.TextViewCompat fun Context.getThemeDrawable( @AttrRes resId: Int, @@ -43,3 +46,9 @@ fun Context.getThemeColorStateList( ) = obtainStyledAttributes(intArrayOf(resId)).use { it.getColorStateList(0) } + +fun TextView.setThemeTextAppearance(@AttrRes resId: Int, @StyleRes fallback: Int) { + context.obtainStyledAttributes(intArrayOf(resId)).use { + TextViewCompat.setTextAppearance(this, it.getResourceId(0, fallback)) + } +} diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index 076467e11..e897803dd 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -9,7 +9,7 @@ android:layout_marginHorizontal="16dp" android:layout_marginVertical="8dp" android:paddingBottom="8dp" - app:cardBackgroundColor="?colorOnPrimary" + app:cardBackgroundColor="?colorPrimaryContainer" app:cardCornerRadius="24dp">