feat(reader): Download from reader chapter list (#1110)

* Download from reader

* Fix for localsource

* Fix actions for merged entries

* fix spotless

* refactor code

* catching Exception instead of Throwable

---------

Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
This commit is contained in:
kana-shii 2025-09-03 08:12:38 -03:00 committed by GitHub
parent 46f0e26dd3
commit 5280caa60d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 10 deletions

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -42,6 +43,9 @@ fun ChapterListDialog(
onClickChapter: (Chapter) -> Unit, onClickChapter: (Chapter) -> Unit,
onBookmark: (Chapter) -> Unit, onBookmark: (Chapter) -> Unit,
dateRelativeTime: Boolean, dateRelativeTime: Boolean,
// KMK -->
onDownloadAction: ((Chapter, ChapterDownloadAction) -> Unit)? = null,
// KMK <--
) { ) {
val manga by screenModel.mangaFlow.collectAsState() val manga by screenModel.mangaFlow.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@ -106,14 +110,22 @@ fun ChapterListDialog(
read = chapterItem.chapter.read, read = chapterItem.chapter.read,
bookmark = chapterItem.chapter.bookmark, bookmark = chapterItem.chapter.bookmark,
selected = false, selected = false,
downloadIndicatorEnabled = false, // KMK -->
downloadIndicatorEnabled = onDownloadAction != null,
// KMK <--
downloadStateProvider = { downloadState }, downloadStateProvider = { downloadState },
downloadProgressProvider = { progress }, downloadProgressProvider = { progress },
chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark, chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark, chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
onLongClick = { /*TODO*/ }, onLongClick = { /*TODO*/ },
onClick = { onClickChapter(chapterItem.chapter) }, onClick = { onClickChapter(chapterItem.chapter) },
onDownloadClick = null, // KMK -->
onDownloadClick = if (onDownloadAction != null) {
{ action -> onDownloadAction(chapterItem.chapter, action) }
} else {
null
},
// KMK <--
onChapterSwipe = { onChapterSwipe = {
onBookmark(chapterItem.chapter) onBookmark(chapterItem.chapter)
}, },

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import exh.source.MERGED_SOURCE_ID
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.util.DataSaver import exh.util.DataSaver
import exh.util.DataSaver.Companion.getImage import exh.util.DataSaver.Companion.getImage
@ -281,6 +282,11 @@ class Downloader(
if (chapters.isEmpty()) return if (chapters.isEmpty()) return
val source = sourceManager.get(manga.source) as? HttpSource ?: return val source = sourceManager.get(manga.source) as? HttpSource ?: return
// KMK -->
if (source.id == MERGED_SOURCE_ID) return
// KMK <--
val wasEmpty = queueState.value.isEmpty() val wasEmpty = queueState.value.isEmpty()
val chaptersToQueue = chapters.asSequence() val chaptersToQueue = chapters.asSequence()
// Filter out those already downloaded. // Filter out those already downloaded.
@ -330,6 +336,10 @@ class Downloader(
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private suspend fun downloadChapter(download: Download) { private suspend fun downloadChapter(download: Download) {
// KMK -->
if (download.source.id == MERGED_SOURCE_ID) return
// KMK <--
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e -> val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e ->
download.status = Download.State.ERROR download.status = Download.State.ERROR
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id) notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.UiModeManager import android.app.UiModeManager
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.ClipData import android.content.ClipData
@ -209,7 +208,7 @@ class ReaderActivity : BaseActivity() {
registerSecureActivity(this) registerSecureActivity(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition( overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN, OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit, R.anim.shared_axis_x_push_exit,
) )
@ -355,7 +354,7 @@ class ReaderActivity : BaseActivity() {
super.finish() super.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition( overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE, OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit, R.anim.shared_axis_x_pop_exit,
) )
@ -689,6 +688,11 @@ class ReaderActivity : BaseActivity() {
}.toImmutableList() }.toImmutableList()
}, },
state.dateRelativeTime, state.dateRelativeTime,
// KMK -->
onDownloadAction = { chapter, action ->
viewModel.handleDownloadAction(chapter, action)
},
// KMK <--
) )
} }
// SY --> // SY -->
@ -1061,7 +1065,7 @@ class ReaderActivity : BaseActivity() {
try { try {
readingModeToast?.cancel() readingModeToast?.cancel()
readingModeToast = toast(ReadingMode.fromPreference(mode).stringRes) readingModeToast = toast(ReadingMode.fromPreference(mode).stringRes)
} catch (e: ArrayIndexOutOfBoundsException) { } catch (_: ArrayIndexOutOfBoundsException) {
logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" } logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" }
} }
} }

