Przeglądaj źródła

Migrate Updates screen to compose (#7534)

* Migrate Updates screen to compose

* Review Changes + Cleanup

Remove more unused stuff and show confirmation dialog when mass deleting chapters

* Review Changes 2 + Rebase
AntsyLich 2 lat temu
rodzic
commit
d8fb6b893f
37 zmienionych plików z 1171 dodań i 895 usunięć
  1. 16 0
      app/src/main/java/eu/kanade/core/util/ListUtils.kt
  2. 26 0
      app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt
  3. 17 0
      app/src/main/java/eu/kanade/data/updates/UpdatesRepositoryImpl.kt
  4. 6 0
      app/src/main/java/eu/kanade/domain/DomainModule.kt
  5. 24 0
      app/src/main/java/eu/kanade/domain/updates/interactor/GetUpdates.kt
  6. 16 0
      app/src/main/java/eu/kanade/domain/updates/model/UpdatesWithRelations.kt
  7. 9 0
      app/src/main/java/eu/kanade/domain/updates/repository/UpdatesRepository.kt
  8. 41 0
      app/src/main/java/eu/kanade/presentation/components/Banners.kt
  9. 7 1
      app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt
  10. 8 8
      app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt
  11. 2 2
      app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt
  12. 2 2
      app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
  13. 2 1
      app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
  14. 0 7
      app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt
  15. 2 3
      app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
  16. 4 26
      app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt
  17. 315 0
      app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
  18. 270 0
      app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt
  19. 2 0
      app/src/main/java/eu/kanade/presentation/util/Constants.kt
  20. 18 0
      app/src/main/java/eu/kanade/presentation/util/LazyListState.kt
  21. 13 0
      app/src/main/java/eu/kanade/presentation/util/NavBarVisibility.kt
  22. 11 2
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  23. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  24. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterDownloadView.kt
  25. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterHolder.kt
  26. 0 53
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/DateSectionItem.kt
  27. 0 33
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/ConfirmDeleteChaptersDialog.kt
  28. 0 29
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesAdapter.kt
  29. 87 366
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt
  30. 0 62
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt
  31. 0 32
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt
  32. 225 137
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt
  33. 0 40
      app/src/main/res/layout/updates_controller.xml
  34. 0 78
      app/src/main/res/layout/updates_item.xml
  35. 0 10
      app/src/main/sqldelight/data/mangas.sq
  36. 20 0
      app/src/main/sqldelight/migrations/18.sqm
  37. 25 0
      app/src/main/sqldelight/view/updatesView.sq

+ 16 - 0
app/src/main/java/eu/kanade/core/util/ListUtils.kt

@@ -0,0 +1,16 @@
+package eu.kanade.core.util
+
+fun <T : R, R : Any> List<T>.insertSeparators(
+    generator: (T?, T?) -> R?,
+): List<R> {
+    if (isEmpty()) return emptyList()
+    val newList = mutableListOf<R>()
+    for (i in -1..lastIndex) {
+        val before = getOrNull(i)
+        before?.let { newList.add(it) }
+        val after = getOrNull(i + 1)
+        val separator = generator.invoke(before, after)
+        separator?.let { newList.add(it) }
+    }
+    return newList
+}

+ 26 - 0
app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt

@@ -0,0 +1,26 @@
+package eu.kanade.data.updates
+
+import eu.kanade.domain.manga.model.MangaCover
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+
+val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Boolean, String?, Long, Long, Long) -> UpdatesWithRelations = {
+        mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
+    UpdatesWithRelations(
+        mangaId = mangaId,
+        mangaTitle = mangaTitle,
+        chapterId = chapterId,
+        chapterName = chapterName,
+        scanlator = scanlator,
+        read = read,
+        bookmark = bookmark,
+        sourceId = sourceId,
+        dateFetch = dateFetch,
+        coverData = MangaCover(
+            mangaId = mangaId,
+            sourceId = sourceId,
+            isMangaFavorite = favorite,
+            url = thumbnailUrl,
+            lastModified = coverLastModified,
+        ),
+    )
+}

+ 17 - 0
app/src/main/java/eu/kanade/data/updates/UpdatesRepositoryImpl.kt

@@ -0,0 +1,17 @@
+package eu.kanade.data.updates
+
+import eu.kanade.data.DatabaseHandler
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+import eu.kanade.domain.updates.repository.UpdatesRepository
+import kotlinx.coroutines.flow.Flow
+
+class UpdatesRepositoryImpl(
+    val databaseHandler: DatabaseHandler,
+) : UpdatesRepository {
+
+    override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> {
+        return databaseHandler.subscribeToList {
+            updatesViewQueries.updates(after, updateWithRelationMapper)
+        }
+    }
+}

+ 6 - 0
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -7,6 +7,7 @@ import eu.kanade.data.manga.MangaRepositoryImpl
 import eu.kanade.data.source.SourceDataRepositoryImpl
 import eu.kanade.data.source.SourceRepositoryImpl
 import eu.kanade.data.track.TrackRepositoryImpl
+import eu.kanade.data.updates.UpdatesRepositoryImpl
 import eu.kanade.domain.category.interactor.CreateCategoryWithName
 import eu.kanade.domain.category.interactor.DeleteCategory
 import eu.kanade.domain.category.interactor.GetCategories
@@ -60,6 +61,8 @@ import eu.kanade.domain.track.interactor.DeleteTrack
 import eu.kanade.domain.track.interactor.GetTracks
 import eu.kanade.domain.track.interactor.InsertTrack
 import eu.kanade.domain.track.repository.TrackRepository
+import eu.kanade.domain.updates.interactor.GetUpdates
+import eu.kanade.domain.updates.repository.UpdatesRepository
 import uy.kohesive.injekt.api.InjektModule
 import uy.kohesive.injekt.api.InjektRegistrar
 import uy.kohesive.injekt.api.addFactory
@@ -119,6 +122,9 @@ class DomainModule : InjektModule {
         addFactory { GetExtensionUpdates(get(), get()) }
         addFactory { GetExtensionLanguages(get(), get()) }
 
+        addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
+        addFactory { GetUpdates(get(), get()) }
+
         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
         addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
         addFactory { GetEnabledSources(get(), get()) }

+ 24 - 0
app/src/main/java/eu/kanade/domain/updates/interactor/GetUpdates.kt

@@ -0,0 +1,24 @@
+package eu.kanade.domain.updates.interactor
+
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+import eu.kanade.domain.updates.repository.UpdatesRepository
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onEach
+import java.util.Calendar
+
+class GetUpdates(
+    private val repository: UpdatesRepository,
+    private val preferences: PreferencesHelper,
+) {
+
+    fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
+
+    fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
+        return repository.subscribeAll(after)
+            .onEach { updates ->
+                // Set unread chapter count for bottom bar badge
+                preferences.unreadUpdatesCount().set(updates.count { it.read.not() })
+            }
+    }
+}

+ 16 - 0
app/src/main/java/eu/kanade/domain/updates/model/UpdatesWithRelations.kt

@@ -0,0 +1,16 @@
+package eu.kanade.domain.updates.model
+
+import eu.kanade.domain.manga.model.MangaCover
+
+data class UpdatesWithRelations(
+    val mangaId: Long,
+    val mangaTitle: String,
+    val chapterId: Long,
+    val chapterName: String,
+    val scanlator: String?,
+    val read: Boolean,
+    val bookmark: Boolean,
+    val sourceId: Long,
+    val dateFetch: Long,
+    val coverData: MangaCover,
+)

+ 9 - 0
app/src/main/java/eu/kanade/domain/updates/repository/UpdatesRepository.kt

@@ -0,0 +1,9 @@
+package eu.kanade.domain.updates.repository
+
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+import kotlinx.coroutines.flow.Flow
+
+interface UpdatesRepository {
+
+    fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>>
+}

+ 41 - 0
app/src/main/java/eu/kanade/presentation/components/Banners.kt

@@ -0,0 +1,41 @@
+package eu.kanade.presentation.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun DownloadedOnlyModeBanner() {
+    Text(
+        text = stringResource(R.string.label_downloaded_only),
+        modifier = Modifier
+            .background(color = MaterialTheme.colorScheme.tertiary)
+            .fillMaxWidth()
+            .padding(4.dp),
+        color = MaterialTheme.colorScheme.onTertiary,
+        textAlign = TextAlign.Center,
+        style = MaterialTheme.typography.labelMedium,
+    )
+}
+
+@Composable
+fun IncognitoModeBanner() {
+    Text(
+        text = stringResource(R.string.pref_incognito_mode),
+        modifier = Modifier
+            .background(color = MaterialTheme.colorScheme.primary)
+            .fillMaxWidth()
+            .padding(4.dp),
+        color = MaterialTheme.colorScheme.onPrimary,
+        textAlign = TextAlign.Center,
+        style = MaterialTheme.typography.labelMedium,
+    )
+}

+ 7 - 1
app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt

