mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 11:01: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.SourceRepositoryImpl
|
||||
import eu.kanade.data.track.TrackRepositoryImpl
|
||||
import eu.kanade.data.updates.UpdatesRepositoryImpl
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
@ -60,6 +61,8 @@ import eu.kanade.domain.track.interactor.DeleteTrack
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
@ -119,6 +122,9 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetExtensionUpdates(get(), get()) }
|
||||
addFactory { GetExtensionLanguages(get(), get()) }
|
||||
|
||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||
addFactory { GetUpdates(get(), get()) }
|
||||
|
||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||
addFactory { GetEnabledSources(get(), get()) }
|
||||
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.updates.interactor
|
||||
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.Calendar
|
||||
|
||||
class GetUpdates(
|
||||
private val repository: UpdatesRepository,
|
||||
private val preferences: PreferencesHelper,
|
||||
) {
|
||||
|
||||
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
|
||||
|
||||
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(after)
|
||||
.onEach { updates ->
|
||||
// Set unread chapter count for bottom bar badge
|
||||
preferences.unreadUpdatesCount().set(updates.count { it.read.not() })
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package eu.kanade.domain.updates.model
|
||||
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
|
||||
data class UpdatesWithRelations(
|
||||
val mangaId: Long,
|
||||
val mangaTitle: String,
|
||||
val chapterId: Long,
|
||||
val chapterName: String,
|
||||
val scanlator: String?,
|
||||
val read: Boolean,
|
||||
val bookmark: Boolean,
|
||||
val sourceId: Long,
|
||||
val dateFetch: Long,
|
||||
val coverData: MangaCover,
|
||||
)
|
@ -0,0 +1,9 @@
|
||||
package eu.kanade.domain.updates.repository
|
||||
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UpdatesRepository {
|
||||
|
||||
fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>>
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun DownloadedOnlyModeBanner() {
|
||||
Text(
|
||||
text = stringResource(R.string.label_downloaded_only),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoModeBanner() {
|
||||
Text(
|
||||
text = stringResource(R.string.pref_incognito_mode),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
@ -27,11 +27,17 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
enum class ChapterDownloadAction {
|
||||
START,
|
||||
START_NOW,
|
||||
CANCEL,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChapterDownloadIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@ -51,13 +51,13 @@ import kotlinx.coroutines.launch
|
||||
fun MangaBottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBookmarkClicked: (() -> Unit)?,
|
||||
onRemoveBookmarkClicked: (() -> Unit)?,
|
||||
onMarkAsReadClicked: (() -> Unit)?,
|
||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
||||
onMarkPreviousAsReadClicked: (() -> Unit)?,
|
||||
onDownloadClicked: (() -> Unit)?,
|
||||
onDeleteClicked: (() -> Unit)?,
|
||||
onBookmarkClicked: (() -> Unit)? = null,
|
||||
onRemoveBookmarkClicked: (() -> Unit)? = null,
|
||||
onMarkAsReadClicked: (() -> Unit)? = null,
|
||||
onMarkAsUnreadClicked: (() -> Unit)? = null,
|
||||
onMarkPreviousAsReadClicked: (() -> Unit)? = null,
|
||||
onDownloadClicked: (() -> Unit)? = null,
|
||||
onDeleteClicked: (() -> Unit)? = null,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.presentation.history.components
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -15,7 +15,7 @@ import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun HistoryHeader(
|
||||
fun RelativeDateHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
date: Date,
|
||||
relativeTime: Int,
|
@ -39,8 +39,8 @@ import androidx.paging.compose.items
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.history.components.HistoryHeader
|
||||
import eu.kanade.presentation.history.components.HistoryItem
|
||||
import eu.kanade.presentation.history.components.HistoryItemShimmer
|
||||
import eu.kanade.presentation.util.plus
|
||||
@ -108,7 +108,7 @@ fun HistoryContent(
|
||||
items(history) { item ->
|
||||
when (item) {
|
||||
is HistoryUiModel.Header -> {
|
||||
HistoryHeader(
|
||||
RelativeDateHeader(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement(),
|
||||
date = item.date,
|
||||
|
@ -52,15 +52,16 @@ import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.MangaBottomActionMenu
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.components.VerticalFastScroller
|
||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
|
||||
import eu.kanade.presentation.manga.components.MangaActionRow
|
||||
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
|
||||
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
||||
import eu.kanade.presentation.manga.components.MangaInfoBox
|
||||
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
||||
|
@ -9,13 +9,6 @@ enum class DownloadAction {
|
||||
ALL_CHAPTERS
|
||||
}
|
||||
|
||||
enum class ChapterDownloadAction {
|
||||
START,
|
||||
START_NOW,
|
||||
CANCEL,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
EDIT,
|
||||
DELETE,
|
||||
|
@ -29,8 +29,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.util.ReadItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
@ -134,5 +135,3 @@ fun MangaChapterListItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ReadItemAlpha = .38f
|
||||
|
@ -1,13 +1,10 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -21,7 +18,6 @@ import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@ -34,10 +30,10 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.components.IncognitoModeBanner
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@ -210,28 +206,10 @@ fun MangaSmallAppBar(
|
||||
)
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_downloaded_only),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
DownloadedOnlyModeBanner()
|
||||
}
|
||||
if (incognitoMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.pref_incognito_mode),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
IncognitoModeBanner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,315 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.IncognitoModeBanner
|
||||
import eu.kanade.presentation.components.MangaBottomActionMenu
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.components.VerticalFastScroller
|
||||
import eu.kanade.presentation.util.NavBarVisibility
|
||||
import eu.kanade.presentation.util.isScrollingDown
|
||||
import eu.kanade.presentation.util.isScrollingUp
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
|
||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun UpdateScreen(
|
||||
state: UpdatesState.Success,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
toggleNavBarVisibility: (NavBarVisibility) -> Unit,
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
// Miscellaneous
|
||||
preferences: PreferencesHelper = Injekt.get(),
|
||||
) {
|
||||
val updatesListState = rememberLazyListState()
|
||||
val insetPaddingValue = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
|
||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
||||
val dateFormat: DateFormat = remember { preferences.dateFormat() }
|
||||
|
||||
val uiModels = remember(state) {
|
||||
state.uiModels
|
||||
}
|
||||
val itemUiModels = remember(uiModels) {
|
||||
uiModels.filterIsInstance<UpdatesUiModel.Item>()
|
||||
}
|
||||
// To prevent selection from getting removed during an update to a item in list
|
||||
val updateIdList = remember(itemUiModels) {
|
||||
itemUiModels.map { it.item.update.chapterId }
|
||||
}
|
||||
val selected = remember(updateIdList) {
|
||||
emptyList<UpdatesUiModel.Item>().toMutableStateList()
|
||||
}
|
||||
// First and last selected index in list
|
||||
val selectedPositions = remember(uiModels) { arrayOf(-1, -1) }
|
||||
|
||||
when {
|
||||
selected.isEmpty() &&
|
||||
updatesListState.isScrollingUp() -> toggleNavBarVisibility(NavBarVisibility.SHOW)
|
||||
selected.isNotEmpty() ||
|
||||
updatesListState.isScrollingDown() -> toggleNavBarVisibility(NavBarVisibility.HIDE)
|
||||
}
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.clear()
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.padding(insetPaddingValue),
|
||||
topBar = {
|
||||
UpdatesAppBar(
|
||||
selected = selected,
|
||||
incognitoMode = state.isIncognitoMode,
|
||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||
onUpdateLibrary = onUpdateLibrary,
|
||||
actionModeCounter = selected.size,
|
||||
onSelectAll = {
|
||||
selected.clear()
|
||||
selected.addAll(itemUiModels)
|
||||
},
|
||||
onInvertSelection = {
|
||||
val toSelect = itemUiModels - selected
|
||||
selected.clear()
|
||||
selected.addAll(toSelect)
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
UpdatesBottomBar(
|
||||
selected = selected,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val contentPaddingWithNavBar = contentPadding +
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(state.showSwipeRefreshIndicator),
|
||||
onRefresh = onUpdateLibrary,
|
||||
indicatorPadding = contentPaddingWithNavBar,
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
if (uiModels.isEmpty()) {
|
||||
EmptyScreen(textResource = R.string.information_no_recent)
|
||||
} else {
|
||||
VerticalFastScroller(
|
||||
listState = updatesListState,
|
||||
topContentPadding = contentPaddingWithNavBar.calculateTopPadding(),
|
||||
endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = updatesListState,
|
||||
contentPadding = contentPaddingWithNavBar,
|
||||
) {
|
||||
updatesUiItems(
|
||||
uiModels = uiModels,
|
||||
itemUiModels = itemUiModels,
|
||||
selected = selected,
|
||||
selectedPositions = selectedPositions,
|
||||
onClickCover = onClickCover,
|
||||
onClickUpdate = onClickUpdate,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdatesAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: MutableList<UpdatesUiModel.Item>,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
val backgroundColor = if (isActionMode) {
|
||||
TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f).value
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.drawBehind { drawRect(backgroundColor) },
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
navigationIcon = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = { selected.clear() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = R.string.action_cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else stringResource(R.string.label_recent_updates),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
contentDescription = stringResource(R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = onUpdateLibrary) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = stringResource(R.string.action_update_library),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// Background handled by parent
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
DownloadedOnlyModeBanner()
|
||||
}
|
||||
if (incognitoMode) {
|
||||
IncognitoModeBanner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdatesBottomBar(
|
||||
selected: MutableList<UpdatesUiModel.Item>,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
) {
|
||||
MangaBottomActionMenu(
|
||||
visible = selected.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.item }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.item.update.bookmark } },
|
||||
onRemoveBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.item }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.all { it.item.update.bookmark } },
|
||||
onMarkAsReadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.item }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.item.update.read } },
|
||||
onMarkAsUnreadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.item }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { it.item.update.read } },
|
||||
onDownloadClicked = {
|
||||
onDownloadChapter(selected.map { it.item }, ChapterDownloadAction.START)
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
selected.any { it.item.downloadStateProvider() != Download.State.DOWNLOADED }
|
||||
},
|
||||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.map { it.item })
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { it.item.downloadStateProvider() == Download.State.DOWNLOADED } },
|
||||
)
|
||||
}
|
||||
|
||||
sealed class UpdatesUiModel {
|
||||
data class Header(val date: Date) : UpdatesUiModel()
|
||||
data class Item(val item: UpdatesItem) : UpdatesUiModel()
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.util.ReadItemAlpha
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
|
||||
import java.text.DateFormat
|
||||
|
||||
fun LazyListScope.updatesUiItems(
|
||||
uiModels: List<UpdatesUiModel>,
|
||||
itemUiModels: List<UpdatesUiModel.Item>,
|
||||
selected: MutableList<UpdatesUiModel.Item>,
|
||||
selectedPositions: Array<Int>,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
relativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
) {
|
||||
items(
|
||||
items = uiModels,
|
||||
contentType = {
|
||||
when (it) {
|
||||
is UpdatesUiModel.Header -> "header"
|
||||
is UpdatesUiModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
key = {
|
||||
when (it) {
|
||||
is UpdatesUiModel.Header -> it.hashCode()
|
||||
is UpdatesUiModel.Item -> it.item.update.chapterId
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is UpdatesUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
is UpdatesUiModel.Item -> {
|
||||
val value = item.item
|
||||
val update = value.update
|
||||
UpdatesUiItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
update = update,
|
||||
selected = selected.contains(item),
|
||||
onClick = {
|
||||
onUpdatesItemClick(
|
||||
updatesItem = item,
|
||||
selected = selected,
|
||||
updates = itemUiModels,
|
||||
selectedPositions = selectedPositions,
|
||||
onUpdateClicked = onClickUpdate,
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
onUpdatesItemLongClick(
|
||||
updatesItem = item,
|
||||
selected = selected,
|
||||
updates = itemUiModels,
|
||||
selectedPositions = selectedPositions,
|
||||
)
|
||||
},
|
||||
onClickCover = { if (selected.size == 0) onClickCover(value) },
|
||||
onDownloadChapter = {
|
||||
if (selected.size == 0) onDownloadChapter(listOf(value), it)
|
||||
},
|
||||
downloadStateProvider = value.downloadStateProvider,
|
||||
downloadProgressProvider = value.downloadProgressProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdatesUiItem(
|
||||
modifier: Modifier,
|
||||
update: UpdatesWithRelations,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onClickCover: () -> Unit,
|
||||
onDownloadChapter: (ChapterDownloadAction) -> Unit,
|
||||
// Download Indicator
|
||||
downloadStateProvider: () -> Download.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.height(56.dp)
|
||||
.padding(horizontal = horizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MangaCover.Square(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.fillMaxHeight(),
|
||||
data = update.coverData,
|
||||
onClick = onClickCover,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.weight(1f),
|
||||
) {
|
||||
val bookmark = remember(update.bookmark) { update.bookmark }
|
||||
val read = remember(update.read) { update.read }
|
||||
|
||||
val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f }
|
||||
|
||||
val secondaryTextColor = if (bookmark && !read) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
Text(
|
||||
text = update.mangaTitle,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(textAlpha),
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
}
|
||||
Text(
|
||||
text = update.chapterName,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
.copy(color = secondaryTextColor),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier.alpha(textAlpha),
|
||||
)
|
||||
}
|
||||
}
|
||||
ChapterDownloadIndicator(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadChapter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUpdatesItemLongClick(
|
||||
updatesItem: UpdatesUiModel.Item,
|
||||
selected: MutableList<UpdatesUiModel.Item>,
|
||||
updates: List<UpdatesUiModel.Item>,
|
||||
selectedPositions: Array<Int>,
|
||||
): Boolean {
|
||||
if (!selected.contains(updatesItem)) {
|
||||
val selectedIndex = updates.indexOf(updatesItem)
|
||||
if (selected.isEmpty()) {
|
||||
selected.add(updatesItem)
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
range = selectedIndex until selectedPositions[0]
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1)..selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = selectedIndex..selectedIndex
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
val toAdd = updates[it]
|
||||
if (!selected.contains(toAdd)) {
|
||||
selected.add(toAdd)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onUpdatesItemClick(
|
||||
updatesItem: UpdatesUiModel.Item,
|
||||
selected: MutableList<UpdatesUiModel.Item>,
|
||||
updates: List<UpdatesUiModel.Item>,
|
||||
selectedPositions: Array<Int>,
|
||||
onUpdateClicked: (UpdatesItem) -> Unit,
|
||||
) {
|
||||
val selectedIndex = updates.indexOf(updatesItem)
|
||||
when {
|
||||
selected.contains(updatesItem) -> {
|
||||
val removedIndex = updates.indexOf(updatesItem)
|
||||
selected.remove(updatesItem)
|
||||
|
||||
if (removedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = updates.indexOfFirst { selected.contains(it) }
|
||||
} else if (removedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = updates.indexOfLast { selected.contains(it) }
|
||||
}
|
||||
}
|
||||
selected.isNotEmpty() -> {
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = selectedIndex
|
||||
}
|
||||
selected.add(updatesItem)
|
||||
}
|
||||
else -> onUpdateClicked(updatesItem.item)
|
||||
}
|
||||
}
|
@ -12,3 +12,5 @@ val horizontalPadding = horizontal
|
||||
val verticalPadding = vertical
|
||||
|
||||
val topPaddingValues = PaddingValues(top = vertical)
|
||||
|
||||
const val ReadItemAlpha = .38f
|
||||
|
@ -27,3 +27,21 @@ fun LazyListState.isScrollingUp(): Boolean {
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrollingDown(): Boolean {
|
||||
var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
|
||||
var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
|
||||
return remember {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex < firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset <= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
}
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
enum class NavBarVisibility {
|
||||
SHOW,
|
||||
HIDE
|
||||
}
|
||||
|
||||
fun NavBarVisibility.toBoolean(): Boolean {
|
||||
return when (this) {
|
||||
NavBarVisibility.SHOW -> true
|
||||
NavBarVisibility.HIDE -> false
|
||||
}
|
||||
}
|
@ -226,7 +226,7 @@ class MainActivity : BaseActivity() {
|
||||
if (!router.hasRootController()) {
|
||||
// Set start screen
|
||||
if (!handleIntentAction(intent)) {
|
||||
setSelectedNavItem(startScreenId)
|
||||
moveToStartScreen()
|
||||
}
|
||||
}
|
||||
syncActivityViewWithController()
|
||||
@ -483,10 +483,15 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Updates screen has custom back handler
|
||||
if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
|
||||
router.handleBack()
|
||||
return
|
||||
}
|
||||
val backstackSize = router.backstackSize
|
||||
if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
||||
// Return to start screen
|
||||
setSelectedNavItem(startScreenId)
|
||||
moveToStartScreen()
|
||||
} else if (shouldHandleExitConfirmation()) {
|
||||
// Exit confirmation (resets after 2 seconds)
|
||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||
@ -499,6 +504,10 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToStartScreen() {
|
||||
setSelectedNavItem(startScreenId)
|
||||
}
|
||||
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
binding.appbar.apply {
|
||||
tag = isTransparentWhenNotLifted
|
||||
|
@ -27,7 +27,7 @@ import eu.kanade.data.chapter.NoChaptersException
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.presentation.manga.MangaScreen
|
||||
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
||||
|
@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
|
||||
open class BaseChapterHolder(
|
||||
view: View,
|
||||
|
@ -1,53 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class DateSectionItem(
|
||||
private val date: Date,
|
||||
private val range: Int,
|
||||
private val dateFormat: DateFormat,
|
||||
) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
|
||||
return DateSectionItemHolder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: DateSectionItemHolder, position: Int, payloads: List<Any?>?) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is DateSectionItem) {
|
||||
return date == other.date
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return date.hashCode()
|
||||
}
|
||||
|
||||
inner class DateSectionItemHolder(private val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
fun bind(item: DateSectionItem) {
|
||||
binding.title.text = item.date.toRelativeString(view.context, range, dateFormat)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ConfirmDeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : ConfirmDeleteChaptersDialog.Listener {
|
||||
|
||||
private var chaptersToDelete = emptyList<UpdatesItem>()
|
||||
|
||||
constructor(target: T, chaptersToDelete: List<UpdatesItem>) : this() {
|
||||
this.chaptersToDelete = chaptersToDelete
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters(chaptersToDelete)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters(chaptersToDelete: List<UpdatesItem>)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.content.Context
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
class UpdatesAdapter(
|
||||
val controller: UpdatesController,
|
||||
context: Context,
|
||||
val items: List<IFlexible<*>>?,
|
||||
) : BaseChaptersAdapter<IFlexible<*>>(controller, items) {
|
||||
|
||||
var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||
var unreadColor = context.getResourceColor(R.attr.colorOnSurface)
|
||||
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
|
||||
var bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
interface OnCoverClickListener {
|
||||
fun onCoverClick(position: Int)
|
||||
}
|
||||
}
|
@ -1,149 +1,65 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.util.NavBarVisibility
|
||||
import eu.kanade.presentation.util.toBoolean
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
*/
|
||||
class UpdatesController :
|
||||
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
|
||||
RootController,
|
||||
ActionModeWithToolbar.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
BaseChaptersAdapter.OnChapterClickListener,
|
||||
ConfirmDeleteChaptersDialog.Listener,
|
||||
UpdatesAdapter.OnCoverClickListener {
|
||||
FullComposeController<UpdatesPresenter>(),
|
||||
RootController {
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionModeWithToolbar? = null
|
||||
override fun createPresenter() = UpdatesPresenter()
|
||||
|
||||
/**
|
||||
* Adapter containing the recent chapters.
|
||||
*/
|
||||
var adapter: UpdatesAdapter? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_updates)
|
||||
}
|
||||
|
||||
override fun createPresenter(): UpdatesPresenter {
|
||||
return UpdatesPresenter()
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val state by presenter.state.collectAsState()
|
||||
when (state) {
|
||||
is UpdatesState.Loading -> LoadingScreen()
|
||||
is UpdatesState.Error -> Text(text = (state as UpdatesState.Error).error.message.orEmpty())
|
||||
is UpdatesState.Success ->
|
||||
UpdateScreen(
|
||||
state = (state as UpdatesState.Success),
|
||||
onClickCover = this::openManga,
|
||||
onClickUpdate = this::openChapter,
|
||||
onDownloadChapter = this::downloadChapters,
|
||||
onUpdateLibrary = this::updateLibrary,
|
||||
onBackClicked = this::onBackClicked,
|
||||
toggleNavBarVisibility = this::toggleNavBarVisibility,
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked = { updatesItems, bookmark ->
|
||||
presenter.bookmarkUpdates(updatesItems, bookmark)
|
||||
},
|
||||
onMultiMarkAsReadClicked = { updatesItems, read ->
|
||||
presenter.markUpdatesRead(updatesItems, read)
|
||||
},
|
||||
onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
|
||||
)
|
||||
}
|
||||
|
||||
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
val layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.layoutManager = layoutManager
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
binding.recycler.scrollStateChanges()
|
||||
.onEach {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
binding.swipeRefresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
binding.swipeRefresh.isRefreshing = true
|
||||
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
|
||||
binding.swipeRefresh.refreshes()
|
||||
.onEach {
|
||||
updateLibrary()
|
||||
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
viewScope.launch {
|
||||
presenter.updates.collectLatest { updatesItems ->
|
||||
destroyActionModeIfNeeded()
|
||||
if (adapter == null) {
|
||||
adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, updatesItems)
|
||||
binding.recycler.adapter = adapter
|
||||
adapter!!.fastScroller = binding.fastScroller
|
||||
} else {
|
||||
adapter?.updateDataSet(updatesItems)
|
||||
}
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.fastScroller.isVisible = true
|
||||
binding.recycler.onAnimationsFinished {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.updates, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_update_library -> updateLibrary()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun updateLibrary() {
|
||||
@ -154,262 +70,67 @@ class UpdatesController :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected chapters
|
||||
* @return list of selected chapters
|
||||
*/
|
||||
private fun getSelectedChapters(): List<UpdatesItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? UpdatesItem }
|
||||
// Let compose view handle this
|
||||
override fun handleBack(): Boolean {
|
||||
(activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
// Get item from position
|
||||
val item = adapter.getItem(position) as? UpdatesItem ?: return false
|
||||
return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
true
|
||||
} else {
|
||||
openChapter(item)
|
||||
false
|
||||
}
|
||||
private fun onBackClicked() {
|
||||
(activity as? MainActivity)?.moveToStartScreen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is long clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity
|
||||
if (actionMode == null && activity is MainActivity) {
|
||||
actionMode = activity.startActionModeAndToolbar(this)
|
||||
activity.showBottomNav(false)
|
||||
}
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to toggle selection
|
||||
* @param position position of selected item
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
adapter.toggleSelection(position)
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open chapter in reader
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.manga.id, item.chapter.id)
|
||||
startActivity(intent)
|
||||
private fun toggleNavBarVisibility(navBarVisibility: NavBarVisibility) {
|
||||
val showNavBar = navBarVisibility.toBoolean()
|
||||
(activity as? MainActivity)?.showBottomNav(showNavBar)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected items
|
||||
* @param chapters list of selected [UpdatesItem]s
|
||||
* @param items list of selected [UpdatesItem]s
|
||||
*/
|
||||
private fun downloadChapters(chapters: List<UpdatesItem>) {
|
||||
presenter.downloadChapters(chapters)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
if (size > 0) {
|
||||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_no_recent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
fun onChapterDownloadUpdate(download: Download) {
|
||||
adapter?.currentItems
|
||||
?.filterIsInstance<UpdatesItem>()
|
||||
?.find { it.chapter.id == download.chapter.id }?.let {
|
||||
adapter?.updateItem(it, it.status)
|
||||
private fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||
if (items.isEmpty()) return
|
||||
viewScope.launch {
|
||||
when (action) {
|
||||
ChapterDownloadAction.START -> {
|
||||
presenter.downloadChapters(items)
|
||||
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||
DownloadService.start(activity!!)
|
||||
}
|
||||
}
|
||||
ChapterDownloadAction.START_NOW -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
presenter.startDownloadingNow(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.CANCEL -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
presenter.cancelDownload(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.DELETE -> {
|
||||
presenter.deleteChapters(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
private fun markAsRead(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, true)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
* @param chapters list of selected [UpdatesItem]
|
||||
*/
|
||||
private fun markAsUnread(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, false)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
override fun deleteChapters(chaptersToDelete: List<UpdatesItem>) {
|
||||
presenter.deleteChapters(chaptersToDelete)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCoverClick(position: Int) {
|
||||
destroyActionModeIfNeeded()
|
||||
|
||||
val chapterClicked = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
openManga(chapterClicked)
|
||||
}
|
||||
|
||||
private fun openManga(chapter: UpdatesItem) {
|
||||
router.pushController(MangaController(chapter.manga.id!!))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapters are deleted
|
||||
*/
|
||||
fun onChaptersDeleted() {
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when error while deleting
|
||||
* @param error error message
|
||||
*/
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
logcat(LogPriority.ERROR, error)
|
||||
}
|
||||
|
||||
override fun downloadChapter(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
if (item.status == Download.State.ERROR) {
|
||||
DownloadService.start(activity!!)
|
||||
} else {
|
||||
downloadChapters(listOf(item))
|
||||
}
|
||||
adapter?.updateItem(item)
|
||||
}
|
||||
|
||||
override fun deleteChapter(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
deleteChapters(listOf(item))
|
||||
adapter?.updateItem(item)
|
||||
}
|
||||
|
||||
override fun startDownloadNow(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
presenter.startDownloadingNow(item.chapter)
|
||||
}
|
||||
|
||||
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode created.
|
||||
* @param mode the ActionMode object
|
||||
* @param menu menu object of ActionMode
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
|
||||
menuInflater.inflate(R.menu.updates_chapter_selection, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter?.selectedItemCount ?: 0
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = count.toString()
|
||||
private fun deleteChaptersWithConfirmation(items: List<UpdatesItem>) {
|
||||
if (items.isEmpty()) return
|
||||
viewScope.launch {
|
||||
val result = MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.await(android.R.string.ok, android.R.string.cancel)
|
||||
if (result == AlertDialog.BUTTON_POSITIVE) presenter.deleteChapters(items)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
||||
val chapters = getSelectedChapters()
|
||||
if (chapters.isEmpty()) return
|
||||
toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
|
||||
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
|
||||
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark }
|
||||
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark }
|
||||
toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
|
||||
toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode item clicked
|
||||
* @param mode the ActionMode object
|
||||
* @param item item from ActionMode.
|
||||
*/
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return onActionItemClicked(item)
|
||||
}
|
||||
|
||||
private fun onActionItemClicked(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_select_all -> selectAll()
|
||||
R.id.action_select_inverse -> selectInverse()
|
||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
||||
R.id.action_delete ->
|
||||
ConfirmDeleteChaptersDialog(this, getSelectedChapters())
|
||||
.showDialog(router)
|
||||
R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true)
|
||||
R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false)
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode destroyed
|
||||
* @param mode the ActionMode object
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||
adapter?.clearSelection()
|
||||
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
private fun selectAll() {
|
||||
val adapter = adapter ?: return
|
||||
adapter.selectAll()
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun selectInverse() {
|
||||
val adapter = adapter ?: return
|
||||
for (i in 0..adapter.itemCount) {
|
||||
adapter.toggleSelection(i)
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
adapter.notifyDataSetChanged()
|
||||
private fun openManga(item: UpdatesItem) {
|
||||
router.pushController(MangaController(item.update.mangaId))
|
||||
}
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
|
||||
|
||||
/**
|
||||
* Holder that contains chapter item
|
||||
* UI related actions should be called from here.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) :
|
||||
BaseChapterHolder(view, adapter) {
|
||||
|
||||
private val binding = UpdatesItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.mangaCover.setOnClickListener {
|
||||
adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.download.listener = downloadActionListener
|
||||
}
|
||||
|
||||
fun bind(item: UpdatesItem) {
|
||||
// Set chapter title
|
||||
binding.chapterTitle.text = item.chapter.name
|
||||
|
||||
// Set manga title
|
||||
binding.mangaTitle.text = item.manga.title
|
||||
|
||||
// Check if chapter is read and/or bookmarked and set correct color
|
||||
if (item.chapter.read) {
|
||||
binding.chapterTitle.setTextColor(adapter.readColor)
|
||||
binding.mangaTitle.setTextColor(adapter.readColor)
|
||||
} else {
|
||||
binding.mangaTitle.setTextColor(adapter.unreadColor)
|
||||
binding.chapterTitle.setTextColor(
|
||||
if (item.chapter.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
// Set bookmark status
|
||||
binding.bookmarkIcon.isVisible = item.chapter.bookmark
|
||||
|
||||
// Set chapter status
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
binding.download.setState(item.status, item.progress)
|
||||
|
||||
// Set cover
|
||||
binding.mangaCover.dispose()
|
||||
binding.mangaCover.load(item.manga)
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
|
||||
class UpdatesItem(chapter: Chapter, val manga: Manga, header: DateSectionItem) :
|
||||
BaseChapterItem<UpdatesHolder, DateSectionItem>(chapter, header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.updates_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): UpdatesHolder {
|
||||
return UpdatesHolder(view, adapter as UpdatesAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: UpdatesHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
}
|
@ -1,233 +1,321 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.data.manga.mangaChapterMapper
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.preference.asHotFlow
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TreeMap
|
||||
|
||||
class UpdatesPresenter(
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val getUpdates: GetUpdates = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : BasePresenter<UpdatesController>() {
|
||||
|
||||
private val relativeTime: Int = preferences.relativeTime().get()
|
||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
||||
private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading)
|
||||
val state: StateFlow<UpdatesState> = _state.asStateFlow()
|
||||
|
||||
private val _updates: MutableStateFlow<List<UpdatesItem>> = MutableStateFlow(listOf())
|
||||
val updates: StateFlow<List<UpdatesItem>> = _updates.asStateFlow()
|
||||
/**
|
||||
* Helper function to update the UI state only if it's currently in success state
|
||||
*/
|
||||
private fun updateSuccessState(func: (UpdatesState.Success) -> UpdatesState.Success) {
|
||||
_state.update { if (it is UpdatesState.Success) func(it) else it }
|
||||
}
|
||||
|
||||
private var incognitoMode = false
|
||||
set(value) {
|
||||
updateSuccessState { it.copy(isIncognitoMode = value) }
|
||||
field = value
|
||||
}
|
||||
private var downloadOnlyMode = false
|
||||
set(value) {
|
||||
updateSuccessState { it.copy(isDownloadedOnlyMode = value) }
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsStatusJob: Job? = null
|
||||
private var observeDownloadsPageJob: Job? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
presenterScope.launchIO {
|
||||
subscribeToUpdates()
|
||||
// Set date limit for recent chapters
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -3)
|
||||
}
|
||||
|
||||
getUpdates.subscribe(calendar)
|
||||
.catch { exception ->
|
||||
_state.value = UpdatesState.Error(exception)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
val uiModels = updates.toUpdateUiModels()
|
||||
_state.update { currentState ->
|
||||
when (currentState) {
|
||||
is UpdatesState.Success -> currentState.copy(uiModels)
|
||||
is UpdatesState.Loading, is UpdatesState.Error ->
|
||||
UpdatesState.Success(
|
||||
uiModels = uiModels,
|
||||
isIncognitoMode = incognitoMode,
|
||||
isDownloadedOnlyMode = downloadOnlyMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
observeDownloads()
|
||||
}
|
||||
}
|
||||
|
||||
preferences.incognitoMode()
|
||||
.asHotFlow { incognito ->
|
||||
incognitoMode = incognito
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
|
||||
preferences.downloadedOnly()
|
||||
.asHotFlow { downloadedOnly ->
|
||||
downloadOnlyMode = downloadedOnly
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateUiModels(): List<UpdatesUiModel> {
|
||||
return this.map { update ->
|
||||
val activeDownload = downloadManager.queue.find { update.chapterId == it.chapter.id }
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
update.chapterName,
|
||||
update.scanlator,
|
||||
update.mangaTitle,
|
||||
update.sourceId,
|
||||
)
|
||||
val downloadState = when {
|
||||
activeDownload != null -> activeDownload.status
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
val item = UpdatesItem(
|
||||
update = update,
|
||||
downloadStateProvider = { downloadState },
|
||||
downloadProgressProvider = { activeDownload?.progress ?: 0 },
|
||||
)
|
||||
UpdatesUiModel.Item(item)
|
||||
}
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L ->
|
||||
UpdatesUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun observeDownloads() {
|
||||
observeDownloadsStatusJob?.cancel()
|
||||
observeDownloadsStatusJob = presenterScope.launchIO {
|
||||
downloadManager.queue.getStatusAsFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collectLatest {
|
||||
withUIContext {
|
||||
onDownloadStatusChange(it)
|
||||
view?.onChapterDownloadUpdate(it)
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeDownloadsPageJob?.cancel()
|
||||
observeDownloadsPageJob = presenterScope.launchIO {
|
||||
downloadManager.queue.getProgressAsFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collectLatest {
|
||||
withUIContext {
|
||||
view?.onChapterDownloadUpdate(it)
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable containing recent chapters and date
|
||||
*/
|
||||
private suspend fun subscribeToUpdates() {
|
||||
// Set date limit for recent chapters
|
||||
val cal = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -3)
|
||||
}
|
||||
|
||||
handler
|
||||
.subscribeToList {
|
||||
mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper)
|
||||
}
|
||||
.map { mangaChapter ->
|
||||
val map = TreeMap<Date, MutableList<Pair<Manga, Chapter>>> { d1, d2 -> d2.compareTo(d1) }
|
||||
val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() }
|
||||
byDate.flatMap { entry ->
|
||||
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
|
||||
entry.value
|
||||
.sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed()
|
||||
.map { UpdatesItem(it.second, it.first, dateItem) }
|
||||
}
|
||||
}
|
||||
.collectLatest { list ->
|
||||
list.forEach { item ->
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
|
||||
|
||||
// If there's an active download, assign it, otherwise ask the manager if
|
||||
// the chapter is downloaded and assign it to the status.
|
||||
if (download != null) {
|
||||
item.download = download
|
||||
}
|
||||
}
|
||||
setDownloadedChapters(list)
|
||||
|
||||
_updates.value = list
|
||||
|
||||
// Set unread chapter count for bottom bar badge
|
||||
preferences.unreadUpdatesCount().set(list.count { !it.chapter.read })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param items the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(items: List<UpdatesItem>) {
|
||||
for (item in items) {
|
||||
val manga = item.manga
|
||||
val chapter = item.chapter
|
||||
|
||||
if (downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)) {
|
||||
item.status = Download.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of chapters.
|
||||
*
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
private fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.State.QUEUE) {
|
||||
val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<UpdatesItem>()
|
||||
val chapter = chapters.find { it.chapter.id == download.chapter.id }
|
||||
if (chapter != null && chapter.download == null) {
|
||||
chapter.download = download
|
||||
private fun updateDownloadState(download: Download) {
|
||||
updateSuccessState { successState ->
|
||||
val modifiedIndex = successState.uiModels.indexOfFirst {
|
||||
it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id
|
||||
}
|
||||
if (modifiedIndex < 0) return@updateSuccessState successState
|
||||
|
||||
val newUiModels = successState.uiModels.toMutableList().apply {
|
||||
var uiModel = removeAt(modifiedIndex)
|
||||
if (uiModel is UpdatesUiModel.Item) {
|
||||
val item = uiModel.item.copy(
|
||||
downloadStateProvider = { download.status },
|
||||
downloadProgressProvider = { download.progress },
|
||||
)
|
||||
uiModel = UpdatesUiModel.Item(item)
|
||||
}
|
||||
add(modifiedIndex, uiModel)
|
||||
}
|
||||
successState.copy(uiModels = newUiModels)
|
||||
}
|
||||
}
|
||||
|
||||
fun startDownloadingNow(chapter: Chapter) {
|
||||
downloadManager.startDownloadNow(chapter.id)
|
||||
fun startDownloadingNow(chapterId: Long) {
|
||||
downloadManager.startDownloadNow(chapterId)
|
||||
}
|
||||
|
||||
fun cancelDownload(chapterId: Long) {
|
||||
val activeDownload = downloadManager.queue.find { chapterId == it.chapter.id } ?: return
|
||||
downloadManager.deletePendingDownload(activeDownload)
|
||||
updateDownloadState(activeDownload.apply { status = Download.State.NOT_DOWNLOADED })
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapter as read
|
||||
*
|
||||
* @param items list of selected chapters
|
||||
* @param read read status
|
||||
* Mark the selected updates list as read/unread.
|
||||
* @param updates the list of selected updates.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
|
||||
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
setReadStatus.await(
|
||||
read = read,
|
||||
values = items
|
||||
.map { it.chapter }
|
||||
values = updates
|
||||
.mapNotNull { getChapter.await(it.update.chapterId) }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param chapters list of chapters
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param updates the list of chapters to bookmark.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
try {
|
||||
deleteChaptersInternal(chapters)
|
||||
withUIContext { view?.onChaptersDeleted() }
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { view?.onChaptersDeletedError(e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapters as bookmarked
|
||||
* @param items list of selected chapters
|
||||
* @param bookmarked bookmark status
|
||||
*/
|
||||
fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
val toUpdate = items.map {
|
||||
ChapterUpdate(
|
||||
bookmark = bookmarked,
|
||||
id = it.chapter.id,
|
||||
)
|
||||
}
|
||||
updateChapter.awaitAll(toUpdate)
|
||||
updates
|
||||
.filterNot { it.update.bookmark == bookmark }
|
||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||
.let { updateChapter.awaitAll(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected chapters
|
||||
* @param items list of recent chapters seleted.
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param updatesItem the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(items: List<UpdatesItem>) {
|
||||
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter.toDbChapter())) }
|
||||
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||
for (updates in groupedUpdates) {
|
||||
val mangaId = updates.first().update.mangaId
|
||||
val manga = getManga.await(mangaId) ?: continue
|
||||
// Don't download if source isn't available
|
||||
sourceManager.get(manga.source) ?: continue
|
||||
val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param items chapters selected
|
||||
* @param updatesItem list of chapters
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
|
||||
val itemsByManga = chapterItems.groupBy { it.manga.id }
|
||||
for ((_, items) in itemsByManga) {
|
||||
val manga = items.first().manga
|
||||
val source = sourceManager.get(manga.source) ?: continue
|
||||
val chapters = items.map { it.chapter.toDbChapter() }
|
||||
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||
val deletedIds = groupedUpdates.flatMap { updates ->
|
||||
val mangaId = updates.first().update.mangaId
|
||||
val manga = getManga.await(mangaId) ?: return@flatMap emptyList()
|
||||
val source = sourceManager.get(manga.source) ?: return@flatMap emptyList()
|
||||
val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
|
||||
downloadManager.deleteChapters(chapters, manga, source).mapNotNull { it.id }
|
||||
}
|
||||
updateSuccessState { successState ->
|
||||
val deletedUpdates = successState.uiModels.filter {
|
||||
it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId)
|
||||
}
|
||||
if (deletedUpdates.isEmpty()) return@updateSuccessState successState
|
||||
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
items.forEach {
|
||||
it.status = Download.State.NOT_DOWNLOADED
|
||||
it.download = null
|
||||
// TODO: Don't do this fake status update
|
||||
val newUiModels = successState.uiModels.toMutableList().apply {
|
||||
deletedUpdates.forEach { deletedUpdate ->
|
||||
val modifiedIndex = indexOf(deletedUpdate)
|
||||
var uiModel = removeAt(modifiedIndex)
|
||||
if (uiModel is UpdatesUiModel.Item) {
|
||||
val item = uiModel.item.copy(
|
||||
downloadStateProvider = { Download.State.NOT_DOWNLOADED },
|
||||
downloadProgressProvider = { 0 },
|
||||
)
|
||||
uiModel = UpdatesUiModel.Item(item)
|
||||
}
|
||||
add(modifiedIndex, uiModel)
|
||||
}
|
||||
}
|
||||
successState.copy(uiModels = newUiModels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UpdatesState {
|
||||
object Loading : UpdatesState()
|
||||
data class Error(val error: Throwable) : UpdatesState()
|
||||
data class Success(
|
||||
val uiModels: List<UpdatesUiModel>,
|
||||
val isIncognitoMode: Boolean = false,
|
||||
val isDownloadedOnlyMode: Boolean = false,
|
||||
val showSwipeRefreshIndicator: Boolean = false,
|
||||
) : UpdatesState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
val downloadStateProvider: () -> Download.State,
|
||||
val downloadProgressProvider: () -> Int,
|
||||
)
|
||||
|
@ -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
|
||||
GROUP BY source;
|
||||
|
||||
getRecentlyUpdated:
|
||||
SELECT *
|
||||
FROM mangas M
|
||||
JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
WHERE M.favorite = 1
|
||||
AND C.date_upload > :after
|
||||
AND C.date_fetch > M.date_added
|
||||
ORDER BY C.date_upload DESC;
|
||||
|
||||
getLibrary:
|
||||
SELECT M.*, COALESCE(MC.category_id, 0) AS category
|
||||
FROM (
|
||||
|
20
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