View file

@ -18,6 +18,7 @@ import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
@ -72,6 +73,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
@ -169,6 +171,69 @@ class ReaderViewModel @JvmOverloads constructor(
field = value field = value
} }
// KMK -->
fun handleDownloadAction(chapter: Chapter, action: ChapterDownloadAction) {
when (action) {
ChapterDownloadAction.START -> downloadChapter(chapter)
ChapterDownloadAction.START_NOW -> downloadManager.startDownloadNow(chapter.id)
ChapterDownloadAction.CANCEL -> cancelDownload(chapter.id)
ChapterDownloadAction.DELETE -> deleteChapter(chapter)
}
}
/**
* @param chapter the chapter to download.
*/
private fun downloadChapter(chapter: Chapter) {
viewModelScope.launch {
val manga = manga?.let {
if (it.source == MERGED_SOURCE_ID) {
state.value.mergedManga?.get(chapter.mangaId) ?: return@launch
} else {
it
}
} ?: return@launch
downloadManager.downloadChapters(manga, listOf(chapter))
downloadManager.startDownloads()
}
}
private fun cancelDownload(chapterId: Long) {
viewModelScope.launch {
val activeDownload = downloadManager.getQueuedDownloadOrNull(chapterId) ?: return@launch
downloadManager.cancelQueuedDownloads(listOf(activeDownload))
// TODO: updateDownloadState(activeDownload.apply { status = Download.State.NOT_DOWNLOADED })
}
}
private fun deleteChapter(chapter: Chapter) {
viewModelScope.launchNonCancellable {
try {
val manga = if (manga?.source == MERGED_SOURCE_ID) {
state.value.mergedManga?.get(chapter.mangaId) ?: return@launchNonCancellable
} else {
manga ?: return@launchNonCancellable
}
val source = sourceManager.get(manga.source) ?: return@launchNonCancellable
downloadManager.deleteChapters(
listOf(chapter),
manga,
source,
ignoreCategoryExclusion = true,
)
// // KMK -->
// if (source.isLocal()) {
// // TODO: Refresh chapters state for Local source
// fetchChaptersFromSource()
// }
// // KMK <--
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}
// KMK <--
/** /**
* The chapter loader for the loaded manga. It'll be null until [manga] is set. * The chapter loader for the loaded manga. It'll be null until [manga] is set.
*/ */
@ -662,7 +727,7 @@ class ReaderViewModel @JvmOverloads constructor(
* if setting is enabled and [currentChapter] is queued for download * if setting is enabled and [currentChapter] is queued for download
*/ */
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? { private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also { return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!)?.also {
downloadManager.cancelQueuedDownloads(listOf(it)) downloadManager.cancelQueuedDownloads(listOf(it))
} }
} }
@ -864,7 +929,7 @@ class ReaderViewModel @JvmOverloads constructor(
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateChapter.await( updateChapter.await(
ChapterUpdate( ChapterUpdate(
id = chapter.id!!.toLong(), id = chapter.id!!,
bookmark = bookmarked, bookmark = bookmarked,
), ),
) )
@ -997,7 +1062,7 @@ class ReaderViewModel @JvmOverloads constructor(
val chapter = page.chapter.chapter val chapter = page.chapter.chapter
val filenameSuffix = " - ${page.number}" val filenameSuffix = " - ${page.number}"
return DiskUtil.buildValidFilename( return DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()), "${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
) + filenameSuffix ) + filenameSuffix
} }
@ -1293,7 +1358,7 @@ class ReaderViewModel @JvmOverloads constructor(
} else { } else {
SetAsCoverResult.AddToLibraryFirst SetAsCoverResult.AddToLibraryFirst
} }
} catch (e: Exception) { } catch (_: Exception) {
SetAsCoverResult.Error SetAsCoverResult.Error
} }
eventChannel.send(Event.SetCoverResult(result)) eventChannel.send(Event.SetCoverResult(result))