Add unread field to feed items

This commit is contained in:
Koitharu
2024-04-16 10:03:01 +03:00
parent f685ed6932
commit 846c346a86
20 changed files with 85 additions and 41 deletions

View File

@@ -16,9 +16,15 @@ interface TrackLogsDao {
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
fun observeUnreadCount(): Flow<Int>
@Query("DELETE FROM track_logs")
suspend fun clear()
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
suspend fun markAsRead(id: Long)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long

View File

@@ -12,5 +12,7 @@ class Migration19To20 : Migration(19, 20) {
db.execSQL("CREATE TABLE tracks (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("INSERT INTO tracks SELECT manga_id, last_chapter_id, chapters_new, last_check AS last_check_time, 0 AS last_chapter_date, 0 AS last_result FROM tracks_bk")
db.execSQL("DROP TABLE tracks_bk")
db.execSQL("ALTER TABLE track_logs ADD COLUMN `unread` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.util
import kotlinx.coroutines.CoroutineExceptionHandler
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.report
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
class AcraCoroutineErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) {
exception.printStackTraceDebug()
exception.report()
}
}

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleDestroyedException
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
@@ -10,17 +9,20 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.lifecycle.RetainedLifecycle
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope
val processLifecycleScope: CoroutineScope
get() = ProcessLifecycleOwner.get().lifecycleScope + AcraCoroutineErrorHandler()
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
inline get() = RetainedLifecycleCoroutineScope(this)

View File

@@ -118,7 +118,7 @@ class ExploreViewModel @Inject constructor(
sourcesRepository.observeNewSources(),
) { content, suggestions, grid, randomLoading, newSources ->
buildList(content, suggestions, grid, randomLoading, newSources)
}
}.withErrorHandling()
private fun buildList(
sources: List<MangaSource>,

View File

@@ -40,7 +40,8 @@ class FavouritesCategoriesViewModel @Inject constructor(
settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) { isAllFavouritesVisible },
) { cats, all, showAll ->
cats.toUiList(all, showAll)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
}.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun deleteCategories(ids: Set<Long>) {
launchJob(Dispatchers.Default) {

View File

@@ -56,6 +56,7 @@ class FavouritesListViewModel @Inject constructor(
}
} else {
repository.observeCategory(categoryId)
.withErrorHandling()
.map { it?.order }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)

View File

@@ -277,7 +277,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
private fun onFeedCounterChanged(counter: Int) {
navigationDelegate.setCounter(NavItem.FEED, counter)
navigationDelegate.setCounter(NavItem.UPDATED, counter)
}
private fun onIncognitoModeChanged(isIncognito: Boolean) {

View File

@@ -32,15 +32,18 @@ class MainViewModel @Inject constructor(
val onOpenReader = MutableEventFlow<Manga>()
val onFirstStart = MutableEventFlow<Unit>()
val isResumeEnabled = readingResumeEnabledUseCase().stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false,
)
val isResumeEnabled = readingResumeEnabledUseCase()
.withErrorHandling()
.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false,
)
val appUpdate = appUpdateRepository.observeAvailableUpdate()
val feedCounter = trackingRepository.observeUpdatedMangaCount()
val feedCounter = trackingRepository.observeUnreadUpdatesCount()
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, 0)
init {

View File

@@ -106,7 +106,7 @@ class SearchSuggestionViewModel @Inject constructor(
}.distinctUntilChanged()
.onEach {
suggestion.value = it
}.launchIn(viewModelScope + Dispatchers.Default)
}.withErrorHandling().launchIn(viewModelScope + Dispatchers.Default)
}
private suspend fun buildSearchSuggestion(

View File

@@ -18,5 +18,6 @@ class RootSettingsViewModel @Inject constructor(
val totalSourcesCount = sourcesRepository.allMangaSources.size
val enabledSourcesCount = sourcesRepository.observeEnabledSourcesCount()
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
}

View File

@@ -16,8 +16,10 @@ class SourcesSettingsViewModel @Inject constructor(
) : BaseViewModel() {
val enabledSourcesCount = sourcesRepository.observeEnabledSourcesCount()
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
val availableSourcesCount = sourcesRepository.observeAvailableSourcesCount()
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
}

View File

@@ -5,26 +5,13 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import java.time.Instant
fun TrackLogWithManga.toTrackingLogItem(counters: MutableMap<Long, Int>): TrackingLogItem {
fun TrackLogWithManga.toTrackingLogItem(): TrackingLogItem {
val chaptersList = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }
return TrackingLogItem(
id = trackLog.id,
chapters = chaptersList,
manga = manga.toManga(tags.toMangaTags()),
createdAt = Instant.ofEpochMilli(trackLog.createdAt),
isNew = counters.decrement(trackLog.mangaId, chaptersList.size),
isNew = trackLog.isUnread,
)
}
private fun MutableMap<Long, Int>.decrement(key: Long, count: Int): Boolean = synchronized(this) {
val counter = get(key)
if (counter == null || counter <= 0) {
return false
}
if (counter < count) {
remove(key)
} else {
put(key, counter - count)
}
return true
}

View File

@@ -23,4 +23,5 @@ class TrackLogEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "chapters") val chapters: String,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "unread") val isUnread: Boolean,
)

