Add unread field to feed items
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -56,6 +56,7 @@ class FavouritesListViewModel @Inject constructor(
|
||||
}
|
||||
} else {
|
||||
repository.observeCategory(categoryId)
|
||||
.withErrorHandling()
|
||||
.map { it?.order }
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user