mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 15:41:52 +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:
parent
bdc5d557d1
commit
d8fb6b893f
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.SourceDataRepositoryImpl
|
||||||
import eu.kanade.data.source.SourceRepositoryImpl
|
import eu.kanade.data.source.SourceRepositoryImpl
|
||||||
import eu.kanade.data.track.TrackRepositoryImpl
|
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.CreateCategoryWithName
|
||||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||||
import eu.kanade.domain.category.interactor.GetCategories
|
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.GetTracks
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
import eu.kanade.domain.track.interactor.InsertTrack
|
||||||
import eu.kanade.domain.track.repository.TrackRepository
|
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.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addFactory
|
import uy.kohesive.injekt.api.addFactory
|
||||||
@ -119,6 +122,9 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetExtensionUpdates(get(), get()) }
|
addFactory { GetExtensionUpdates(get(), get()) }
|
||||||
addFactory { GetExtensionLanguages(get(), get()) }
|
addFactory { GetExtensionLanguages(get(), get()) }
|
||||||
|
|
||||||
|
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||||
|
addFactory { GetUpdates(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||||
addFactory { GetEnabledSources(get(), 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.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
|
|
||||||
|
enum class ChapterDownloadAction {
|
||||||
|
START,
|
||||||
|
START_NOW,
|
||||||
|
CANCEL,
|
||||||
|
DELETE,
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterDownloadIndicator(
|
fun ChapterDownloadIndicator(
|
||||||
modifier: Modifier = Modifier,
|
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.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@ -51,13 +51,13 @@ import kotlinx.coroutines.launch
|
|||||||
fun MangaBottomActionMenu(
|
fun MangaBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBookmarkClicked: (() -> Unit)?,
|
onBookmarkClicked: (() -> Unit)? = null,
|
||||||
onRemoveBookmarkClicked: (() -> Unit)?,
|
onRemoveBookmarkClicked: (() -> Unit)? = null,
|
||||||
onMarkAsReadClicked: (() -> Unit)?,
|
onMarkAsReadClicked: (() -> Unit)? = null,
|
||||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
onMarkAsUnreadClicked: (() -> Unit)? = null,
|
||||||
onMarkPreviousAsReadClicked: (() -> Unit)?,
|
onMarkPreviousAsReadClicked: (() -> Unit)? = null,
|
||||||
onDownloadClicked: (() -> Unit)?,
|
onDownloadClicked: (() -> Unit)? = null,
|
||||||
onDeleteClicked: (() -> Unit)?,
|
onDeleteClicked: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -15,7 +15,7 @@ import java.text.DateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryHeader(
|
fun RelativeDateHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
date: Date,
|
date: Date,
|
||||||
relativeTime: Int,
|
relativeTime: Int,
|
@ -39,8 +39,8 @@ import androidx.paging.compose.items
|
|||||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.presentation.components.RelativeDateHeader
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
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.HistoryItem
|
||||||
import eu.kanade.presentation.history.components.HistoryItemShimmer
|
import eu.kanade.presentation.history.components.HistoryItemShimmer
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
@ -108,7 +108,7 @@ fun HistoryContent(
|
|||||||
items(history) { item ->
|
items(history) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is HistoryUiModel.Header -> {
|
is HistoryUiModel.Header -> {
|
||||||
HistoryHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement(),
|
.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
|
@ -52,15 +52,16 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
import eu.kanade.domain.chapter.model.Chapter
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
import eu.kanade.presentation.components.LazyColumn
|
||||||
|
import eu.kanade.presentation.components.MangaBottomActionMenu
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||||
import eu.kanade.presentation.components.VerticalFastScroller
|
import eu.kanade.presentation.components.VerticalFastScroller
|
||||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||||
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
|
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
|
||||||
import eu.kanade.presentation.manga.components.MangaActionRow
|
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.MangaChapterListItem
|
||||||
import eu.kanade.presentation.manga.components.MangaInfoBox
|
import eu.kanade.presentation.manga.components.MangaInfoBox
|
||||||
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
||||||
|
@ -9,13 +9,6 @@ enum class DownloadAction {
|
|||||||
ALL_CHAPTERS
|
ALL_CHAPTERS
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ChapterDownloadAction {
|
|
||||||
START,
|
|
||||||
START_NOW,
|
|
||||||
CANCEL,
|
|
||||||
DELETE,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class EditCoverAction {
|
enum class EditCoverAction {
|
||||||
EDIT,
|
EDIT,
|
||||||
DELETE,
|
DELETE,
|
||||||
|
@ -29,8 +29,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
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
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
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.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
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.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.DropdownMenu
|
||||||
|
import eu.kanade.presentation.components.IncognitoModeBanner
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
@ -210,28 +206,10 @@ fun MangaSmallAppBar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (downloadedOnlyMode) {
|
if (downloadedOnlyMode) {
|
||||||
Text(
|
DownloadedOnlyModeBanner()
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (incognitoMode) {
|
if (incognitoMode) {
|
||||||
Text(
|
IncognitoModeBanner()
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 verticalPadding = vertical
|
||||||
|
|
||||||
val topPaddingValues = PaddingValues(top = vertical)
|
val topPaddingValues = PaddingValues(top = vertical)
|
||||||
|
|
||||||
|
const val ReadItemAlpha = .38f
|
||||||
|
@ -27,3 +27,21 @@ fun LazyListState.isScrollingUp(): Boolean {
|
|||||||
}
|
}
|
||||||
}.value
|
}.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()) {
|
if (!router.hasRootController()) {
|
||||||
// Set start screen
|
// Set start screen
|
||||||
if (!handleIntentAction(intent)) {
|
if (!handleIntentAction(intent)) {
|
||||||
setSelectedNavItem(startScreenId)
|
moveToStartScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncActivityViewWithController()
|
syncActivityViewWithController()
|
||||||
@ -483,10 +483,15 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
// Updates screen has custom back handler
|
||||||
|
if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
|
||||||
|
router.handleBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
val backstackSize = router.backstackSize
|
val backstackSize = router.backstackSize
|
||||||
if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
||||||
// Return to start screen
|
// Return to start screen
|
||||||
setSelectedNavItem(startScreenId)
|
moveToStartScreen()
|
||||||
} else if (shouldHandleExitConfirmation()) {
|
} else if (shouldHandleExitConfirmation()) {
|
||||||
// Exit confirmation (resets after 2 seconds)
|
// Exit confirmation (resets after 2 seconds)
|
||||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||||
@ -499,6 +504,10 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun moveToStartScreen() {
|
||||||
|
setSelectedNavItem(startScreenId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
binding.appbar.apply {
|
binding.appbar.apply {
|
||||||
tag = isTransparentWhenNotLifted
|
tag = isTransparentWhenNotLifted
|
||||||
|
@ -27,7 +27,7 @@ import eu.kanade.data.chapter.NoChaptersException
|
|||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
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.DownloadAction
|
||||||
import eu.kanade.presentation.manga.MangaScreen
|
import eu.kanade.presentation.manga.MangaScreen
|
||||||
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
||||||
|
@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.AbstractComposeView
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
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 android.view.View
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
|
|
||||||
open class BaseChapterHolder(
|
open class BaseChapterHolder(
|
||||||
view: View,
|
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
|
package eu.kanade.tachiyomi.ui.recent.updates
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import androidx.activity.OnBackPressedDispatcherOwner
|
||||||
import android.view.Menu
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.MenuInflater
|
import androidx.compose.material3.Text
|
||||||
import android.view.MenuItem
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.View
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.view.isVisible
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.kanade.presentation.updates.UpdateScreen
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.kanade.presentation.util.NavBarVisibility
|
||||||
|
import eu.kanade.presentation.util.toBoolean
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||||
import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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.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.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
||||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
|
||||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows recent chapters.
|
* Fragment that shows recent chapters.
|
||||||
*/
|
*/
|
||||||
class UpdatesController :
|
class UpdatesController :
|
||||||
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
|
FullComposeController<UpdatesPresenter>(),
|
||||||
RootController,
|
RootController {
|
||||||
ActionModeWithToolbar.Callback,
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
FlexibleAdapter.OnUpdateListener,
|
|
||||||
BaseChaptersAdapter.OnChapterClickListener,
|
|
||||||
ConfirmDeleteChaptersDialog.Listener,
|
|
||||||
UpdatesAdapter.OnCoverClickListener {
|
|
||||||
|
|
||||||
/**
|
override fun createPresenter() = UpdatesPresenter()
|
||||||
* Action mode for multiple selection.
|
|
||||||
*/
|
|
||||||
private var actionMode: ActionModeWithToolbar? = null
|
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* Adapter containing the recent chapters.
|
override fun ComposeContent() {
|
||||||
*/
|
val state by presenter.state.collectAsState()
|
||||||
var adapter: UpdatesAdapter? = null
|
when (state) {
|
||||||
private set
|
is UpdatesState.Loading -> LoadingScreen()
|
||||||
|
is UpdatesState.Error -> Text(text = (state as UpdatesState.Error).error.message.orEmpty())
|
||||||
init {
|
is UpdatesState.Success ->
|
||||||
setHasOptionsMenu(true)
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
private fun updateLibrary() {
|
||||||
@ -154,262 +70,67 @@ class UpdatesController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Let compose view handle this
|
||||||
* Returns selected chapters
|
override fun handleBack(): Boolean {
|
||||||
* @return list of selected chapters
|
(activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher?.onBackPressed()
|
||||||
*/
|
return true
|
||||||
private fun getSelectedChapters(): List<UpdatesItem> {
|
|
||||||
val adapter = adapter ?: return emptyList()
|
|
||||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? UpdatesItem }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun onBackClicked() {
|
||||||
* Called when item in list is clicked
|
(activity as? MainActivity)?.moveToStartScreen()
|
||||||
* @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 toggleNavBarVisibility(navBarVisibility: NavBarVisibility) {
|
||||||
* Called when item in list is long clicked
|
val showNavBar = navBarVisibility.toBoolean()
|
||||||
* @param position position of clicked item
|
(activity as? MainActivity)?.showBottomNav(showNavBar)
|
||||||
*/
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download selected items
|
* Download selected items
|
||||||
* @param chapters list of selected [UpdatesItem]s
|
* @param items list of selected [UpdatesItem]s
|
||||||
*/
|
*/
|
||||||
private fun downloadChapters(chapters: List<UpdatesItem>) {
|
private fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||||
presenter.downloadChapters(chapters)
|
if (items.isEmpty()) return
|
||||||
destroyActionModeIfNeeded()
|
viewScope.launch {
|
||||||
}
|
when (action) {
|
||||||
|
ChapterDownloadAction.START -> {
|
||||||
override fun onUpdateEmptyView(size: Int) {
|
presenter.downloadChapters(items)
|
||||||
if (size > 0) {
|
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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!!)
|
DownloadService.start(activity!!)
|
||||||
} else {
|
|
||||||
downloadChapters(listOf(item))
|
|
||||||
}
|
}
|
||||||
adapter?.updateItem(item)
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteChapter(position: Int) {
|
private fun deleteChaptersWithConfirmation(items: List<UpdatesItem>) {
|
||||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
if (items.isEmpty()) return
|
||||||
deleteChapters(listOf(item))
|
viewScope.launch {
|
||||||
adapter?.updateItem(item)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startDownloadNow(position: Int) {
|
private fun openChapter(item: UpdatesItem) {
|
||||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
val activity = activity ?: return
|
||||||
presenter.startDownloadingNow(item.chapter)
|
val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
|
private fun openManga(item: UpdatesItem) {
|
||||||
presenter.bookmarkChapters(chapters, bookmarked)
|
router.pushController(MangaController(item.update.mangaId))
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when ActionMode created.
|
|
||||||
* @param mode the ActionMode object
|
|
||||||
* @param menu menu object of ActionMode
|
|
||||||
*/
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
|
||||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
|
|
||||||
menuInflater.inflate(R.menu.updates_chapter_selection, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
val count = adapter?.selectedItemCount ?: 0
|
|
||||||
if (count == 0) {
|
|
||||||
// Destroy action mode if there are no items selected.
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
} else {
|
|
||||||
mode.title = count.toString()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
|
||||||
val chapters = getSelectedChapters()
|
|
||||||
if (chapters.isEmpty()) return
|
|
||||||
toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
|
|
||||||
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
|
|
||||||
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark }
|
|
||||||
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark }
|
|
||||||
toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
|
|
||||||
toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when ActionMode item clicked
|
|
||||||
* @param mode the ActionMode object
|
|
||||||
* @param item item from ActionMode.
|
|
||||||
*/
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
return onActionItemClicked(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onActionItemClicked(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_select_all -> selectAll()
|
|
||||||
R.id.action_select_inverse -> selectInverse()
|
|
||||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
|
||||||
R.id.action_delete ->
|
|
||||||
ConfirmDeleteChaptersDialog(this, getSelectedChapters())
|
|
||||||
.showDialog(router)
|
|
||||||
R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true)
|
|
||||||
R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false)
|
|
||||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
|
||||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,134 +1,177 @@
|
|||||||
package eu.kanade.tachiyomi.ui.recent.updates
|
package eu.kanade.tachiyomi.ui.recent.updates
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.data.DatabaseHandler
|
import androidx.compose.runtime.Immutable
|
||||||
import eu.kanade.data.manga.mangaChapterMapper
|
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.SetReadStatus
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
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.ChapterUpdate
|
||||||
import eu.kanade.domain.chapter.model.toDbChapter
|
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.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
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.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import eu.kanade.tachiyomi.util.preference.asHotFlow
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeMap
|
|
||||||
|
|
||||||
class UpdatesPresenter(
|
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 updateChapter: UpdateChapter = Injekt.get(),
|
||||||
private val setReadStatus: SetReadStatus = 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>() {
|
) : BasePresenter<UpdatesController>() {
|
||||||
|
|
||||||
private val relativeTime: Int = preferences.relativeTime().get()
|
private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading)
|
||||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
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?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
subscribeToUpdates()
|
|
||||||
|
|
||||||
downloadManager.queue.getStatusAsFlow()
|
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
|
||||||
.collectLatest {
|
|
||||||
withUIContext {
|
|
||||||
onDownloadStatusChange(it)
|
|
||||||
view?.onChapterDownloadUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadManager.queue.getProgressAsFlow()
|
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
|
||||||
.collectLatest {
|
|
||||||
withUIContext {
|
|
||||||
view?.onChapterDownloadUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get observable containing recent chapters and date
|
|
||||||
*/
|
|
||||||
private suspend fun subscribeToUpdates() {
|
|
||||||
// Set date limit for recent chapters
|
// Set date limit for recent chapters
|
||||||
val cal = Calendar.getInstance().apply {
|
val calendar = Calendar.getInstance().apply {
|
||||||
time = Date()
|
time = Date()
|
||||||
add(Calendar.MONTH, -3)
|
add(Calendar.MONTH, -3)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler
|
getUpdates.subscribe(calendar)
|
||||||
.subscribeToList {
|
.catch { exception ->
|
||||||
mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper)
|
_state.value = UpdatesState.Error(exception)
|
||||||
}
|
}
|
||||||
.map { mangaChapter ->
|
.collectLatest { updates ->
|
||||||
val map = TreeMap<Date, MutableList<Pair<Manga, Chapter>>> { d1, d2 -> d2.compareTo(d1) }
|
val uiModels = updates.toUpdateUiModels()
|
||||||
val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() }
|
_state.update { currentState ->
|
||||||
byDate.flatMap { entry ->
|
when (currentState) {
|
||||||
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
|
is UpdatesState.Success -> currentState.copy(uiModels)
|
||||||
entry.value
|
is UpdatesState.Loading, is UpdatesState.Error ->
|
||||||
.sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed()
|
UpdatesState.Success(
|
||||||
.map { UpdatesItem(it.second, it.first, dateItem) }
|
uiModels = uiModels,
|
||||||
}
|
isIncognitoMode = incognitoMode,
|
||||||
}
|
isDownloadedOnlyMode = downloadOnlyMode,
|
||||||
.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 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
observeDownloads()
|
||||||
* 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)) {
|
preferences.incognitoMode()
|
||||||
item.status = Download.State.DOWNLOADED
|
.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 {
|
||||||
|
updateDownloadState(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observeDownloadsPageJob?.cancel()
|
||||||
|
observeDownloadsPageJob = presenterScope.launchIO {
|
||||||
|
downloadManager.queue.getProgressAsFlow()
|
||||||
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
|
.collectLatest {
|
||||||
|
withUIContext {
|
||||||
|
updateDownloadState(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,96 +181,141 @@ class UpdatesPresenter(
|
|||||||
*
|
*
|
||||||
* @param download download object containing progress.
|
* @param download download object containing progress.
|
||||||
*/
|
*/
|
||||||
private fun onDownloadStatusChange(download: Download) {
|
private fun updateDownloadState(download: Download) {
|
||||||
// Assign the download to the model object.
|
updateSuccessState { successState ->
|
||||||
if (download.status == Download.State.QUEUE) {
|
val modifiedIndex = successState.uiModels.indexOfFirst {
|
||||||
val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<UpdatesItem>()
|
it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id
|
||||||
val chapter = chapters.find { it.chapter.id == download.chapter.id }
|
|
||||||
if (chapter != null && chapter.download == null) {
|
|
||||||
chapter.download = download
|
|
||||||
}
|
}
|
||||||
|
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) {
|
fun startDownloadingNow(chapterId: Long) {
|
||||||
downloadManager.startDownloadNow(chapter.id)
|
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
|
* Mark the selected updates list as read/unread.
|
||||||
*
|
* @param updates the list of selected updates.
|
||||||
* @param items list of selected chapters
|
* @param read whether to mark chapters as read or unread.
|
||||||
* @param read read status
|
|
||||||
*/
|
*/
|
||||||
fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
|
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
setReadStatus.await(
|
setReadStatus.await(
|
||||||
read = read,
|
read = read,
|
||||||
values = items
|
values = updates
|
||||||
.map { it.chapter }
|
.mapNotNull { getChapter.await(it.update.chapterId) }
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete selected chapters
|
* Bookmarks the given list of chapters.
|
||||||
*
|
* @param updates the list of chapters to bookmark.
|
||||||
* @param chapters list of chapters
|
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<UpdatesItem>) {
|
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||||
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) {
|
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
val toUpdate = items.map {
|
updates
|
||||||
ChapterUpdate(
|
.filterNot { it.update.bookmark == bookmark }
|
||||||
bookmark = bookmarked,
|
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||||
id = it.chapter.id,
|
.let { updateChapter.awaitAll(it) }
|
||||||
)
|
|
||||||
}
|
|
||||||
updateChapter.awaitAll(toUpdate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download selected chapters
|
* Downloads the given list of chapters with the manager.
|
||||||
* @param items list of recent chapters seleted.
|
* @param updatesItem the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
fun downloadChapters(items: List<UpdatesItem>) {
|
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||||
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter.toDbChapter())) }
|
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
|
* Delete selected chapters
|
||||||
*
|
*
|
||||||
* @param items chapters selected
|
* @param updatesItem list of chapters
|
||||||
*/
|
*/
|
||||||
private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
|
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||||
val itemsByManga = chapterItems.groupBy { it.manga.id }
|
launchIO {
|
||||||
for ((_, items) in itemsByManga) {
|
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||||
val manga = items.first().manga
|
val deletedIds = groupedUpdates.flatMap { updates ->
|
||||||
val source = sourceManager.get(manga.source) ?: continue
|
val mangaId = updates.first().update.mangaId
|
||||||
val chapters = items.map { it.chapter.toDbChapter() }
|
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)
|
// TODO: Don't do this fake status update
|
||||||
items.forEach {
|
val newUiModels = successState.uiModels.toMutableList().apply {
|
||||||
it.status = Download.State.NOT_DOWNLOADED
|
deletedUpdates.forEach { deletedUpdate ->
|
||||||
it.download = null
|
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,
|
||||||
|
)
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/swipe_refresh"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="4dp"
|
|
||||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
|
||||||
tools:listitem="@layout/updates_item" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
|
||||||
android:id="@+id/fast_scroller"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:fastScrollerBubbleEnabled="false"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.EmptyView
|
|
||||||
android:id="@+id/empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
|
|
@ -1,78 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:background="@drawable/list_item_selector_background"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingEnd="4dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/manga_cover"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintDimensionRatio="h,1:1"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
|
|
||||||
tools:src="@mipmap/ic_launcher" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/chapter_title"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/download"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/manga_cover"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
|
||||||
tools:text="Manga title" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/bookmark_icon"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/manga_title"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/manga_title"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/chapter_title"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/chapter_title"
|
|
||||||
app:srcCompat="@drawable/ic_bookmark_24dp"
|
|
||||||
app:tint="?attr/colorAccent"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/chapter_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/download"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/bookmark_icon"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/manga_title"
|
|
||||||
tools:text="Chapter title" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.ui.manga.chapter.ChapterDownloadView
|
|
||||||
android:id="@+id/download"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="4dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -72,16 +72,6 @@ FROM mangas
|
|||||||
WHERE favorite = 0
|
WHERE favorite = 0
|
||||||
GROUP BY source;
|
GROUP BY source;
|
||||||
|
|
||||||
getRecentlyUpdated:
|
|
||||||
SELECT *
|
|
||||||
FROM mangas M
|
|
||||||
JOIN chapters C
|
|
||||||
ON M._id = C.manga_id
|
|
||||||
WHERE M.favorite = 1
|
|
||||||
AND C.date_upload > :after
|
|
||||||
AND C.date_fetch > M.date_added
|
|
||||||
ORDER BY C.date_upload DESC;
|
|
||||||
|
|
||||||
getLibrary:
|
getLibrary:
|
||||||
SELECT M.*, COALESCE(MC.category_id, 0) AS category
|
SELECT M.*, COALESCE(MC.category_id, 0) AS category
|
||||||
FROM (
|
FROM (
|
||||||
|
20
app/src/main/sqldelight/migrations/18.sqm
Normal file
20
app/src/main/sqldelight/migrations/18.sqm
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE VIEW updatesView AS
|
||||||
|
SELECT
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
chapters.name AS chapterName,
|
||||||
|
chapters.scanlator,
|
||||||
|
chapters.read,
|
||||||
|
chapters.bookmark,
|
||||||
|
mangas.source,
|
||||||
|
mangas.favorite,
|
||||||
|
mangas.thumbnail_url AS thumbnailUrl,
|
||||||
|
mangas.cover_last_modified AS coverLastModified,
|
||||||
|
chapters.date_upload AS dateUpload,
|
||||||
|
chapters.date_fetch AS datefetch
|
||||||
|
FROM mangas JOIN chapters
|
||||||
|
ON mangas._id = chapters.manga_id
|
||||||
|
WHERE favorite = 1
|
||||||
|
AND date_fetch > date_added
|
||||||
|
ORDER BY date_fetch DESC;
|
25
app/src/main/sqldelight/view/updatesView.sq
Normal file
25
app/src/main/sqldelight/view/updatesView.sq
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
CREATE VIEW updatesView AS
|
||||||
|
SELECT
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
chapters.name AS chapterName,
|
||||||
|
chapters.scanlator,
|
||||||
|
chapters.read,
|
||||||
|
chapters.bookmark,
|
||||||
|
mangas.source,
|
||||||
|
mangas.favorite,
|
||||||
|
mangas.thumbnail_url AS thumbnailUrl,
|
||||||
|
mangas.cover_last_modified AS coverLastModified,
|
||||||
|
chapters.date_upload AS dateUpload,
|
||||||
|
chapters.date_fetch AS datefetch
|
||||||
|
FROM mangas JOIN chapters
|
||||||
|
ON mangas._id = chapters.manga_id
|
||||||
|
WHERE favorite = 1
|
||||||
|
AND date_fetch > date_added
|
||||||
|
ORDER BY date_fetch DESC;
|
||||||
|
|
||||||
|
updates:
|
||||||
|
SELECT *
|
||||||
|
FROM updatesView
|
||||||
|
WHERE dateUpload > :after;
|
Loading…
Reference in New Issue
Block a user