View File

@@ -4,7 +4,6 @@ import androidx.annotation.VisibleForTesting
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@@ -55,11 +54,16 @@ class TrackingRepository @Inject constructor(
return db.getTracksDao().observeNewChapters(mangaId).map { it ?: 0 }
}
@Deprecated("")
fun observeUpdatedMangaCount(): Flow<Int> {
return db.getTracksDao().observeNewChapters().map { list -> list.count { it > 0 } }
.onStart { gcIfNotCalled() }
}
fun observeUnreadUpdatesCount(): Flow<Int> {
return db.getTrackLogsDao().observeUnreadCount()
}
fun observeUpdatedManga(limit: Int = 0): Flow<List<MangaTracking>> {
return if (limit == 0) {
db.getTracksDao().observeUpdatedManga()
@@ -112,13 +116,8 @@ class TrackingRepository @Inject constructor(
fun observeTrackingLog(limit: Flow<Int>): Flow<List<TrackingLogItem>> {
return limit.flatMapLatest { limitValue ->
combine(
db.getTracksDao().observeNewChaptersMap(),
db.getTrackLogsDao().observeAll(limitValue),
) { counters, entities ->
val countersMap = counters.toMutableMap()
entities.map { x -> x.toTrackingLogItem(countersMap) }
}
db.getTrackLogsDao().observeAll(limitValue)
.mapItems { it.toTrackingLogItem() }
}.onStart {
gcIfNotCalled()
}
@@ -130,6 +129,8 @@ class TrackingRepository @Inject constructor(
suspend fun clearCounters() = db.getTracksDao().clearCounters()
suspend fun markAsRead(trackLogId: Long) = db.getTrackLogsDao().markAsRead(trackLogId)
suspend fun gc() = db.withTransaction {
db.getTracksDao().gc()
db.getTrackLogsDao().run {
@@ -148,6 +149,7 @@ class TrackingRepository @Inject constructor(
mangaId = updates.manga.id,
chapters = updates.newChapters.joinToString("\n") { x -> x.name },
createdAt = System.currentTimeMillis(),
isUnread = true,
)
db.getTrackLogsDao().insert(logEntity)
}

View File

@@ -57,7 +57,10 @@ class FeedFragment :
override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width))
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver)
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v ->
viewModel.onItemClick(item)
onItemClick(item.manga, v)
}
with(binding.recyclerView) {
adapter = feedAdapter
setHasFixedSize(true)

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.tracker.ui.feed
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -27,6 +28,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem
import org.koitharu.kotatsu.tracker.work.TrackWorker
@@ -108,6 +110,12 @@ class FeedViewModel @Inject constructor(
settings.isFeedHeaderVisible = value
}
fun onItemClick(item: FeedItem) {
launchJob(Dispatchers.Default, CoroutineStart.ATOMIC) {
repository.markAsRead(item.id)
}
}
private fun List<TrackingLogItem>.mapListTo(destination: MutableList<ListModel>) {
var prevDate: DateTimeAgo? = null
for (item in this) {

View File

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -15,16 +16,18 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
class FeedAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
sizeResolver: ItemSizeResolver,
feedClickListener: OnListItemClickListener<FeedItem>,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.FEED, feedItemAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.FEED, feedItemAD(coil, lifecycleOwner, feedClickListener))
addDelegate(
ListItemType.MANGA_NESTED_GROUP,
updatedMangaAD(

View File

@@ -12,20 +12,19 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemFeedBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
fun feedItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>,
clickListener: OnListItemClickListener<FeedItem>,
) = adapterDelegateViewBinding<FeedItem, ListModel, ItemFeedBinding>(
{ inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) },
) {
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
clickListener.onItemClick(item, it)
}
bind {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.tracker.ui.feed.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
@@ -11,7 +12,14 @@ data class FeedItem(
val count: Int,
val isNew: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is FeedItem && other.id == id
}
override fun getChangePayload(previousState: ListModel): Any? = when {
previousState !is FeedItem -> null
isNew != previousState.isNew -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
else -> super.getChangePayload(previousState)
}
}