@@ -27,11 +27,17 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.dp
-import eu.kanade.presentation.manga.ChapterDownloadAction
 import eu.kanade.presentation.util.secondaryItemAlpha
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 
+enum class ChapterDownloadAction {
+    START,
+    START_NOW,
+    CANCEL,
+    DELETE,
+}
+
 @Composable
 fun ChapterDownloadIndicator(
     modifier: Modifier = Modifier,

+ 8 - 8
app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt → app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.manga.components
+package eu.kanade.presentation.components
 
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.animateFloatAsState
@@ -51,13 +51,13 @@ import kotlinx.coroutines.launch
 fun MangaBottomActionMenu(
     visible: Boolean,
     modifier: Modifier = Modifier,
-    onBookmarkClicked: (() -> Unit)?,
-    onRemoveBookmarkClicked: (() -> Unit)?,
-    onMarkAsReadClicked: (() -> Unit)?,
-    onMarkAsUnreadClicked: (() -> Unit)?,
-    onMarkPreviousAsReadClicked: (() -> Unit)?,
-    onDownloadClicked: (() -> Unit)?,
-    onDeleteClicked: (() -> Unit)?,
+    onBookmarkClicked: (() -> Unit)? = null,
+    onRemoveBookmarkClicked: (() -> Unit)? = null,
+    onMarkAsReadClicked: (() -> Unit)? = null,
+    onMarkAsUnreadClicked: (() -> Unit)? = null,
+    onMarkPreviousAsReadClicked: (() -> Unit)? = null,
+    onDownloadClicked: (() -> Unit)? = null,
+    onDeleteClicked: (() -> Unit)? = null,
 ) {
     AnimatedVisibility(
         visible = visible,

+ 2 - 2
app/src/main/java/eu/kanade/presentation/history/components/HistoryHeader.kt → app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.history.components
+package eu.kanade.presentation.components
 
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.MaterialTheme
@@ -15,7 +15,7 @@ import java.text.DateFormat
 import java.util.Date
 
 @Composable
-fun HistoryHeader(
+fun RelativeDateHeader(
     modifier: Modifier = Modifier,
     date: Date,
     relativeTime: Int,

+ 2 - 2
app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt

@@ -39,8 +39,8 @@ import androidx.paging.compose.items
 import eu.kanade.domain.history.model.HistoryWithRelations
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.LoadingScreen
+import eu.kanade.presentation.components.RelativeDateHeader
 import eu.kanade.presentation.components.ScrollbarLazyColumn
-import eu.kanade.presentation.history.components.HistoryHeader
 import eu.kanade.presentation.history.components.HistoryItem
 import eu.kanade.presentation.history.components.HistoryItemShimmer
 import eu.kanade.presentation.util.plus
@@ -108,7 +108,7 @@ fun HistoryContent(
         items(history) { item ->
             when (item) {
                 is HistoryUiModel.Header -> {
-                    HistoryHeader(
+                    RelativeDateHeader(
                         modifier = Modifier
                             .animateItemPlacement(),
                         date = item.date,

+ 2 - 1
app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt

@@ -52,15 +52,16 @@ import androidx.compose.ui.unit.dp
 import com.google.accompanist.swiperefresh.SwipeRefresh
 import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
 import eu.kanade.domain.chapter.model.Chapter
+import eu.kanade.presentation.components.ChapterDownloadAction
 import eu.kanade.presentation.components.ExtendedFloatingActionButton
 import eu.kanade.presentation.components.LazyColumn
+import eu.kanade.presentation.components.MangaBottomActionMenu
 import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.components.SwipeRefreshIndicator
 import eu.kanade.presentation.components.VerticalFastScroller
 import eu.kanade.presentation.manga.components.ChapterHeader
 import eu.kanade.presentation.manga.components.ExpandableMangaDescription
 import eu.kanade.presentation.manga.components.MangaActionRow
-import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 import eu.kanade.presentation.manga.components.MangaChapterListItem
 import eu.kanade.presentation.manga.components.MangaInfoBox
 import eu.kanade.presentation.manga.components.MangaSmallAppBar

+ 0 - 7
app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt

@@ -9,13 +9,6 @@ enum class DownloadAction {
     ALL_CHAPTERS
 }
 
-enum class ChapterDownloadAction {
-    START,
-    START_NOW,
-    CANCEL,
-    DELETE,
-}
-
 enum class EditCoverAction {
     EDIT,
     DELETE,

+ 2 - 3
app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt

@@ -29,8 +29,9 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import eu.kanade.presentation.components.ChapterDownloadAction
 import eu.kanade.presentation.components.ChapterDownloadIndicator
-import eu.kanade.presentation.manga.ChapterDownloadAction
+import eu.kanade.presentation.util.ReadItemAlpha
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 
@@ -134,5 +135,3 @@ fun MangaChapterListItem(
         }
     }
 }
-
-private const val ReadItemAlpha = .38f

+ 4 - 26
app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt

@@ -1,13 +1,10 @@
 package eu.kanade.presentation.manga.components
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.systemBars
 import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.material.icons.Icons
@@ -21,7 +18,6 @@ import androidx.compose.material.icons.outlined.Share
 import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.SmallTopAppBar
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
@@ -34,10 +30,10 @@ import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
+import eu.kanade.presentation.components.DownloadedOnlyModeBanner
 import eu.kanade.presentation.components.DropdownMenu
+import eu.kanade.presentation.components.IncognitoModeBanner
 import eu.kanade.presentation.manga.DownloadAction
 import eu.kanade.tachiyomi.R
 
@@ -210,28 +206,10 @@ fun MangaSmallAppBar(
         )
 
         if (downloadedOnlyMode) {
-            Text(
-                text = stringResource(R.string.label_downloaded_only),
-                modifier = Modifier
-                    .background(color = MaterialTheme.colorScheme.tertiary)
-                    .fillMaxWidth()
-                    .padding(4.dp),
-                color = MaterialTheme.colorScheme.onTertiary,
-                textAlign = TextAlign.Center,
-                style = MaterialTheme.typography.labelMedium,
-            )
+            DownloadedOnlyModeBanner()
         }
         if (incognitoMode) {
-            Text(
-                text = stringResource(R.string.pref_incognito_mode),
-                modifier = Modifier
-                    .background(color = MaterialTheme.colorScheme.primary)
-                    .fillMaxWidth()
-                    .padding(4.dp),
-                color = MaterialTheme.colorScheme.onPrimary,
-                textAlign = TextAlign.Center,
-                style = MaterialTheme.typography.labelMedium,
-            )
+            IncognitoModeBanner()
         }
     }
 }

+ 315 - 0
app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt

@@ -0,0 +1,315 @@
+package eu.kanade.presentation.updates
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.FlipToBack
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.SelectAll
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+import eu.kanade.presentation.components.ChapterDownloadAction
+import eu.kanade.presentation.components.DownloadedOnlyModeBanner
+import eu.kanade.presentation.components.EmptyScreen
+import eu.kanade.presentation.components.IncognitoModeBanner
+import eu.kanade.presentation.components.MangaBottomActionMenu
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.components.SwipeRefreshIndicator
+import eu.kanade.presentation.components.VerticalFastScroller
+import eu.kanade.presentation.util.NavBarVisibility
+import eu.kanade.presentation.util.isScrollingDown
+import eu.kanade.presentation.util.isScrollingUp
+import eu.kanade.presentation.util.plus
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
+import eu.kanade.tachiyomi.ui.recent.updates.UpdatesState
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.DateFormat
+import java.util.Date
+
+@Composable
+fun UpdateScreen(
+    state: UpdatesState.Success,
+    onClickCover: (UpdatesItem) -> Unit,
+    onClickUpdate: (UpdatesItem) -> Unit,
+    onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
+    onUpdateLibrary: () -> Unit,
+    onBackClicked: () -> Unit,
+    toggleNavBarVisibility: (NavBarVisibility) -> Unit,
+    // For bottom action menu
+    onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
+    onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
+    onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
+    // Miscellaneous
+    preferences: PreferencesHelper = Injekt.get(),
+) {
+    val updatesListState = rememberLazyListState()
+    val insetPaddingValue = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
+
+    val relativeTime: Int = remember { preferences.relativeTime().get() }
+    val dateFormat: DateFormat = remember { preferences.dateFormat() }
+
+    val uiModels = remember(state) {
+        state.uiModels
+    }
+    val itemUiModels = remember(uiModels) {
+        uiModels.filterIsInstance<UpdatesUiModel.Item>()
+    }
+    // To prevent selection from getting removed during an update to a item in list
+    val updateIdList = remember(itemUiModels) {
+        itemUiModels.map { it.item.update.chapterId }
+    }
+    val selected = remember(updateIdList) {
+        emptyList<UpdatesUiModel.Item>().toMutableStateList()
+    }
+    // First and last selected index in list
+    val selectedPositions = remember(uiModels) { arrayOf(-1, -1) }
+
+    when {
+        selected.isEmpty() &&
+            updatesListState.isScrollingUp() -> toggleNavBarVisibility(NavBarVisibility.SHOW)
+        selected.isNotEmpty() ||
+            updatesListState.isScrollingDown() -> toggleNavBarVisibility(NavBarVisibility.HIDE)
+    }
+
+    val internalOnBackPressed = {
+        if (selected.isNotEmpty()) {
+            selected.clear()
+        } else {
+            onBackClicked()
+        }
+    }
+    BackHandler(onBack = internalOnBackPressed)
+
+    Scaffold(
+        modifier = Modifier
+            .padding(insetPaddingValue),
+        topBar = {
+            UpdatesAppBar(
+                selected = selected,
+                incognitoMode = state.isIncognitoMode,
+                downloadedOnlyMode = state.isDownloadedOnlyMode,
+                onUpdateLibrary = onUpdateLibrary,
+                actionModeCounter = selected.size,
+                onSelectAll = {
+                    selected.clear()
+                    selected.addAll(itemUiModels)
+                },
+                onInvertSelection = {
+                    val toSelect = itemUiModels - selected
+                    selected.clear()
+                    selected.addAll(toSelect)
+                },
+            )
+        },
+        bottomBar = {
+            UpdatesBottomBar(
+                selected = selected,
+                onDownloadChapter = onDownloadChapter,
+                onMultiBookmarkClicked = onMultiBookmarkClicked,
+                onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
+                onMultiDeleteClicked = onMultiDeleteClicked,
+            )
+        },
+    ) { contentPadding ->
+        val contentPaddingWithNavBar = contentPadding +
+            WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
+
+        SwipeRefresh(
+            state = rememberSwipeRefreshState(state.showSwipeRefreshIndicator),
+            onRefresh = onUpdateLibrary,
+            indicatorPadding = contentPaddingWithNavBar,
+            indicator = { s, trigger ->
+                SwipeRefreshIndicator(
+                    state = s,
+                    refreshTriggerDistance = trigger,
+                )
+            },
+        ) {
+            if (uiModels.isEmpty()) {
+                EmptyScreen(textResource = R.string.information_no_recent)
+            } else {
+                VerticalFastScroller(
+                    listState = updatesListState,
+                    topContentPadding = contentPaddingWithNavBar.calculateTopPadding(),
+                    endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current),
+                ) {
+                    LazyColumn(
+                        modifier = Modifier.fillMaxHeight(),
+                        state = updatesListState,
+                        contentPadding = contentPaddingWithNavBar,
+                    ) {
+                        updatesUiItems(
+                            uiModels = uiModels,
+                            itemUiModels = itemUiModels,
+                            selected = selected,
+                            selectedPositions = selectedPositions,
+                            onClickCover = onClickCover,
+                            onClickUpdate = onClickUpdate,
+                            onDownloadChapter = onDownloadChapter,
+                            relativeTime = relativeTime,
+                            dateFormat = dateFormat,
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun UpdatesAppBar(
+    modifier: Modifier = Modifier,
+    selected: MutableList<UpdatesUiModel.Item>,
+    incognitoMode: Boolean,
+    downloadedOnlyMode: Boolean,
+    onUpdateLibrary: () -> Unit,
+    // For action mode
+    actionModeCounter: Int,
+    onSelectAll: () -> Unit,
+    onInvertSelection: () -> Unit,
+) {
+    val isActionMode = actionModeCounter > 0
+    val backgroundColor = if (isActionMode) {
+        TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f).value
+    } else {
+        MaterialTheme.colorScheme.surface
+    }
+
+    Column(
+        modifier = modifier.drawBehind { drawRect(backgroundColor) },
+    ) {
+        SmallTopAppBar(
+            modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
+            navigationIcon = {
+                if (isActionMode) {
+                    IconButton(onClick = { selected.clear() }) {
+                        Icon(
+                            imageVector = Icons.Default.Close,
+                            contentDescription = stringResource(id = R.string.action_cancel),
+                        )
+                    }
+                }
+            },
+            title = {
+                Text(
+                    text = if (isActionMode) actionModeCounter.toString() else stringResource(R.string.label_recent_updates),
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            },
+            actions = {
+                if (isActionMode) {
+                    IconButton(onClick = onSelectAll) {
+                        Icon(
+                            imageVector = Icons.Default.SelectAll,
+                            contentDescription = stringResource(R.string.action_select_all),
+                        )
+                    }
+                    IconButton(onClick = onInvertSelection) {
+                        Icon(
+                            imageVector = Icons.Default.FlipToBack,
+                            contentDescription = stringResource(R.string.action_select_inverse),
+                        )
+                    }
+                } else {
+                    IconButton(onClick = onUpdateLibrary) {
+                        Icon(
+                            imageVector = Icons.Default.Refresh,
+                            contentDescription = stringResource(R.string.action_update_library),
+                        )
+                    }
+                }
+            },
+            // Background handled by parent
+            colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+                containerColor = Color.Transparent,
+                scrolledContainerColor = Color.Transparent,
+            ),
+        )
+
+        if (downloadedOnlyMode) {
+            DownloadedOnlyModeBanner()
+        }
+        if (incognitoMode) {
+            IncognitoModeBanner()
+        }
+    }
+}
+
+@Composable
+fun UpdatesBottomBar(
+    selected: MutableList<UpdatesUiModel.Item>,
+    onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
+    onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
+    onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
+    onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
+) {
+    MangaBottomActionMenu(
+        visible = selected.isNotEmpty(),
+        modifier = Modifier.fillMaxWidth(),
+        onBookmarkClicked = {
+            onMultiBookmarkClicked.invoke(selected.map { it.item }, true)
+            selected.clear()
+        }.takeIf { selected.any { !it.item.update.bookmark } },
+        onRemoveBookmarkClicked = {
+            onMultiBookmarkClicked.invoke(selected.map { it.item }, false)
+            selected.clear()
+        }.takeIf { selected.all { it.item.update.bookmark } },
+        onMarkAsReadClicked = {
+            onMultiMarkAsReadClicked(selected.map { it.item }, true)
+            selected.clear()
+        }.takeIf { selected.any { !it.item.update.read } },
+        onMarkAsUnreadClicked = {
+            onMultiMarkAsReadClicked(selected.map { it.item }, false)
+            selected.clear()
+        }.takeIf { selected.any { it.item.update.read } },
+        onDownloadClicked = {
+            onDownloadChapter(selected.map { it.item }, ChapterDownloadAction.START)
+            selected.clear()
+        }.takeIf {
+            selected.any { it.item.downloadStateProvider() != Download.State.DOWNLOADED }
+        },
+        onDeleteClicked = {
+            onMultiDeleteClicked(selected.map { it.item })
+            selected.clear()
+        }.takeIf { selected.any { it.item.downloadStateProvider() == Download.State.DOWNLOADED } },
+    )
+}
+
+sealed class UpdatesUiModel {
+    data class Header(val date: Date) : UpdatesUiModel()
+    data class Item(val item: UpdatesItem) : UpdatesUiModel()
+}

+ 270 - 0
app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt

@@ -0,0 +1,270 @@
+package eu.kanade.presentation.updates
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Bookmark
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+import eu.kanade.presentation.components.ChapterDownloadAction
+import eu.kanade.presentation.components.ChapterDownloadIndicator
+import eu.kanade.presentation.components.MangaCover
+import eu.kanade.presentation.components.RelativeDateHeader
+import eu.kanade.presentation.util.ReadItemAlpha
+import eu.kanade.presentation.util.horizontalPadding
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
+import java.text.DateFormat
+
+fun LazyListScope.updatesUiItems(
+    uiModels: List<UpdatesUiModel>,
+    itemUiModels: List<UpdatesUiModel.Item>,
+    selected: MutableList<UpdatesUiModel.Item>,
+    selectedPositions: Array<Int>,
+    onClickCover: (UpdatesItem) -> Unit,
+    onClickUpdate: (UpdatesItem) -> Unit,
+    onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
+    relativeTime: Int,
+    dateFormat: DateFormat,
+) {
+    items(
+        items = uiModels,
+        contentType = {
+            when (it) {
+                is UpdatesUiModel.Header -> "header"
+                is UpdatesUiModel.Item -> "item"
+            }
+        },
+        key = {
+            when (it) {
+                is UpdatesUiModel.Header -> it.hashCode()
+                is UpdatesUiModel.Item -> it.item.update.chapterId
+            }
+        },
+    ) { item ->
+        when (item) {
+            is UpdatesUiModel.Header -> {
+                RelativeDateHeader(
+                    modifier = Modifier.animateItemPlacement(),
+                    date = item.date,
+                    relativeTime = relativeTime,
+                    dateFormat = dateFormat,
+                )
+            }
+            is UpdatesUiModel.Item -> {
+                val value = item.item
+                val update = value.update
+                UpdatesUiItem(
+                    modifier = Modifier.animateItemPlacement(),
+                    update = update,
+                    selected = selected.contains(item),
+                    onClick = {
+                        onUpdatesItemClick(
+                            updatesItem = item,
+                            selected = selected,
+                            updates = itemUiModels,
+                            selectedPositions = selectedPositions,
+                            onUpdateClicked = onClickUpdate,
+                        )
+                    },
+                    onLongClick = {
+                        onUpdatesItemLongClick(
+                            updatesItem = item,
+                            selected = selected,
+                            updates = itemUiModels,
+                            selectedPositions = selectedPositions,
+                        )
+                    },
+                    onClickCover = { if (selected.size == 0) onClickCover(value) },
+                    onDownloadChapter = {
+                        if (selected.size == 0) onDownloadChapter(listOf(value), it)
+                    },
+                    downloadStateProvider = value.downloadStateProvider,
+                    downloadProgressProvider = value.downloadProgressProvider,
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun UpdatesUiItem(
+    modifier: Modifier,
+    update: UpdatesWithRelations,
+    selected: Boolean,
+    onClick: () -> Unit,
+    onLongClick: () -> Unit,
+    onClickCover: () -> Unit,
+    onDownloadChapter: (ChapterDownloadAction) -> Unit,
+    // Download Indicator
+    downloadStateProvider: () -> Download.State,
+    downloadProgressProvider: () -> Int,
+) {
+    Row(
+        modifier = modifier
+            .background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
+            .combinedClickable(
+                onClick = onClick,
+                onLongClick = onLongClick,
+            )
+            .height(56.dp)
+            .padding(horizontal = horizontalPadding),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        MangaCover.Square(
+            modifier = Modifier
+                .padding(vertical = 6.dp)
+                .fillMaxHeight(),
+            data = update.coverData,
+            onClick = onClickCover,
+        )
+        Column(
+            modifier = Modifier
+                .padding(horizontal = horizontalPadding)
+                .weight(1f),
+        ) {
+            val bookmark = remember(update.bookmark) { update.bookmark }
+            val read = remember(update.read) { update.read }
+
+            val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f }
+
+            val secondaryTextColor = if (bookmark && !read) {
+                MaterialTheme.colorScheme.primary
+            } else {
+                MaterialTheme.colorScheme.onSurface
+            }
+
+            Text(
+                text = update.mangaTitle,
+                maxLines = 1,
+                style = MaterialTheme.typography.bodyMedium,
+                overflow = TextOverflow.Ellipsis,
+                modifier = Modifier.alpha(textAlpha),
+            )
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                var textHeight by remember { mutableStateOf(0) }
+                if (bookmark) {
+                    Icon(
+                        imageVector = Icons.Default.Bookmark,
+                        contentDescription = stringResource(R.string.action_filter_bookmarked),
+                        modifier = Modifier
+                            .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
+                        tint = MaterialTheme.colorScheme.primary,
+                    )
+                    Spacer(modifier = Modifier.width(2.dp))
+                }
+                Text(
+                    text = update.chapterName,
+                    maxLines = 1,
+                    style = MaterialTheme.typography.bodySmall
+                        .copy(color = secondaryTextColor),
+                    overflow = TextOverflow.Ellipsis,
+                    onTextLayout = { textHeight = it.size.height },
+                    modifier = Modifier.alpha(textAlpha),
+                )
+            }
+        }
+        ChapterDownloadIndicator(
+            modifier = Modifier.padding(start = 4.dp),
+            downloadStateProvider = downloadStateProvider,
+            downloadProgressProvider = downloadProgressProvider,
+            onClick = onDownloadChapter,
+        )
+    }
+}
+
+private fun onUpdatesItemLongClick(
+    updatesItem: UpdatesUiModel.Item,
+    selected: MutableList<UpdatesUiModel.Item>,
+    updates: List<UpdatesUiModel.Item>,
+    selectedPositions: Array<Int>,
+): Boolean {
+    if (!selected.contains(updatesItem)) {
+        val selectedIndex = updates.indexOf(updatesItem)
+        if (selected.isEmpty()) {
+            selected.add(updatesItem)
+            selectedPositions[0] = selectedIndex
+            selectedPositions[1] = selectedIndex
+            return true
+        }
+
+        // Try to select the items in-between when possible
+        val range: IntRange
+        if (selectedIndex < selectedPositions[0]) {
+            range = selectedIndex until selectedPositions[0]
+            selectedPositions[0] = selectedIndex
+        } else if (selectedIndex > selectedPositions[1]) {
+            range = (selectedPositions[1] + 1)..selectedIndex
+            selectedPositions[1] = selectedIndex
+        } else {
+            // Just select itself
+            range = selectedIndex..selectedIndex
+        }
+
+        range.forEach {
+            val toAdd = updates[it]
+            if (!selected.contains(toAdd)) {
+                selected.add(toAdd)
+            }
+        }
+        return true
+    }
+    return false
+}
+
+private fun onUpdatesItemClick(
+    updatesItem: UpdatesUiModel.Item,
+    selected: MutableList<UpdatesUiModel.Item>,
+    updates: List<UpdatesUiModel.Item>,
+    selectedPositions: Array<Int>,
+    onUpdateClicked: (UpdatesItem) -> Unit,
+) {
+    val selectedIndex = updates.indexOf(updatesItem)
+    when {
+        selected.contains(updatesItem) -> {
+            val removedIndex = updates.indexOf(updatesItem)
+            selected.remove(updatesItem)
+
+            if (removedIndex == selectedPositions[0]) {
+                selectedPositions[0] = updates.indexOfFirst { selected.contains(it) }
+            } else if (removedIndex == selectedPositions[1]) {
+                selectedPositions[1] = updates.indexOfLast { selected.contains(it) }
+            }
+        }
+        selected.isNotEmpty() -> {
+            if (selectedIndex < selectedPositions[0]) {
+                selectedPositions[0] = selectedIndex
+            } else if (selectedIndex > selectedPositions[1]) {
+                selectedPositions[1] = selectedIndex
+            }
+            selected.add(updatesItem)
+        }
+        else -> onUpdateClicked(updatesItem.item)
+    }
+}

+ 2 - 0
app/src/main/java/eu/kanade/presentation/util/Constants.kt

@@ -12,3 +12,5 @@ val horizontalPadding = horizontal
 val verticalPadding = vertical
 
 val topPaddingValues = PaddingValues(top = vertical)
+
+const val ReadItemAlpha = .38f

+ 18 - 0
app/src/main/java/eu/kanade/presentation/util/LazyListState.kt

@@ -27,3 +27,21 @@ fun LazyListState.isScrollingUp(): Boolean {
         }
     }.value
 }
+
+@Composable
+fun LazyListState.isScrollingDown(): Boolean {
+    var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
+    var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
+    return remember {
+        derivedStateOf {
+            if (previousIndex != firstVisibleItemIndex) {
+                previousIndex < firstVisibleItemIndex
+            } else {
+                previousScrollOffset <= firstVisibleItemScrollOffset
+            }.also {
+                previousIndex = firstVisibleItemIndex
+                previousScrollOffset = firstVisibleItemScrollOffset
+            }
+        }
+    }.value
+}

+ 13 - 0
app/src/main/java/eu/kanade/presentation/util/NavBarVisibility.kt

@@ -0,0 +1,13 @@
+package eu.kanade.presentation.util
+
+enum class NavBarVisibility {
+    SHOW,
+    HIDE
+}
+
+fun NavBarVisibility.toBoolean(): Boolean {
+    return when (this) {
+        NavBarVisibility.SHOW -> true
+        NavBarVisibility.HIDE -> false
+    }
+}

+ 11 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -226,7 +226,7 @@ class MainActivity : BaseActivity() {
         if (!router.hasRootController()) {
             // Set start screen
             if (!handleIntentAction(intent)) {
-                setSelectedNavItem(startScreenId)
+                moveToStartScreen()
             }
         }
         syncActivityViewWithController()
@@ -483,10 +483,15 @@ class MainActivity : BaseActivity() {
     }
 
     override fun onBackPressed() {
+        // Updates screen has custom back handler
+        if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
+            router.handleBack()
+            return
+        }
         val backstackSize = router.backstackSize
         if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
             // Return to start screen
-            setSelectedNavItem(startScreenId)
+            moveToStartScreen()
         } else if (shouldHandleExitConfirmation()) {
             // Exit confirmation (resets after 2 seconds)
             lifecycleScope.launchUI { resetExitConfirmation() }
@@ -499,6 +504,10 @@ class MainActivity : BaseActivity() {
         }
     }
 
+    fun moveToStartScreen() {
+        setSelectedNavItem(startScreenId)
+    }
+
     override fun onSupportActionModeStarted(mode: ActionMode) {
         binding.appbar.apply {
             tag = isTransparentWhenNotLifted

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -27,7 +27,7 @@ import eu.kanade.data.chapter.NoChaptersException
 import eu.kanade.domain.category.model.Category
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.toDbManga
-import eu.kanade.presentation.manga.ChapterDownloadAction
+import eu.kanade.presentation.components.ChapterDownloadAction
 import eu.kanade.presentation.manga.DownloadAction
 import eu.kanade.presentation.manga.MangaScreen
 import eu.kanade.presentation.util.calculateWindowWidthSizeClass

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterDownloadView.kt

@@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.AbstractComposeView
+import eu.kanade.presentation.components.ChapterDownloadAction
 import eu.kanade.presentation.components.ChapterDownloadIndicator
-import eu.kanade.presentation.manga.ChapterDownloadAction
 import eu.kanade.presentation.theme.TachiyomiTheme
 import eu.kanade.tachiyomi.data.download.model.Download
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterHolder.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base
 
 import android.view.View
 import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.presentation.manga.ChapterDownloadAction
+import eu.kanade.presentation.components.ChapterDownloadAction
 
 open class BaseChapterHolder(
     view: View,

+ 0 - 53
app/src/main/java/eu/kanade/tachiyomi/ui/recent/DateSectionItem.kt

@@ -1,53 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractHeaderItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
-import eu.kanade.tachiyomi.util.lang.toRelativeString
-import java.text.DateFormat
-import java.util.Date
-
-class DateSectionItem(
-    private val date: Date,
-    private val range: Int,
-    private val dateFormat: DateFormat,
-) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.section_header_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
-        return DateSectionItemHolder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: DateSectionItemHolder, position: Int, payloads: List<Any?>?) {
-        holder.bind(this)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other is DateSectionItem) {
-            return date == other.date
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return date.hashCode()
-    }
-
-    inner class DateSectionItemHolder(private val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
-
-        private val binding = SectionHeaderItemBinding.bind(view)
-
-        fun bind(item: DateSectionItem) {
-            binding.title.text = item.date.toRelativeString(view.context, range, dateFormat)
-        }
-    }
-}

+ 0 - 33
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/ConfirmDeleteChaptersDialog.kt

@@ -1,33 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent.updates
-
-import android.app.Dialog
-import android.os.Bundle
-import com.bluelinelabs.conductor.Controller
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class ConfirmDeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
-        where T : Controller, T : ConfirmDeleteChaptersDialog.Listener {
-
-    private var chaptersToDelete = emptyList<UpdatesItem>()
-
-    constructor(target: T, chaptersToDelete: List<UpdatesItem>) : this() {
-        this.chaptersToDelete = chaptersToDelete
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        return MaterialAlertDialogBuilder(activity!!)
-            .setMessage(R.string.confirm_delete_chapters)
-            .setPositiveButton(android.R.string.ok) { _, _ ->
-                (targetController as? Listener)?.deleteChapters(chaptersToDelete)
-            }
-            .setNegativeButton(android.R.string.cancel, null)
-            .create()
-    }
-
-    interface Listener {
-        fun deleteChapters(chaptersToDelete: List<UpdatesItem>)
-    }
-}

+ 0 - 29
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesAdapter.kt

@@ -1,29 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent.updates
-
-import android.content.Context
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
-import eu.kanade.tachiyomi.util.system.getResourceColor
-
-class UpdatesAdapter(
-    val controller: UpdatesController,
-    context: Context,
-    val items: List<IFlexible<*>>?,
-) : BaseChaptersAdapter<IFlexible<*>>(controller, items) {
-
-    var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
-    var unreadColor = context.getResourceColor(R.attr.colorOnSurface)
-    val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
-    var bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
-
-    val coverClickListener: OnCoverClickListener = controller
-
-    init {
-        setDisplayHeadersAtStartUp(true)
-    }
-
-    interface OnCoverClickListener {
-        fun onCoverClick(position: Int)
-    }
-}

+ 87 - 366
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt

@@ -1,149 +1,65 @@
 package eu.kanade.tachiyomi.ui.recent.updates
 
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.view.ActionMode
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.LinearLayoutManager
-import dev.chrisbanes.insetter.applyInsetter
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.SelectableAdapter
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.appcompat.app.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import eu.kanade.presentation.components.ChapterDownloadAction
+import eu.kanade.presentation.components.LoadingScreen
+import eu.kanade.presentation.updates.UpdateScreen
+import eu.kanade.presentation.util.NavBarVisibility
+import eu.kanade.presentation.util.toBoolean
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.notification.Notifications
-import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.system.logcat
-import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.util.view.onAnimationsFinished
-import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import eu.kanade.tachiyomi.widget.materialdialogs.await
 import kotlinx.coroutines.launch
-import logcat.LogPriority
-import reactivecircus.flowbinding.recyclerview.scrollStateChanges
-import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 
 /**
  * Fragment that shows recent chapters.
  */
 class UpdatesController :
-    NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
-    RootController,
-    ActionModeWithToolbar.Callback,
-    FlexibleAdapter.OnItemClickListener,
-    FlexibleAdapter.OnItemLongClickListener,
-    FlexibleAdapter.OnUpdateListener,
-    BaseChaptersAdapter.OnChapterClickListener,
-    ConfirmDeleteChaptersDialog.Listener,
-    UpdatesAdapter.OnCoverClickListener {
-
-    /**
-     * Action mode for multiple selection.
-     */
-    private var actionMode: ActionModeWithToolbar? = null
-
-    /**
-     * Adapter containing the recent chapters.
-     */
-    var adapter: UpdatesAdapter? = null
-        private set
-
-    init {
-        setHasOptionsMenu(true)
-    }
-
-    override fun getTitle(): String? {
-        return resources?.getString(R.string.label_recent_updates)
-    }
-
-    override fun createPresenter(): UpdatesPresenter {
-        return UpdatesPresenter()
-    }
-
-    override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater)
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-        binding.recycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
-            }
+    FullComposeController<UpdatesPresenter>(),
+    RootController {
+
+    override fun createPresenter() = UpdatesPresenter()
+
+    @Composable
+    override fun ComposeContent() {
+        val state by presenter.state.collectAsState()
+        when (state) {
+            is UpdatesState.Loading -> LoadingScreen()
+            is UpdatesState.Error -> Text(text = (state as UpdatesState.Error).error.message.orEmpty())
+            is UpdatesState.Success ->
+                UpdateScreen(
+                    state = (state as UpdatesState.Success),
+                    onClickCover = this::openManga,
+                    onClickUpdate = this::openChapter,
+                    onDownloadChapter = this::downloadChapters,
+                    onUpdateLibrary = this::updateLibrary,
+                    onBackClicked = this::onBackClicked,
+                    toggleNavBarVisibility = this::toggleNavBarVisibility,
+                    // For bottom action menu
+                    onMultiBookmarkClicked = { updatesItems, bookmark ->
+                        presenter.bookmarkUpdates(updatesItems, bookmark)
+                    },
+                    onMultiMarkAsReadClicked = { updatesItems, read ->
+                        presenter.markUpdatesRead(updatesItems, read)
+                    },
+                    onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
+                )
         }
-
-        view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
-
-        // Init RecyclerView and adapter
-        val layoutManager = LinearLayoutManager(view.context)
-        binding.recycler.layoutManager = layoutManager
-        binding.recycler.setHasFixedSize(true)
-        binding.recycler.scrollStateChanges()
-            .onEach {
-                // Disable swipe refresh when view is not at the top
-                val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
-                binding.swipeRefresh.isEnabled = firstPos <= 0
-            }
-            .launchIn(viewScope)
-
-        binding.swipeRefresh.isRefreshing = true
-        binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
-        binding.swipeRefresh.refreshes()
-            .onEach {
-                updateLibrary()
-
-                // It can be a very long operation, so we disable swipe refresh and show a toast.
-                binding.swipeRefresh.isRefreshing = false
-            }
-            .launchIn(viewScope)
-
-        viewScope.launch {
-            presenter.updates.collectLatest { updatesItems ->
-                destroyActionModeIfNeeded()
-                if (adapter == null) {
-                    adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, updatesItems)
-                    binding.recycler.adapter = adapter
-                    adapter!!.fastScroller = binding.fastScroller
-                } else {
-                    adapter?.updateDataSet(updatesItems)
-                }
-                binding.swipeRefresh.isRefreshing = false
-                binding.fastScroller.isVisible = true
-                binding.recycler.onAnimationsFinished {
-                    (activity as? MainActivity)?.ready = true
-                }
-            }
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        destroyActionModeIfNeeded()
-        adapter = null
-        super.onDestroyView(view)
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.updates, menu)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_update_library -> updateLibrary()
-        }
-
-        return super.onOptionsItemSelected(item)
     }
 
     private fun updateLibrary() {
@@ -154,262 +70,67 @@ class UpdatesController :
         }
     }
 
-    /**
-     * Returns selected chapters
-     * @return list of selected chapters
-     */
-    private fun getSelectedChapters(): List<UpdatesItem> {
-        val adapter = adapter ?: return emptyList()
-        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? UpdatesItem }
-    }
-
-    /**
-     * Called when item in list is clicked
-     * @param position position of clicked item
-     */
-    override fun onItemClick(view: View, position: Int): Boolean {
-        val adapter = adapter ?: return false
-
-        // Get item from position
-        val item = adapter.getItem(position) as? UpdatesItem ?: return false
-        return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
-            toggleSelection(position)
-            true
-        } else {
-            openChapter(item)
-            false
-        }
-    }
-
-    /**
-     * Called when item in list is long clicked
-     * @param position position of clicked item
-     */
-    override fun onItemLongClick(position: Int) {
-        val activity = activity
-        if (actionMode == null && activity is MainActivity) {
-            actionMode = activity.startActionModeAndToolbar(this)
-            activity.showBottomNav(false)
-        }
-        toggleSelection(position)
+    // Let compose view handle this
+    override fun handleBack(): Boolean {
+        (activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher?.onBackPressed()
+        return true
     }
 
-    /**
-     * Called to toggle selection
-     * @param position position of selected item
-     */
-    private fun toggleSelection(position: Int) {
-        val adapter = adapter ?: return
-        adapter.toggleSelection(position)
-        actionMode?.invalidate()
+    private fun onBackClicked() {
+        (activity as? MainActivity)?.moveToStartScreen()
     }
 
-    /**
-     * Open chapter in reader
-     * @param chapter selected chapter
-     */
-    private fun openChapter(item: UpdatesItem) {
-        val activity = activity ?: return
-        val intent = ReaderActivity.newIntent(activity, item.manga.id, item.chapter.id)
-        startActivity(intent)
+    private fun toggleNavBarVisibility(navBarVisibility: NavBarVisibility) {
+        val showNavBar = navBarVisibility.toBoolean()
+        (activity as? MainActivity)?.showBottomNav(showNavBar)
     }
 
     /**
      * Download selected items
-     * @param chapters list of selected [UpdatesItem]s
-     */
-    private fun downloadChapters(chapters: List<UpdatesItem>) {
-        presenter.downloadChapters(chapters)
-        destroyActionModeIfNeeded()
-    }
-
-    override fun onUpdateEmptyView(size: Int) {
-        if (size > 0) {
-            binding.emptyView.hide()
-        } else {
-            binding.emptyView.show(R.string.information_no_recent)
-        }
-    }
-
-    /**
-     * Update download status of chapter
-     * @param download [Download] object containing download progress.
+     * @param items list of selected [UpdatesItem]s
      */
-    fun onChapterDownloadUpdate(download: Download) {
-        adapter?.currentItems
-            ?.filterIsInstance<UpdatesItem>()
-            ?.find { it.chapter.id == download.chapter.id }?.let {
-                adapter?.updateItem(it, it.status)
+    private fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
+        if (items.isEmpty()) return
+        viewScope.launch {
+            when (action) {
+                ChapterDownloadAction.START -> {
+                    presenter.downloadChapters(items)
+                    if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
+                        DownloadService.start(activity!!)
+                    }
+                }
+                ChapterDownloadAction.START_NOW -> {
+                    val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
+                    presenter.startDownloadingNow(chapterId)
+                }
+                ChapterDownloadAction.CANCEL -> {
+                    val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
+                    presenter.cancelDownload(chapterId)
+                }
+                ChapterDownloadAction.DELETE -> {
+                    presenter.deleteChapters(items)
+                }
             }
-    }
-
-    /**
-     * Mark chapter as read
-     * @param chapters list of chapters
-     */
-    private fun markAsRead(chapters: List<UpdatesItem>) {
-        presenter.markChapterRead(chapters, true)
-        destroyActionModeIfNeeded()
-    }
-
-    /**
-     * Mark chapter as unread
-     * @param chapters list of selected [UpdatesItem]
-     */
-    private fun markAsUnread(chapters: List<UpdatesItem>) {
-        presenter.markChapterRead(chapters, false)
-        destroyActionModeIfNeeded()
-    }
-
-    override fun deleteChapters(chaptersToDelete: List<UpdatesItem>) {
-        presenter.deleteChapters(chaptersToDelete)
-        destroyActionModeIfNeeded()
-    }
-
-    private fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    override fun onCoverClick(position: Int) {
-        destroyActionModeIfNeeded()
-
-        val chapterClicked = adapter?.getItem(position) as? UpdatesItem ?: return
-        openManga(chapterClicked)
-    }
-
-    private fun openManga(chapter: UpdatesItem) {
-        router.pushController(MangaController(chapter.manga.id!!))
-    }
-
-    /**
-     * Called when chapters are deleted
-     */
-    fun onChaptersDeleted() {
-        adapter?.notifyDataSetChanged()
-    }
-
-    /**
-     * Called when error while deleting
-     * @param error error message
-     */
-    fun onChaptersDeletedError(error: Throwable) {
-        logcat(LogPriority.ERROR, error)
-    }
-
-    override fun downloadChapter(position: Int) {
-        val item = adapter?.getItem(position) as? UpdatesItem ?: return
-        if (item.status == Download.State.ERROR) {
-            DownloadService.start(activity!!)
-        } else {
-            downloadChapters(listOf(item))
         }
-        adapter?.updateItem(item)
-    }
-
-    override fun deleteChapter(position: Int) {
-        val item = adapter?.getItem(position) as? UpdatesItem ?: return
-        deleteChapters(listOf(item))
-        adapter?.updateItem(item)
     }
 
-    override fun startDownloadNow(position: Int) {
-        val item = adapter?.getItem(position) as? UpdatesItem ?: return
-        presenter.startDownloadingNow(item.chapter)
-    }
-
-    private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
-        presenter.bookmarkChapters(chapters, bookmarked)
-        destroyActionModeIfNeeded()
-    }
-
-    /**
-     * Called when ActionMode created.
-     * @param mode the ActionMode object
-     * @param menu menu object of ActionMode
-     */
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.generic_selection, menu)
-        adapter?.mode = SelectableAdapter.Mode.MULTI
-        return true
-    }
-
-    override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
-        menuInflater.inflate(R.menu.updates_chapter_selection, menu)
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = adapter?.selectedItemCount ?: 0
-        if (count == 0) {
-            // Destroy action mode if there are no items selected.
-            destroyActionModeIfNeeded()
-        } else {
-            mode.title = count.toString()
-        }
-        return true
-    }
-
-    override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
-        val chapters = getSelectedChapters()
-        if (chapters.isEmpty()) return
-        toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
-        toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
-        toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark }
-        toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark }
-        toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
-        toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
-    }
-
-    /**
-     * Called when ActionMode item clicked
-     * @param mode the ActionMode object
-     * @param item item from ActionMode.
-     */
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        return onActionItemClicked(item)
-    }
-
-    private fun onActionItemClicked(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_select_all -> selectAll()
-            R.id.action_select_inverse -> selectInverse()
-            R.id.action_download -> downloadChapters(getSelectedChapters())
-            R.id.action_delete ->
-                ConfirmDeleteChaptersDialog(this, getSelectedChapters())
-                    .showDialog(router)
-            R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true)
-            R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false)
-            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
-            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
-            else -> return false
+    private fun deleteChaptersWithConfirmation(items: List<UpdatesItem>) {
+        if (items.isEmpty()) return
+        viewScope.launch {
+            val result = MaterialAlertDialogBuilder(activity!!)
+                .setMessage(R.string.confirm_delete_chapters)
+                .await(android.R.string.ok, android.R.string.cancel)
+            if (result == AlertDialog.BUTTON_POSITIVE) presenter.deleteChapters(items)
         }
-        return true
-    }
-
-    /**
-     * Called when ActionMode destroyed
-     * @param mode the ActionMode object
-     */
-    override fun onDestroyActionMode(mode: ActionMode) {
-        adapter?.mode = SelectableAdapter.Mode.IDLE
-        adapter?.clearSelection()
-
-        (activity as? MainActivity)?.showBottomNav(true)
-
-        actionMode = null
     }
 
-    private fun selectAll() {
-        val adapter = adapter ?: return
-        adapter.selectAll()
-        actionMode?.invalidate()
+    private fun openChapter(item: UpdatesItem) {
+        val activity = activity ?: return
+        val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
+        startActivity(intent)
     }
 
-    private fun selectInverse() {
-        val adapter = adapter ?: return
-        for (i in 0..adapter.itemCount) {
-            adapter.toggleSelection(i)
-        }
-        actionMode?.invalidate()
-        adapter.notifyDataSetChanged()
+    private fun openManga(item: UpdatesItem) {
+        router.pushController(MangaController(item.update.mangaId))
     }
 }

+ 0 - 62
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt

@@ -1,62 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent.updates
-
-import android.view.View
-import androidx.core.view.isVisible
-import coil.dispose
-import coil.load
-import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
-
-/**
- * Holder that contains chapter item
- * UI related actions should be called from here.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new recent chapter holder.
- */
-class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) :
-    BaseChapterHolder(view, adapter) {
-
-    private val binding = UpdatesItemBinding.bind(view)
-
-    init {
-        binding.mangaCover.setOnClickListener {
-            adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
-        }
-
-        binding.download.listener = downloadActionListener
-    }
-
-    fun bind(item: UpdatesItem) {
-        // Set chapter title
-        binding.chapterTitle.text = item.chapter.name
-
-        // Set manga title
-        binding.mangaTitle.text = item.manga.title
-
-        // Check if chapter is read and/or bookmarked and set correct color
-        if (item.chapter.read) {
-            binding.chapterTitle.setTextColor(adapter.readColor)
-            binding.mangaTitle.setTextColor(adapter.readColor)
-        } else {
-            binding.mangaTitle.setTextColor(adapter.unreadColor)
-            binding.chapterTitle.setTextColor(
-                if (item.chapter.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary,
-            )
-        }
-
-        // Set bookmark status
-        binding.bookmarkIcon.isVisible = item.chapter.bookmark
-
-        // Set chapter status
-        binding.download.isVisible = item.manga.source != LocalSource.ID
-        binding.download.setState(item.status, item.progress)
-
-        // Set cover
-        binding.mangaCover.dispose()
-        binding.mangaCover.load(item.manga)
-    }
-}

+ 0 - 32
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt

@@ -1,32 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent.updates
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.domain.chapter.model.Chapter
-import eu.kanade.domain.manga.model.Manga
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
-import eu.kanade.tachiyomi.ui.recent.DateSectionItem
-
-class UpdatesItem(chapter: Chapter, val manga: Manga, header: DateSectionItem) :
-    BaseChapterItem<UpdatesHolder, DateSectionItem>(chapter, header) {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.updates_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): UpdatesHolder {
-        return UpdatesHolder(view, adapter as UpdatesAdapter)
-    }
-
-    override fun bindViewHolder(
-        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-        holder: UpdatesHolder,
-        position: Int,
-        payloads: List<Any?>?,
-    ) {
-        holder.bind(this)
-    }
-}

+ 225 - 137
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt

@@ -1,233 +1,321 @@
 package eu.kanade.tachiyomi.ui.recent.updates
 
 import android.os.Bundle
-import eu.kanade.data.DatabaseHandler
-import eu.kanade.data.manga.mangaChapterMapper
+import androidx.compose.runtime.Immutable
+import eu.kanade.core.util.insertSeparators
+import eu.kanade.domain.chapter.interactor.GetChapter
 import eu.kanade.domain.chapter.interactor.SetReadStatus
 import eu.kanade.domain.chapter.interactor.UpdateChapter
-import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.chapter.model.ChapterUpdate
 import eu.kanade.domain.chapter.model.toDbChapter
-import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.interactor.GetManga
+import eu.kanade.domain.updates.interactor.GetUpdates
+import eu.kanade.domain.updates.model.UpdatesWithRelations
+import eu.kanade.presentation.updates.UpdatesUiModel
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.recent.DateSectionItem
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.toDateKey
 import eu.kanade.tachiyomi.util.lang.withUIContext
+import eu.kanade.tachiyomi.util.preference.asHotFlow
 import eu.kanade.tachiyomi.util.system.logcat
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.update
 import logcat.LogPriority
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
-import java.text.DateFormat
 import java.util.Calendar
 import java.util.Date
-import java.util.TreeMap
 
 class UpdatesPresenter(
-    private val preferences: PreferencesHelper = Injekt.get(),
-    private val downloadManager: DownloadManager = Injekt.get(),
-    private val sourceManager: SourceManager = Injekt.get(),
-    private val handler: DatabaseHandler = Injekt.get(),
     private val updateChapter: UpdateChapter = Injekt.get(),
     private val setReadStatus: SetReadStatus = Injekt.get(),
+    private val getUpdates: GetUpdates = Injekt.get(),
+    private val getManga: GetManga = Injekt.get(),
+    private val sourceManager: SourceManager = Injekt.get(),
+    private val downloadManager: DownloadManager = Injekt.get(),
+    private val getChapter: GetChapter = Injekt.get(),
+    private val preferences: PreferencesHelper = Injekt.get(),
 ) : BasePresenter<UpdatesController>() {
 
-    private val relativeTime: Int = preferences.relativeTime().get()
-    private val dateFormat: DateFormat = preferences.dateFormat()
+    private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading)
+    val state: StateFlow<UpdatesState> = _state.asStateFlow()
 
-    private val _updates: MutableStateFlow<List<UpdatesItem>> = MutableStateFlow(listOf())
-    val updates: StateFlow<List<UpdatesItem>> = _updates.asStateFlow()
+    /**
+     * Helper function to update the UI state only if it's currently in success state
+     */
+    private fun updateSuccessState(func: (UpdatesState.Success) -> UpdatesState.Success) {
+        _state.update { if (it is UpdatesState.Success) func(it) else it }
+    }
+
+    private var incognitoMode = false
+        set(value) {
+            updateSuccessState { it.copy(isIncognitoMode = value) }
+            field = value
+        }
+    private var downloadOnlyMode = false
+        set(value) {
+            updateSuccessState { it.copy(isDownloadedOnlyMode = value) }
+            field = value
+        }
+
+    /**
+     * Subscription to observe download status changes.
+     */
+    private var observeDownloadsStatusJob: Job? = null
+    private var observeDownloadsPageJob: Job? = null
 
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
 
         presenterScope.launchIO {
-            subscribeToUpdates()
+            // Set date limit for recent chapters
+            val calendar = Calendar.getInstance().apply {
+                time = Date()
+                add(Calendar.MONTH, -3)
+            }
 
+            getUpdates.subscribe(calendar)
+                .catch { exception ->
+                    _state.value = UpdatesState.Error(exception)
+                }
+                .collectLatest { updates ->
+                    val uiModels = updates.toUpdateUiModels()
+                    _state.update { currentState ->
+                        when (currentState) {
+                            is UpdatesState.Success -> currentState.copy(uiModels)
+                            is UpdatesState.Loading, is UpdatesState.Error ->
+                                UpdatesState.Success(
+                                    uiModels = uiModels,
+                                    isIncognitoMode = incognitoMode,
+                                    isDownloadedOnlyMode = downloadOnlyMode,
+                                )
+                        }
+                    }
+
+                    observeDownloads()
+                }
+        }
+
+        preferences.incognitoMode()
+            .asHotFlow { incognito ->
+                incognitoMode = incognito
+            }
+            .launchIn(presenterScope)
+
+        preferences.downloadedOnly()
+            .asHotFlow { downloadedOnly ->
+                downloadOnlyMode = downloadedOnly
+            }
+            .launchIn(presenterScope)
+    }
+
+    private fun List<UpdatesWithRelations>.toUpdateUiModels(): List<UpdatesUiModel> {
+        return this.map { update ->
+            val activeDownload = downloadManager.queue.find { update.chapterId == it.chapter.id }
+            val downloaded = downloadManager.isChapterDownloaded(
+                update.chapterName,
+                update.scanlator,
+                update.mangaTitle,
+                update.sourceId,
+            )
+            val downloadState = when {
+                activeDownload != null -> activeDownload.status
+                downloaded -> Download.State.DOWNLOADED
+                else -> Download.State.NOT_DOWNLOADED
+            }
+            val item = UpdatesItem(
+                update = update,
+                downloadStateProvider = { downloadState },
+                downloadProgressProvider = { activeDownload?.progress ?: 0 },
+            )
+            UpdatesUiModel.Item(item)
+        }
+            .insertSeparators { before, after ->
+                val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
+                val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
+                when {
+                    beforeDate.time != afterDate.time && afterDate.time != 0L ->
+                        UpdatesUiModel.Header(afterDate)
+                    // Return null to avoid adding a separator between two items.
+                    else -> null
+                }
+            }
+    }
+
+    private suspend fun observeDownloads() {
+        observeDownloadsStatusJob?.cancel()
+        observeDownloadsStatusJob = presenterScope.launchIO {
             downloadManager.queue.getStatusAsFlow()
                 .catch { error -> logcat(LogPriority.ERROR, error) }
                 .collectLatest {
                     withUIContext {
-                        onDownloadStatusChange(it)
-                        view?.onChapterDownloadUpdate(it)
+                        updateDownloadState(it)
                     }
                 }
+        }
 
+        observeDownloadsPageJob?.cancel()
+        observeDownloadsPageJob = presenterScope.launchIO {
             downloadManager.queue.getProgressAsFlow()
                 .catch { error -> logcat(LogPriority.ERROR, error) }
                 .collectLatest {
                     withUIContext {
-                        view?.onChapterDownloadUpdate(it)
+                        updateDownloadState(it)
                     }
                 }
         }
     }
 
     /**
-     * Get observable containing recent chapters and date
-     */
-    private suspend fun subscribeToUpdates() {
-        // Set date limit for recent chapters
-        val cal = Calendar.getInstance().apply {
-            time = Date()
-            add(Calendar.MONTH, -3)
-        }
-
-        handler
-            .subscribeToList {
-                mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper)
-            }
-            .map { mangaChapter ->
-                val map = TreeMap<Date, MutableList<Pair<Manga, Chapter>>> { d1, d2 -> d2.compareTo(d1) }
-                val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() }
-                byDate.flatMap { entry ->
-                    val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
-                    entry.value
-                        .sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed()
-                        .map { UpdatesItem(it.second, it.first, dateItem) }
-                }
-            }
-            .collectLatest { list ->
-                list.forEach { item ->
-                    // Find an active download for this chapter.
-                    val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
-
-                    // If there's an active download, assign it, otherwise ask the manager if
-                    // the chapter is downloaded and assign it to the status.
-                    if (download != null) {
-                        item.download = download
-                    }
-                }
-                setDownloadedChapters(list)
-
-                _updates.value = list
-
-                // Set unread chapter count for bottom bar badge
-                preferences.unreadUpdatesCount().set(list.count { !it.chapter.read })
-            }
-    }
-
-    /**
-     * Finds and assigns the list of downloaded chapters.
+     * Update status of chapters.
      *
-     * @param items the list of chapter from the database.
+     * @param download download object containing progress.
      */
-    private fun setDownloadedChapters(items: List<UpdatesItem>) {
-        for (item in items) {
-            val manga = item.manga
-            val chapter = item.chapter
+    private fun updateDownloadState(download: Download) {
+        updateSuccessState { successState ->
+            val modifiedIndex = successState.uiModels.indexOfFirst {
+                it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id
+            }
+            if (modifiedIndex < 0) return@updateSuccessState successState
 
-            if (downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)) {
-                item.status = Download.State.DOWNLOADED
+            val newUiModels = successState.uiModels.toMutableList().apply {
+                var uiModel = removeAt(modifiedIndex)
+                if (uiModel is UpdatesUiModel.Item) {
+                    val item = uiModel.item.copy(
+                        downloadStateProvider = { download.status },
+                        downloadProgressProvider = { download.progress },
+                    )
+                    uiModel = UpdatesUiModel.Item(item)
+                }
+                add(modifiedIndex, uiModel)
             }
+            successState.copy(uiModels = newUiModels)
         }
     }
 
-    /**
-     * Update status of chapters.
-     *
-     * @param download download object containing progress.
-     */
-    private fun onDownloadStatusChange(download: Download) {
-        // Assign the download to the model object.
-        if (download.status == Download.State.QUEUE) {
-            val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<UpdatesItem>()
-            val chapter = chapters.find { it.chapter.id == download.chapter.id }
-            if (chapter != null && chapter.download == null) {
-                chapter.download = download
-            }
-        }
+    fun startDownloadingNow(chapterId: Long) {
+        downloadManager.startDownloadNow(chapterId)
     }
 
-    fun startDownloadingNow(chapter: Chapter) {
-        downloadManager.startDownloadNow(chapter.id)
+    fun cancelDownload(chapterId: Long) {
+        val activeDownload = downloadManager.queue.find { chapterId == it.chapter.id } ?: return
+        downloadManager.deletePendingDownload(activeDownload)
+        updateDownloadState(activeDownload.apply { status = Download.State.NOT_DOWNLOADED })
     }
 
     /**
-     * Mark selected chapter as read
-     *
-     * @param items list of selected chapters
-     * @param read read status
+     * Mark the selected updates list as read/unread.
+     * @param updates the list of selected updates.
+     * @param read whether to mark chapters as read or unread.
      */
-    fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
+    fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
         presenterScope.launchIO {
             setReadStatus.await(
                 read = read,
-                values = items
-                    .map { it.chapter }
+                values = updates
+                    .mapNotNull { getChapter.await(it.update.chapterId) }
                     .toTypedArray(),
             )
         }
     }
 
     /**
-     * Delete selected chapters
-     *
-     * @param chapters list of chapters
+     * Bookmarks the given list of chapters.
+     * @param updates the list of chapters to bookmark.
      */
-    fun deleteChapters(chapters: List<UpdatesItem>) {
-        launchIO {
-            try {
-                deleteChaptersInternal(chapters)
-                withUIContext { view?.onChaptersDeleted() }
-            } catch (e: Throwable) {
-                withUIContext { view?.onChaptersDeletedError(e) }
-            }
+    fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
+        presenterScope.launchIO {
+            updates
+                .filterNot { it.update.bookmark == bookmark }
+                .map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
+                .let { updateChapter.awaitAll(it) }
         }
     }
 
     /**
-     * Mark selected chapters as bookmarked
-     * @param items list of selected chapters
-     * @param bookmarked bookmark status
+     * Downloads the given list of chapters with the manager.
+     * @param updatesItem the list of chapters to download.
      */
-    fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
-        presenterScope.launchIO {
-            val toUpdate = items.map {
-                ChapterUpdate(
-                    bookmark = bookmarked,
-                    id = it.chapter.id,
-                )
+    fun downloadChapters(updatesItem: List<UpdatesItem>) {
+        launchIO {
+            val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
+            for (updates in groupedUpdates) {
+                val mangaId = updates.first().update.mangaId
+                val manga = getManga.await(mangaId) ?: continue
+                // Don't download if source isn't available
+                sourceManager.get(manga.source) ?: continue
+                val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
+                downloadManager.downloadChapters(manga, chapters)
             }
-            updateChapter.awaitAll(toUpdate)
         }
     }
 
-    /**
-     * Download selected chapters
-     * @param items list of recent chapters seleted.
-     */
-    fun downloadChapters(items: List<UpdatesItem>) {
-        items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter.toDbChapter())) }
-    }
-
     /**
      * Delete selected chapters
      *
-     * @param items chapters selected
+     * @param updatesItem list of chapters
      */
-    private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
-        val itemsByManga = chapterItems.groupBy { it.manga.id }
-        for ((_, items) in itemsByManga) {
-            val manga = items.first().manga
-            val source = sourceManager.get(manga.source) ?: continue
-            val chapters = items.map { it.chapter.toDbChapter() }
-
-            downloadManager.deleteChapters(chapters, manga, source)
-            items.forEach {
-                it.status = Download.State.NOT_DOWNLOADED
-                it.download = null
+    fun deleteChapters(updatesItem: List<UpdatesItem>) {
+        launchIO {
+            val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
+            val deletedIds = groupedUpdates.flatMap { updates ->
+                val mangaId = updates.first().update.mangaId
+                val manga = getManga.await(mangaId) ?: return@flatMap emptyList()
+                val source = sourceManager.get(manga.source) ?: return@flatMap emptyList()
+                val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
+                downloadManager.deleteChapters(chapters, manga, source).mapNotNull { it.id }
+            }
+            updateSuccessState { successState ->
+                val deletedUpdates = successState.uiModels.filter {
+                    it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId)
+                }
+                if (deletedUpdates.isEmpty()) return@updateSuccessState successState
+
+                // TODO: Don't do this fake status update
+                val newUiModels = successState.uiModels.toMutableList().apply {
+                    deletedUpdates.forEach { deletedUpdate ->
+                        val modifiedIndex = indexOf(deletedUpdate)
+                        var uiModel = removeAt(modifiedIndex)
+                        if (uiModel is UpdatesUiModel.Item) {
+                            val item = uiModel.item.copy(
+                                downloadStateProvider = { Download.State.NOT_DOWNLOADED },
+                                downloadProgressProvider = { 0 },
+                            )
+                            uiModel = UpdatesUiModel.Item(item)
+                        }
+                        add(modifiedIndex, uiModel)
+                    }
+                }
+                successState.copy(uiModels = newUiModels)
             }
         }
     }
 }
+
+sealed class UpdatesState {
+    object Loading : UpdatesState()
+    data class Error(val error: Throwable) : UpdatesState()
+    data class Success(
+        val uiModels: List<UpdatesUiModel>,
+        val isIncognitoMode: Boolean = false,
+        val isDownloadedOnlyMode: Boolean = false,
+        val showSwipeRefreshIndicator: Boolean = false,
+    ) : UpdatesState()
+}
+
+@Immutable
+data class UpdatesItem(
+    val update: UpdatesWithRelations,
+    val downloadStateProvider: () -> Download.State,
+    val downloadProgressProvider: () -> Int,
+)

+ 0 - 40
app/src/main/res/layout/updates_controller.xml

@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/swipe_refresh"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/recycler"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:paddingTop="4dp"
-            android:paddingBottom="@dimen/action_toolbar_list_padding"
-            tools:listitem="@layout/updates_item" />
-
-        <eu.kanade.tachiyomi.widget.MaterialFastScroll
-            android:id="@+id/fast_scroller"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_gravity="end"
-            android:visibility="gone"
-            app:fastScrollerBubbleEnabled="false"
-            tools:visibility="visible" />
-
-        <eu.kanade.tachiyomi.widget.EmptyView
-            android:id="@+id/empty_view"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:visibility="gone" />
-
-    </FrameLayout>
-
-</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>

+ 0 - 78
app/src/main/res/layout/updates_item.xml

@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="56dp"
-    android:background="@drawable/list_item_selector_background"
-    android:paddingStart="16dp"
-    android:paddingEnd="4dp">
-
-    <com.google.android.material.imageview.ShapeableImageView
-        android:id="@+id/manga_cover"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_marginTop="8dp"
-        android:layout_marginBottom="8dp"
-        android:scaleType="centerCrop"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintDimensionRatio="h,1:1"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
-        tools:src="@mipmap/ic_launcher" />
-
-    <TextView
-        android:id="@+id/manga_title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="16dp"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        app:layout_constraintBottom_toTopOf="@+id/chapter_title"
-        app:layout_constraintEnd_toStartOf="@+id/download"
-        app:layout_constraintStart_toEndOf="@+id/manga_cover"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintVertical_chainStyle="packed"
-        tools:text="Manga title" />
-
-    <ImageView
-        android:id="@+id/bookmark_icon"
-        android:layout_width="16dp"
-        android:layout_height="0dp"
-        android:visibility="gone"
-        android:layout_marginEnd="4dp"
-        app:layout_constraintStart_toStartOf="@id/manga_title"
-        app:layout_constraintTop_toBottomOf="@id/manga_title"
-        app:layout_constraintBottom_toBottomOf="@id/chapter_title"
-        app:layout_constraintEnd_toStartOf="@id/chapter_title"
-        app:srcCompat="@drawable/ic_bookmark_24dp"
-        app:tint="?attr/colorAccent"
-        tools:visibility="visible" />
-
-    <TextView
-        android:id="@+id/chapter_title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        android:textColor="?android:attr/textColorSecondary"
-        android:textSize="12sp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@+id/download"
-        app:layout_constraintStart_toEndOf="@id/bookmark_icon"
-        app:layout_constraintTop_toBottomOf="@+id/manga_title"
-        tools:text="Chapter title" />
-
-    <eu.kanade.tachiyomi.ui.manga.chapter.ChapterDownloadView
-        android:id="@+id/download"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingStart="4dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 10
app/src/main/sqldelight/data/mangas.sq

@@ -72,16 +72,6 @@ FROM mangas
 WHERE favorite = 0
 GROUP BY source;
 
-getRecentlyUpdated:
-SELECT *
-FROM mangas M
-JOIN chapters C
-ON M._id = C.manga_id
-WHERE M.favorite = 1
-AND C.date_upload > :after
-AND C.date_fetch > M.date_added
-ORDER BY C.date_upload DESC;
-
 getLibrary:
 SELECT M.*, COALESCE(MC.category_id, 0) AS category
 FROM (

+ 20 - 0
app/src/main/sqldelight/migrations/18.sqm

@@ -0,0 +1,20 @@
+CREATE VIEW updatesView AS
+SELECT
+    mangas._id AS mangaId,
+    mangas.title AS mangaTitle,
+    chapters._id AS chapterId,
+    chapters.name AS chapterName,
+    chapters.scanlator,
+    chapters.read,
+    chapters.bookmark,
+    mangas.source,
+    mangas.favorite,
+    mangas.thumbnail_url AS thumbnailUrl,
+    mangas.cover_last_modified AS coverLastModified,
+    chapters.date_upload AS dateUpload,
+    chapters.date_fetch AS datefetch
+FROM mangas JOIN chapters
+ON mangas._id = chapters.manga_id
+WHERE favorite = 1
+AND date_fetch > date_added
+ORDER BY date_fetch DESC;

+ 25 - 0
app/src/main/sqldelight/view/updatesView.sq

@@ -0,0 +1,25 @@
+CREATE VIEW updatesView AS
+SELECT
+    mangas._id AS mangaId,
+    mangas.title AS mangaTitle,
+    chapters._id AS chapterId,
+    chapters.name AS chapterName,
+    chapters.scanlator,
+    chapters.read,
+    chapters.bookmark,
+    mangas.source,
+    mangas.favorite,
+    mangas.thumbnail_url AS thumbnailUrl,
+    mangas.cover_last_modified AS coverLastModified,
+    chapters.date_upload AS dateUpload,
+    chapters.date_fetch AS datefetch
+FROM mangas JOIN chapters
+ON mangas._id = chapters.manga_id
+WHERE favorite = 1
+AND date_fetch > date_added
+ORDER BY date_fetch DESC;
+
+updates:
+SELECT *
+FROM updatesView
+WHERE dateUpload > :after;