mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							
								
								
									
										16
									
								
								app/src/main/java/eu/kanade/core/util/ListUtils.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/java/eu/kanade/core/util/ListUtils.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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
									
								
								app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()) }
 | 
			
		||||
 
 | 
			
		||||
@@ -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() })
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,
 | 
			
		||||
)
 | 
			
		||||
@@ -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>>
 | 
			
		||||
}
 | 
			
		||||
@@ -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,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,6 @@ enum class DownloadAction {
 | 
			
		||||
    ALL_CHAPTERS
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum class ChapterDownloadAction {
 | 
			
		||||
    START,
 | 
			
		||||
    START_NOW,
 | 
			
		||||
    CANCEL,
 | 
			
		||||
    DELETE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum class EditCoverAction {
 | 
			
		||||
    EDIT,
 | 
			
		||||
    DELETE,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,3 +12,5 @@ val horizontalPadding = horizontal
 | 
			
		||||
val verticalPadding = vertical
 | 
			
		||||
 | 
			
		||||
val topPaddingValues = PaddingValues(top = vertical)
 | 
			
		||||
 | 
			
		||||
const val ReadItemAlpha = .38f
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 {
 | 
			
		||||
    FullComposeController<UpdatesPresenter>(),
 | 
			
		||||
    RootController {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Action mode for multiple selection.
 | 
			
		||||
     */
 | 
			
		||||
    private var actionMode: ActionModeWithToolbar? = null
 | 
			
		||||
    override fun createPresenter() = UpdatesPresenter()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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()
 | 
			
		||||
            }
 | 
			
		||||
    @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 }
 | 
			
		||||
    // Let compose view handle this
 | 
			
		||||
    override fun handleBack(): Boolean {
 | 
			
		||||
        (activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher?.onBackPressed()
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
        }
 | 
			
		||||
    private fun onBackClicked() {
 | 
			
		||||
        (activity as? MainActivity)?.moveToStartScreen()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called to toggle selection
 | 
			
		||||
     * @param position position of selected item
 | 
			
		||||
     */
 | 
			
		||||
    private fun toggleSelection(position: Int) {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        adapter.toggleSelection(position)
 | 
			
		||||
        actionMode?.invalidate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     * @param items 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.
 | 
			
		||||
     */
 | 
			
		||||
    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()
 | 
			
		||||
    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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 }
 | 
			
		||||
    private fun openChapter(item: UpdatesItem) {
 | 
			
		||||
        val activity = activity ?: return
 | 
			
		||||
        val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
        }
 | 
			
		||||
        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 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))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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.
 | 
			
		||||
     *
 | 
			
		||||
     * @param items the list of chapter from the database.
 | 
			
		||||
     */
 | 
			
		||||
    private fun setDownloadedChapters(items: List<UpdatesItem>) {
 | 
			
		||||
        for (item in items) {
 | 
			
		||||
            val manga = item.manga
 | 
			
		||||
            val chapter = item.chapter
 | 
			
		||||
 | 
			
		||||
            if (downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)) {
 | 
			
		||||
                item.status = Download.State.DOWNLOADED
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun startDownloadingNow(chapter: Chapter) {
 | 
			
		||||
        downloadManager.startDownloadNow(chapter.id)
 | 
			
		||||
    fun startDownloadingNow(chapterId: Long) {
 | 
			
		||||
        downloadManager.startDownloadNow(chapterId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mark selected chapters as bookmarked
 | 
			
		||||
     * @param items list of selected chapters
 | 
			
		||||
     * @param bookmarked bookmark status
 | 
			
		||||
     */
 | 
			
		||||
    fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
 | 
			
		||||
    fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
 | 
			
		||||
        presenterScope.launchIO {
 | 
			
		||||
            val toUpdate = items.map {
 | 
			
		||||
                ChapterUpdate(
 | 
			
		||||
                    bookmark = bookmarked,
 | 
			
		||||
                    id = it.chapter.id,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            updateChapter.awaitAll(toUpdate)
 | 
			
		||||
            updates
 | 
			
		||||
                .filterNot { it.update.bookmark == bookmark }
 | 
			
		||||
                .map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
 | 
			
		||||
                .let { updateChapter.awaitAll(it) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download selected chapters
 | 
			
		||||
     * @param items list of recent chapters seleted.
 | 
			
		||||
     * Downloads the given list of chapters with the manager.
 | 
			
		||||
     * @param updatesItem the list of chapters to download.
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadChapters(items: List<UpdatesItem>) {
 | 
			
		||||
        items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter.toDbChapter())) }
 | 
			
		||||
    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)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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() }
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
            downloadManager.deleteChapters(chapters, manga, source)
 | 
			
		||||
            items.forEach {
 | 
			
		||||
                it.status = Download.State.NOT_DOWNLOADED
 | 
			
		||||
                it.download = null
 | 
			
		||||
                // 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,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user