@@ -0,0 +1,39 @@
|
||||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
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
|
||||
|
||||
class UpdatesController :
|
||||
FullComposeController<UpdatesPresenter>(),
|
||||
RootController {
|
||||
|
||||
override fun createPresenter() = UpdatesPresenter()
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
UpdateScreen(
|
||||
presenter = presenter,
|
||||
onClickCover = { item ->
|
||||
router.pushController(MangaController(item.update.mangaId))
|
||||
},
|
||||
onBackClicked = {
|
||||
(activity as? MainActivity)?.moveToStartScreen()
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(presenter.selectionMode) {
|
||||
(activity as? MainActivity)?.showBottomNav(presenter.selectionMode.not())
|
||||
}
|
||||
LaunchedEffect(presenter.isLoading) {
|
||||
if (!presenter.isLoading) {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
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.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.updates.UpdatesState
|
||||
import eu.kanade.presentation.updates.UpdatesStateImpl
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
|
||||
class UpdatesPresenter(
|
||||
private val state: UpdatesStateImpl = UpdatesState() as UpdatesStateImpl,
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadCache: DownloadCache = 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 getChapter: GetChapter = Injekt.get(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) : BasePresenter<UpdatesController>(), UpdatesState by state {
|
||||
|
||||
val isDownloadOnly: Boolean by basePreferences.downloadedOnly().asState()
|
||||
val isIncognitoMode: Boolean by basePreferences.incognitoMode().asState()
|
||||
|
||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState()
|
||||
|
||||
val relativeTime: Int by uiPreferences.relativeTime().asState()
|
||||
val dateFormat: DateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
// First and last selected index in list
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
presenterScope.launchIO {
|
||||
// Set date limit for recent chapters
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -3)
|
||||
}
|
||||
|
||||
combine(
|
||||
getUpdates.subscribe(calendar).distinctUntilChanged(),
|
||||
downloadCache.changes,
|
||||
) { updates, _ -> updates }
|
||||
.onStart { delay(500) } // Defer to avoid crashing on initial render
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
state.items = updates.toUpdateItems()
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
presenterScope.launchIO {
|
||||
downloadManager.queue.statusFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collect {
|
||||
withUIContext {
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presenterScope.launchIO {
|
||||
downloadManager.queue.progressFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collect {
|
||||
withUIContext {
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
||||
return this
|
||||
.distinctBy { it.chapterId }
|
||||
.map {
|
||||
val activeDownload = downloadManager.queue.find { download -> it.chapterId == download.chapter.id }
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
it.chapterName,
|
||||
it.scanlator,
|
||||
it.mangaTitle,
|
||||
it.sourceId,
|
||||
)
|
||||
val downloadState = when {
|
||||
activeDownload != null -> activeDownload.status
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
UpdatesItem(
|
||||
update = it,
|
||||
downloadStateProvider = { downloadState },
|
||||
downloadProgressProvider = { activeDownload?.progress ?: 0 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of chapters.
|
||||
*
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
private fun updateDownloadState(download: Download) {
|
||||
state.items = items.toMutableList().apply {
|
||||
val modifiedIndex = indexOfFirst {
|
||||
it.update.chapterId == download.chapter.id
|
||||
}
|
||||
if (modifiedIndex < 0) return@apply
|
||||
|
||||
val item = removeAt(modifiedIndex)
|
||||
.copy(
|
||||
downloadStateProvider = { download.status },
|
||||
downloadProgressProvider = { download.progress },
|
||||
)
|
||||
add(modifiedIndex, item)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||
if (items.isEmpty()) return
|
||||
presenterScope.launch {
|
||||
when (action) {
|
||||
ChapterDownloadAction.START -> {
|
||||
downloadChapters(items)
|
||||
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||
DownloadService.start(view!!.activity!!)
|
||||
}
|
||||
}
|
||||
ChapterDownloadAction.START_NOW -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
startDownloadingNow(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.CANCEL -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
cancelDownload(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.DELETE -> {
|
||||
deleteChapters(items)
|
||||
}
|
||||
}
|
||||
toggleAllSelection(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDownloadingNow(chapterId: Long) {
|
||||
downloadManager.startDownloadNow(chapterId)
|
||||
}
|
||||
|
||||
private 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 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 markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
setReadStatus.await(
|
||||
read = read,
|
||||
chapters = updates
|
||||
.mapNotNull { getChapter.await(it.update.chapterId) }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param updates the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
updates
|
||||
.filterNot { it.update.bookmark == bookmark }
|
||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||
.let { updateChapter.awaitAll(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param updatesItem the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
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 updatesItem list of chapters
|
||||
*/
|
||||
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
updatesItem
|
||||
.groupBy { it.update.mangaId }
|
||||
.entries
|
||||
.forEach { (mangaId, updates) ->
|
||||
val manga = getManga.await(mangaId) ?: return@forEach
|
||||
val source = sourceManager.get(manga.source) ?: return@forEach
|
||||
val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelection(
|
||||
item: UpdatesItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
state.items = items.toMutableList().apply {
|
||||
val modifiedIndex = indexOfFirst { it == item }
|
||||
if (modifiedIndex < 0) return@apply
|
||||
|
||||
val oldItem = get(modifiedIndex)
|
||||
if (oldItem.selected == selected) return@apply
|
||||
|
||||
val firstSelection = none { it.selected }
|
||||
var newItem = removeAt(modifiedIndex).copy(selected = selected)
|
||||
add(modifiedIndex, newItem)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = modifiedIndex
|
||||
selectedPositions[1] = modifiedIndex
|
||||
} else {
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (modifiedIndex < selectedPositions[0]) {
|
||||
range = modifiedIndex + 1 until selectedPositions[0]
|
||||
selectedPositions[0] = modifiedIndex
|
||||
} else if (modifiedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1) until modifiedIndex
|
||||
selectedPositions[1] = modifiedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = IntRange.EMPTY
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
newItem = removeAt(it).copy(selected = true)
|
||||
add(it, newItem)
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
if (!selected) {
|
||||
if (modifiedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
} else if (modifiedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = indexOfLast { it.selected }
|
||||
}
|
||||
} else {
|
||||
if (modifiedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = modifiedIndex
|
||||
} else if (modifiedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = modifiedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAllSelection(selected: Boolean) {
|
||||
state.items = items.map {
|
||||
it.copy(selected = selected)
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
state.items = items.map {
|
||||
it.copy(selected = !it.selected)
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
object InternalError : Event()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
val downloadStateProvider: () -> Download.State,
|
||||
val downloadProgressProvider: () -> Int,
|
||||
val selected: Boolean = false,
|
||||
)
|
||||
Reference in New Issue
Block a user