diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e662f0327..3faa30c69 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,6 +68,9 @@ + diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt index 5a86ecdc4..5b2789d6f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt @@ -10,10 +10,10 @@ import com.google.android.material.navigation.NavigationBarView import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.explore.ui.ExploreFragment -import org.koitharu.kotatsu.shelf.ui.ShelfFragment import org.koitharu.kotatsu.settings.tools.ToolsFragment -import org.koitharu.kotatsu.tracker.ui.FeedFragment -import java.util.* +import org.koitharu.kotatsu.shelf.ui.ShelfFragment +import org.koitharu.kotatsu.tracker.ui.feed.FeedFragment +import java.util.LinkedList private const val TAG_PRIMARY = "primary" diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index a59b7bfb5..c61f866f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.base.ui.BaseFragment @@ -23,15 +22,17 @@ import org.koitharu.kotatsu.databinding.FragmentShelfBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.favourites.ui.FavouritesActivity import org.koitharu.kotatsu.history.ui.HistoryActivity -import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter -import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener -import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter +import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener +import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel +import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import javax.inject.Inject @AndroidEntryPoint class ShelfFragment : @@ -102,6 +103,7 @@ class ShelfFragment : val intent = when (section) { is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context) is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category) + is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context) } startActivity(intent) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt index f343ad825..6f167897e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt @@ -11,10 +11,10 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet -import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.flattenTo +import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations @@ -41,8 +41,9 @@ class ShelfSelectionCallback( mode: ActionMode, menu: Menu, ): Boolean { - menu.findItem(R.id.action_remove).isVisible = - controller.peekCheckedIds().count { (_, v) -> v.isNotEmpty() } == 1 + val checkedIds = controller.peekCheckedIds() + menu.findItem(R.id.action_remove).isVisible = checkedIds.none { (key, _) -> key is ShelfSectionModel.Updated } + && checkedIds.count { (_, v) -> v.isNotEmpty() } == 1 return super.onPrepareActionMode(controller, mode, menu) } @@ -57,25 +58,30 @@ class ShelfSelectionCallback( mode.finish() true } + R.id.action_favourite -> { FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller)) mode.finish() true } + R.id.action_save -> { DownloadService.confirmAndStart(context, collectSelectedItems(controller)) mode.finish() true } + R.id.action_remove -> { val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false when (group) { is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids) is ShelfSectionModel.History -> viewModel.removeFromHistory(ids) + is ShelfSectionModel.Updated -> return false } mode.finish() true } + else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 99164a2ab..3708d089b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -4,8 +4,6 @@ import androidx.collection.ArraySet import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.* -import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -15,22 +13,25 @@ import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import org.koitharu.kotatsu.shelf.domain.ShelfRepository -import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.list.domain.ListExtraProvider -import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.list.ui.model.toGridModel +import org.koitharu.kotatsu.list.ui.model.toUi +import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.shelf.domain.ShelfRepository +import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.daysDiff - -private const val HISTORY_MAX_SEGMENTS = 2 +import javax.inject.Inject @HiltViewModel class ShelfViewModel @Inject constructor( @@ -38,6 +39,7 @@ class ShelfViewModel @Inject constructor( private val historyRepository: HistoryRepository, private val favouritesRepository: FavouritesRepository, private val trackingRepository: TrackingRepository, + private val localMangaRepository: LocalMangaRepository, private val settings: AppSettings, ) : BaseViewModel(), ListExtraProvider { @@ -46,8 +48,9 @@ class ShelfViewModel @Inject constructor( val content: LiveData> = combine( historyRepository.observeAllWithHistory(), repository.observeFavourites(), - ) { history, favourites -> - mapList(history, favourites) + trackingRepository.observeUpdatedManga(), + ) { history, favourites, updated -> + mapList(history, favourites, updated) }.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) @@ -119,11 +122,15 @@ class ShelfViewModel @Inject constructor( private suspend fun mapList( history: List, favourites: Map>, + updated: Map, ): List { - val result = ArrayList(favourites.keys.size + 1) + val result = ArrayList(favourites.keys.size + 2) if (history.isNotEmpty()) { mapHistory(result, history) } + if (updated.isNotEmpty()) { + mapUpdated(result, updated) + } if (favourites.isNotEmpty()) { mapFavourites(result, favourites) } @@ -135,7 +142,6 @@ class ShelfViewModel @Inject constructor( actionStringRes = 0, ) } - result.trimToSize() return result } @@ -144,23 +150,28 @@ class ShelfViewModel @Inject constructor( list: List, ) { val showPercent = settings.isReadingIndicatorsEnabled - val groups = list.groupByTo(LinkedHashMap()) { timeAgo(it.history.updatedAt) } - while (groups.size > HISTORY_MAX_SEGMENTS) { - val lastKey = groups.keys.last() - val subList = groups.remove(lastKey) ?: continue - groups[groups.keys.last()]?.addAll(subList) - } - for ((timeAgo, subList) in groups) { - destination += ShelfSectionModel.History( - items = subList.map { (manga, history) -> - val counter = trackingRepository.getNewChaptersCount(manga.id) - val percent = if (showPercent) history.percent else PROGRESS_NONE - manga.toGridModel(counter, percent) - }, - timeAgo = timeAgo, - showAllButtonText = R.string.show_all, - ) - } + destination += ShelfSectionModel.History( + items = list.map { (manga, history) -> + val counter = trackingRepository.getNewChaptersCount(manga.id) + val percent = if (showPercent) history.percent else PROGRESS_NONE + manga.toGridModel(counter, percent) + }, + showAllButtonText = R.string.show_all, + ) + } + + private suspend fun mapUpdated( + destination: MutableList, + updated: Map, + ) { + val showPercent = settings.isReadingIndicatorsEnabled + destination += ShelfSectionModel.Updated( + items = updated.map { (manga, counter) -> + val percent = if (showPercent) getProgress(manga.id) else PROGRESS_NONE + manga.toGridModel(counter, percent) + }, + showAllButtonText = R.string.show_all, + ) } private suspend fun mapFavourites( @@ -177,14 +188,4 @@ class ShelfViewModel @Inject constructor( } } } - - private fun timeAgo(date: Date): DateTimeAgo { - val diffDays = -date.daysDiff(System.currentTimeMillis()) - return when { - diffDays < 1 -> DateTimeAgo.Today - diffDays == 1 -> DateTimeAgo.Yesterday - diffDays <= 3 -> DateTimeAgo.DaysAgo(diffDays) - else -> DateTimeAgo.LongAgo - } - } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt index e9df8971c..a72bf0b47 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt @@ -4,30 +4,29 @@ import android.content.res.Resources import androidx.annotation.StringRes import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel -sealed class ShelfSectionModel( - val items: List, - @StringRes val showAllButtonText: Int, -) : ListModel { +sealed interface ShelfSectionModel : ListModel { - abstract val key: Any - abstract fun getTitle(resources: Resources): CharSequence + val items: List + + @get:StringRes + val showAllButtonText: Int + + val key: String + fun getTitle(resources: Resources): CharSequence + + override fun toString(): String class History( - items: List, - val timeAgo: DateTimeAgo?, - showAllButtonText: Int, - ) : ShelfSectionModel(items, showAllButtonText) { + override val items: List, + override val showAllButtonText: Int, + ) : ShelfSectionModel { - override val key: Any - get() = timeAgo?.javaClass ?: this::class.java + override val key = "history" - override fun getTitle(resources: Resources): CharSequence { - return timeAgo?.format(resources) ?: resources.getString(R.string.history) - } + override fun getTitle(resources: Resources) = resources.getString(R.string.history) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -35,7 +34,6 @@ sealed class ShelfSectionModel( other as History - if (timeAgo != other.timeAgo) return false if (showAllButtonText != other.showAllButtonText) return false if (items != other.items) return false @@ -44,28 +42,22 @@ sealed class ShelfSectionModel( override fun hashCode(): Int { var result = items.hashCode() - result = 31 * result + (timeAgo?.hashCode() ?: 0) result = 31 * result + showAllButtonText.hashCode() return result } - override fun toString(): String { - return "hist_$timeAgo" - } + override fun toString(): String = key } class Favourites( - items: List, + override val items: List, val category: FavouriteCategory, - showAllButtonText: Int, - ) : ShelfSectionModel(items, showAllButtonText) { + override val showAllButtonText: Int, + ) : ShelfSectionModel { - override val key: Any - get() = category.id + override val key = "fav_${category.id}" - override fun getTitle(resources: Resources): CharSequence { - return category.title - } + override fun getTitle(resources: Resources) = category.title override fun equals(other: Any?): Boolean { if (this === other) return true @@ -87,8 +79,36 @@ sealed class ShelfSectionModel( return result } - override fun toString(): String { - return "fav_${category.id}" + override fun toString(): String = key + } + + class Updated( + override val items: List, + override val showAllButtonText: Int, + ) : ShelfSectionModel { + + override val key = "upd" + + override fun getTitle(resources: Resources) = resources.getString(R.string.updated) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Updated + + if (items != other.items) return false + if (showAllButtonText != other.showAllButtonText) return false + + return true + } + + override fun hashCode(): Int { + var result = items.hashCode() + result = 31 * result + showAllButtonText + return result } + + override fun toString(): String = key } } 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 0eb482202..d248de40e 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 @@ -1,7 +1,14 @@ package org.koitharu.kotatsu.tracker.data -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapInfo +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.entity.MangaWithTags @Dao abstract class TracksDao { @@ -28,6 +35,11 @@ abstract class TracksDao { @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") abstract fun observeNewChapters(mangaId: Long): Flow + @Transaction + @MapInfo(valueColumn = "chapters_new") + @Query("SELECT manga.*, chapters_new FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY chapters_new DESC") + abstract fun observeUpdatedManga(): Flow> + @Query("DELETE FROM tracks") abstract suspend fun clear() 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 2fdf8b404..a9aac2ce1 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 @@ -2,10 +2,9 @@ package org.koitharu.kotatsu.tracker.domain import androidx.annotation.VisibleForTesting 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.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase @@ -22,6 +21,8 @@ import org.koitharu.kotatsu.tracker.data.toTrackingLogItem import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem +import java.util.Date +import javax.inject.Inject private const val NO_ID = 0L @@ -41,6 +42,12 @@ class TrackingRepository @Inject constructor( return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } } } + fun observeUpdatedManga(): Flow> { + return db.tracksDao.observeUpdatedManga() + .map { x -> x.mapKeys { it.key.toManga() } } + .distinctUntilChanged() + } + suspend fun getTracks(mangaList: Collection): List { val ids = mangaList.mapToSet { it.id } val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index ef2022fa0..9bdff5c5a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.tracker.ui +package org.koitharu.kotatsu.tracker.ui.feed import android.os.Bundle import android.view.LayoutInflater @@ -10,7 +10,6 @@ import androidx.fragment.app.viewModels import coil.ImageLoader import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener @@ -23,11 +22,12 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter +import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getThemeColor +import javax.inject.Inject @AndroidEntryPoint class FeedFragment : diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt index 0b83034f3..e20d67933 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.tracker.ui +package org.koitharu.kotatsu.tracker.ui.feed import android.content.Context import android.view.Menu @@ -37,6 +37,7 @@ class FeedMenuProvider( snackbar.show() true } + R.id.action_clear_feed -> { MaterialAlertDialogBuilder(context) .setTitle(R.string.clear_updates_feed) @@ -47,11 +48,13 @@ class FeedMenuProvider( }.show() true } + R.id.action_settings -> { val intent = SettingsActivity.newTrackerSettingsIntent(context) context.startActivity(intent) true } + else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt index 1511cc217..1401873e8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt @@ -1,11 +1,7 @@ -package org.koitharu.kotatsu.tracker.ui +package org.koitharu.kotatsu.tracker.ui.feed 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.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -17,10 +13,14 @@ import org.koitharu.kotatsu.list.ui.model.ListModel 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.tracker.ui.feed.model.toFeedItem import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.daysDiff +import java.util.Date +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject private const val PAGE_SIZE = 20 diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedAdapter.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt index 4ed934498..3737e3242 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt @@ -1,14 +1,20 @@ -package org.koitharu.kotatsu.tracker.ui.adapter +package org.koitharu.kotatsu.tracker.ui.feed.adapter import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import kotlin.jvm.internal.Intrinsics import org.koitharu.kotatsu.core.ui.DateTimeAgo -import org.koitharu.kotatsu.list.ui.adapter.* +import org.koitharu.kotatsu.list.ui.adapter.MangaListListener +import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD +import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD +import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD +import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.tracker.ui.model.FeedItem +import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem +import kotlin.jvm.internal.Intrinsics class FeedAdapter( coil: ImageLoader, @@ -33,9 +39,11 @@ class FeedAdapter( oldItem is FeedItem && newItem is FeedItem -> { oldItem.id == newItem.id } + oldItem is DateTimeAgo && newItem is DateTimeAgo -> { oldItem == newItem } + else -> oldItem.javaClass == newItem.javaClass } 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/feed/adapter/FeedItemAD.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt index 96375fe47..b98b25e17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.tracker.ui.adapter +package org.koitharu.kotatsu.tracker.ui.feed.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader @@ -8,7 +8,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener 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.model.FeedItem +import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.isBold 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/feed/model/FeedItem.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt index 4d1db25f9..90e97624d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/FeedItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.tracker.ui.model +package org.koitharu.kotatsu.tracker.ui.feed.model import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga 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/feed/model/ListModelConversionExt.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt rename to app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt index 4e2b91233..f99997c82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.tracker.ui.model +package org.koitharu.kotatsu.tracker.ui.feed.model import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt new file mode 100644 index 000000000..56f213dd1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.tracker.ui.updates + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import androidx.fragment.app.commit +import com.google.android.material.appbar.AppBarLayout +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityContainerBinding +import org.koitharu.kotatsu.main.ui.owners.AppBarOwner + +@AndroidEntryPoint +class UpdatesActivity : + BaseActivity(), + AppBarOwner { + + override val appBar: AppBarLayout + get() = binding.appbar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityContainerBinding.inflate(layoutInflater)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val fm = supportFragmentManager + if (fm.findFragmentById(R.id.container) == null) { + fm.commit { + setReorderingAllowed(true) + val fragment = UpdatesFragment.newInstance() + replace(R.id.container, fragment) + } + } + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.root.updatePadding( + left = insets.left, + right = insets.right, + ) + } + + companion object { + + fun newIntent(context: Context) = Intent(context, UpdatesActivity::class.java) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt new file mode 100644 index 000000000..1b860a256 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.tracker.ui.updates + +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.list.ui.MangaListFragment + +@AndroidEntryPoint +class UpdatesFragment : MangaListFragment() { + + override val viewModel by viewModels() + override val isSwipeRefreshEnabled = false + + override fun onScrolledToEnd() = Unit + + companion object { + + fun newInstance() = UpdatesFragment() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt new file mode 100644 index 000000000..586ef35c2 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.tracker.ui.updates + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onStart +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorState +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.parsers.model.Manga +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.utils.asFlowLiveData +import org.koitharu.kotatsu.utils.ext.onFirst +import javax.inject.Inject + +@HiltViewModel +class UpdatesViewModel @Inject constructor( + private val repository: TrackingRepository, + private val settings: AppSettings, + private val historyRepository: HistoryRepository, +) : MangaListViewModel(settings) { + + override val content = combine( + repository.observeUpdatedManga(), + createListModeFlow(), + ) { mangaMap, mode -> + when { + mangaMap.isEmpty() -> listOf( + EmptyState( + icon = R.drawable.ic_empty_history, + textPrimary = R.string.text_history_holder_primary, + textSecondary = R.string.text_history_holder_secondary, + actionStringRes = 0, + ), + ) + + else -> mapList(mangaMap, mode) + } + }.onStart { + loadingCounter.increment() + }.onFirst { + loadingCounter.decrement() + }.catch { + emit(listOf(it.toErrorState(canRetry = false))) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + + override fun onRefresh() = Unit + + override fun onRetry() = Unit + + private suspend fun mapList( + mangaMap: Map, + mode: ListMode, + ): List { + val showPercent = settings.isReadingIndicatorsEnabled + return mangaMap.map { (manga, counter) -> + val percent = if (showPercent) historyRepository.getProgress(manga.id) else PROGRESS_NONE + when (mode) { + ListMode.LIST -> manga.toListModel(counter, percent) + ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent) + ListMode.GRID -> manga.toGridModel(counter, percent) + } + } + } +}