add video quality dialog to episode downloads

This commit is contained in:
Quickdesh 2023-05-30 19:30:36 +05:30
parent 24e9e1846d
commit 1d1aa51de3
15 changed files with 369 additions and 174 deletions

View file

@ -1,16 +1,12 @@
package eu.kanade.presentation.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@ -24,14 +20,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload
@ -95,7 +86,7 @@ private fun NotDownloadedIndicator(
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(R.drawable.ic_download_chapter_24dp),
painter = painterResource(R.drawable.ic_download_item_24dp),
contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -235,38 +226,3 @@ private fun ErrorIndicator(
)
}
}
private fun Modifier.commonClickable(
enabled: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp
// To match composable parameter name when used later
private val IndicatorStrokeWidth = IndicatorPadding
private val IndicatorModifier = Modifier
.size(IndicatorSize)
.padding(IndicatorPadding)
private val ArrowModifier = Modifier
.size(IndicatorSize - 7.dp)

View file

@ -1,16 +1,12 @@
package eu.kanade.presentation.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@ -24,26 +20,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import uy.kohesive.injekt.injectLazy
enum class EpisodeDownloadAction {
START,
START_NOW,
CANCEL,
DELETE,
START_ALT,
SHOW_OPTIONS,
}
@Composable
@ -91,29 +80,14 @@ private fun NotDownloadedIndicator(
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(EpisodeDownloadAction.START_NOW) },
onLongClick = { onClick(EpisodeDownloadAction.SHOW_OPTIONS) },
onClick = { onClick(EpisodeDownloadAction.START) },
)
.secondaryItemAlpha(),
contentAlignment = Alignment.Center,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
val downloadText = if (preferences.useExternalDownloader().get()) {
stringResource(R.string.action_start_download_internally)
} else {
stringResource(R.string.action_start_download_externally)
}
DropdownMenuItem(
text = { Text(text = downloadText) },
onClick = {
onClick(EpisodeDownloadAction.START_ALT)
isMenuExpanded = false
},
)
}
Icon(
painter = painterResource(R.drawable.ic_download_chapter_24dp),
painter = painterResource(R.drawable.ic_download_item_24dp),
contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -253,40 +227,3 @@ private fun ErrorIndicator(
)
}
}
private fun Modifier.commonClickable(
enabled: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp
// To match composable parameter name when used later
private val IndicatorStrokeWidth = IndicatorPadding
private val IndicatorModifier = Modifier
.size(IndicatorSize)
.padding(IndicatorPadding)
private val ArrowModifier = Modifier
.size(IndicatorSize - 7.dp)
private val preferences: DownloadPreferences by injectLazy()

View file

@ -0,0 +1,53 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.kanade.domain.download.service.DownloadPreferences
import uy.kohesive.injekt.injectLazy
internal fun Modifier.commonClickable(
enabled: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
internal val IndicatorSize = 26.dp
internal val IndicatorPadding = 2.dp
// To match composable parameter name when used later
internal val IndicatorStrokeWidth = IndicatorPadding
internal val IndicatorModifier = Modifier
.size(IndicatorSize)
.padding(IndicatorPadding)
internal val ArrowModifier = Modifier
.size(IndicatorSize - 7.dp)
internal val preferences: DownloadPreferences by injectLazy()

View file

@ -0,0 +1,230 @@
package eu.kanade.presentation.entries.anime
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.SystemUpdateAlt
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import eu.kanade.core.util.asFlow
import eu.kanade.domain.entries.anime.interactor.GetAnime
import eu.kanade.domain.items.episode.interactor.GetEpisode
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.ui.player.EpisodeLoader
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class EpisodeOptionsDialogScreen(
private val episodeId: Long,
private val animeId: Long,
private val sourceId: Long,
private val useExternalDownloader: Boolean,
) : Screen {
@Composable
override fun Content() {
val sm = rememberScreenModel {
EpisodeOptionsDialogScreenModel(
episodeId = episodeId,
animeId = animeId,
sourceId = sourceId,
)
}
val state by sm.state.collectAsState()
EpisodeOptionsDialog(
episode = state.episode,
anime = state.anime,
resultList = state.resultList,
// onDismissRequest = onDismissRequest,
)
}
class EpisodeOptionsDialogScreenModel(
episodeId: Long,
animeId: Long,
sourceId: Long,
) : StateScreenModel<State>(State()) {
private val sourceManager: AnimeSourceManager = Injekt.get()
init {
coroutineScope.launch {
// To show loading state
mutableState.update { it.copy(episode = null, anime = null, resultList = null) }
val episode = Injekt.get<GetEpisode>().await(episodeId)!!
val anime = Injekt.get<GetAnime>().await(animeId)!!
val source = sourceManager.getOrStub(sourceId)
val result = withIOContext {
try {
val results =
EpisodeLoader.getLinks(episode, anime, source).asFlow().first()
Result.success(results)
} catch (e: Throwable) {
Result.failure(e)
}
}
mutableState.update { it.copy(episode = episode, anime = anime, resultList = result) }
}
}
}
@Immutable
data class State(
val episode: Episode? = null,
val anime: Anime? = null,
val resultList: Result<List<Video>>? = null,
)
@Composable
fun EpisodeOptionsDialog(
episode: Episode?,
anime: Anime?,
resultList: Result<List<Video>>? = null,
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val copiedString = stringResource(R.string.copied_video_link_to_clipboard)
val downloadManager = Injekt.get<AnimeDownloadManager>()
Column(
modifier = Modifier
.animateContentSize()
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(vertical = TabbedDialogPaddings.Vertical)
.windowInsetsPadding(WindowInsets.systemBars),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
QualityItem(
label = stringResource(R.string.choose_video_quality),
enabled = false,
useExternalDownloader = false,
)
if (resultList == null) {
LoadingScreen()
} else {
val videoList = resultList.getOrNull()
if (videoList != null) {
videoList.forEach { video ->
val downloadEpisode: (Boolean) -> Unit = {
downloadManager.downloadEpisodes(anime!!, listOf(episode!!), true, it, video)
onDismissEpisodeOptionsDialogScreen()
}
QualityItem(
label = video.quality,
enabled = true,
useExternalDownloader = useExternalDownloader,
onQualityClick = { downloadEpisode(false) },
onAlternateClick = { downloadEpisode(true) },
onCopyClick = {
if (video.videoUrl != null) {
clipboardManager.setText(AnnotatedString(video.videoUrl!!))
scope.launch { context.toast(copiedString) }
}
},
)
}
} else {
// yes
}
}
}
}
}
var onDismissEpisodeOptionsDialogScreen: () -> Unit = {}
@Composable
private fun QualityItem(
label: String,
enabled: Boolean,
useExternalDownloader: Boolean,
onQualityClick: () -> Unit = {},
onAlternateClick: () -> Unit = {},
onCopyClick: () -> Unit = {},
) {
Row(
modifier = Modifier
.clickable(
enabled = enabled,
onClick = onQualityClick,
)
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = label,
style = MaterialTheme.typography.bodyMedium,
)
if (enabled) {
IconButton(
onClick = onCopyClick,
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = stringResource(R.string.copy),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
val icon = if (useExternalDownloader) Icons.Outlined.Download else Icons.Outlined.SystemUpdateAlt
val description = if (useExternalDownloader) R.string.action_start_download_internally else R.string.action_start_download_externally
IconButton(
onClick = onAlternateClick,
) {
Icon(
imageVector = icon,
contentDescription = stringResource(description),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}

View file

@ -140,22 +140,11 @@ class AnimeDownloadManager(
*
* @param anime the anime of the episodes.
* @param episodes the list of episodes to enqueue.
* @param autoStart whether to start the downloader after enqueing the episodes.
* @param autoStart whether to start the downloader after enqueuing the episodes.
* @param alt whether to use the alternative downloader
*/
fun downloadEpisodes(anime: Anime, episodes: List<Episode>, autoStart: Boolean = true, alt: Boolean = false) {
downloader.queueEpisodes(anime, episodes, autoStart, alt)
}
/**
* Tells the downloader to enqueue the given list of episodes
* using the alternative method of downloading.
*
* @param anime the anime of the episodes.
* @param episodes the list of episodes to enqueue.
* @param autoStart whether to start the downloader after enqueing the episodes.
*/
fun downloadEpisodesAlt(anime: Anime, episodes: List<Episode>, autoStart: Boolean = true) {
downloader.queueEpisodes(anime, episodes, autoStart, true)
fun downloadEpisodes(anime: Anime, episodes: List<Episode>, autoStart: Boolean = true, alt: Boolean = false, video: Video? = null) {
downloader.queueEpisodes(anime, episodes, autoStart, alt, video)
}
/**

View file

@ -268,7 +268,7 @@ class AnimeDownloader(
* @param episodes the list of episodes to download.
* @param autoStart whether to start the downloader after enqueing the episodes.
*/
fun queueEpisodes(anime: Anime, episodes: List<Episode>, autoStart: Boolean, changeDownloader: Boolean = false) = launchIO {
fun queueEpisodes(anime: Anime, episodes: List<Episode>, autoStart: Boolean, changeDownloader: Boolean = false, video: Video? = null) = launchIO {
if (episodes.isEmpty()) {
return@launchIO
}
@ -289,7 +289,7 @@ class AnimeDownloader(
// Filter out those already enqueued.
.filter { episode -> queue.none { it.episode.id == episode.id } }
// Create a download for each one.
.map { AnimeDownload(source, anime, it, changeDownloader) }
.map { AnimeDownload(source, anime, it, changeDownloader, video) }
if (episodesToQueue.isNotEmpty()) {
queue.addAll(episodesToQueue)

View file

@ -41,8 +41,10 @@ import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.entries.DeleteItemsDialog
import eu.kanade.presentation.entries.EditCoverAction
import eu.kanade.presentation.entries.anime.AnimeScreen
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
import eu.kanade.presentation.entries.anime.EpisodeSettingsDialog
import eu.kanade.presentation.entries.anime.components.AnimeCoverDialog
import eu.kanade.presentation.entries.anime.onDismissEpisodeOptionsDialogScreen
import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.R
@ -247,6 +249,18 @@ class AnimeScreen(
defaultIntroLength = screenModel.playerPreferences.defaultIntroLength().get(),
)
}
is AnimeInfoScreenModel.Dialog.Options -> {
onDismissEpisodeOptionsDialogScreen = onDismissRequest
NavigatorAdaptiveSheet(
screen = EpisodeOptionsDialogScreen(
episodeId = dialog.episode.id,
animeId = dialog.anime.id,
sourceId = dialog.source.id,
useExternalDownloader = screenModel.downloadPreferences.useExternalDownloader().get(),
),
onDismissRequest = onDismissRequest,
)
}
}
}

View file

@ -34,6 +34,7 @@ import eu.kanade.presentation.components.EpisodeDownloadAction
import eu.kanade.presentation.entries.DownloadAction
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadService
@ -80,7 +81,7 @@ class AnimeInfoScreenModel(
val context: Context,
val animeId: Long,
private val isFromSource: Boolean,
private val downloadPreferences: DownloadPreferences = Injekt.get(),
internal val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
@ -236,7 +237,7 @@ class AnimeInfoScreenModel(
coroutineScope.launch {
if (!hasDownloads()) return@launch
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.delete_downloads_for_manga),
message = context.getString(R.string.delete_downloads_for_anime),
actionLabel = context.getString(R.string.action_delete),
withDismissAction = true,
)
@ -559,6 +560,7 @@ class AnimeInfoScreenModel(
private fun startDownload(
episodes: List<Episode>,
startNow: Boolean,
video: Video? = null,
) {
val successState = successState ?: return
@ -566,7 +568,7 @@ class AnimeInfoScreenModel(
val episodeId = episodes.singleOrNull()?.id ?: return
downloadManager.startDownloadNow(episodeId)
} else {
downloadEpisodes(episodes)
downloadEpisodes(episodes, false, video)
}
if (!isFavorited && !successState.hasPromptedToAddBefore) {
coroutineScope.launch {
@ -607,7 +609,10 @@ class AnimeInfoScreenModel(
EpisodeDownloadAction.DELETE -> {
deleteEpisodes(items.map { it.episode })
}
EpisodeDownloadAction.START_ALT -> TODO()
EpisodeDownloadAction.SHOW_OPTIONS -> {
val episode = items.singleOrNull()?.episode ?: return
showOptionsDialog(episode)
}
}
}
@ -659,13 +664,9 @@ class AnimeInfoScreenModel(
* Downloads the given list of episodes with the manager.
* @param episodes the list of episodes to download.
*/
private fun downloadEpisodes(episodes: List<Episode>, alt: Boolean = false) {
private fun downloadEpisodes(episodes: List<Episode>, alt: Boolean = false, video: Video? = null) {
val anime = successState?.anime ?: return
if (alt) {
downloadManager.downloadEpisodesAlt(anime, episodes)
} else {
downloadManager.downloadEpisodes(anime, episodes)
}
downloadManager.downloadEpisodes(anime, episodes, true, alt, video)
toggleAllSelection(false)
}
@ -920,6 +921,7 @@ class AnimeInfoScreenModel(
data class ChangeCategory(val anime: Anime, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteEpisodes(val episodes: List<Episode>) : Dialog()
data class DuplicateAnime(val anime: Anime, val duplicate: Anime) : Dialog()
data class Options(val episode: Episode, val anime: Anime, val source: AnimeSource) : Dialog()
object ChangeAnimeSkipIntro : Dialog()
object SettingsSheet : Dialog()
object TrackSheet : Dialog()
@ -979,6 +981,15 @@ class AnimeInfoScreenModel(
}
}
}
private fun showOptionsDialog(episode: Episode) {
mutableState.update { state ->
when (state) {
AnimeScreenState.Loading -> state
is AnimeScreenState.Success -> { state.copy(dialog = Dialog.Options(episode, state.anime, state.source)) }
}
}
}
}
sealed class AnimeScreenState {

View file

@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.ui.player
import android.net.Uri
import eu.kanade.domain.items.episode.model.toSEpisode
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.animesource.online.fetchUrlFromVideo
import eu.kanade.tachiyomi.data.database.models.anime.Episode
import eu.kanade.tachiyomi.data.database.models.anime.toDomainEpisode
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import rx.Observable
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
@ -36,38 +36,21 @@ class EpisodeLoader {
return downloadManager.isEpisodeDownloaded(episode.name, episode.scanlator, anime.title, anime.source, skipCache = true)
}
fun getLink(episode: Episode, anime: Anime, source: AnimeSource): Observable<Video?> {
val downloadManager: AnimeDownloadManager = Injekt.get()
val isDownloaded = downloadManager.isEpisodeDownloaded(episode.name, episode.scanlator, anime.title, anime.source)
return when {
isDownloaded -> isDownloaded(episode, anime, source, downloadManager).map {
it.firstOrNull()
}
source is AnimeHttpSource -> isHttp(episode, source).map {
it.firstOrNull()
}
source is LocalAnimeSource -> isLocal(episode).map {
it.firstOrNull()
}
else -> error("source not supported")
}
}
private fun isHttp(episode: Episode, source: AnimeHttpSource): Observable<List<Video>> {
return source.fetchVideoList(episode)
return source.fetchVideoList(episode.toSEpisode())
.flatMapIterable { it }
.flatMap {
source.fetchUrlFromVideo(it)
}.toList()
}
fun isDownloaded(
private fun isDownloaded(
episode: Episode,
anime: Anime,
source: AnimeSource,
downloadManager: AnimeDownloadManager,
): Observable<List<Video>> {
return downloadManager.buildVideo(source, anime, episode.toDomainEpisode()!!)
return downloadManager.buildVideo(source, anime, episode)
.onErrorReturn { null }
.map {
if (it == null) {
@ -78,7 +61,7 @@ class EpisodeLoader {
}
}
fun isLocal(
private fun isLocal(
episode: Episode,
): Observable<List<Video>> {
return try {

View file

@ -64,7 +64,7 @@ class ExternalIntents {
anime = getAnime.await(animeId!!) ?: return null
source = sourceManager.get(anime.source) ?: return null
episode = getEpisodeByAnimeId.await(anime.id).find { it.id == episodeId } ?: return null
val video = EpisodeLoader.getLinks(episode.toDbEpisode(), anime, source).asFlow().first()[0]
val video = EpisodeLoader.getLinks(episode, anime, source).asFlow().first()[0]
val videoUrl = if (video.videoUrl == null) {
makeErrorToast(context, Exception("video URL is null."))

View file

@ -234,7 +234,7 @@ class PlayerViewModel(
val currentEpisode = currentEpisode ?: throw Exception("No episode loaded.")
currentVideoList = EpisodeLoader.getLinks(currentEpisode, anime, source).asFlow().first()
currentVideoList = EpisodeLoader.getLinks(currentEpisode.toDomainEpisode()!!, anime, source).asFlow().first()
episodeId = currentEpisode.id!!
Pair(currentVideoList, Result.success(true))
@ -251,7 +251,7 @@ class PlayerViewModel(
fun isEpisodeOnline(): Boolean? {
val anime = anime ?: return null
val episode = currentEpisode ?: return null
return source is AnimeHttpSource && !EpisodeLoader.isDownloaded(episode, anime)
return source is AnimeHttpSource && !EpisodeLoader.isDownloaded(episode.toDomainEpisode()!!, anime)
}
suspend fun nextEpisode(): Pair<List<Video>?, String?>? {
@ -265,7 +265,7 @@ class PlayerViewModel(
return withIOContext {
try {
val currentEpisode = currentEpisode ?: throw Exception("No episode loaded.")
currentVideoList = EpisodeLoader.getLinks(currentEpisode, anime, source).asFlow().first()
currentVideoList = EpisodeLoader.getLinks(currentEpisode.toDomainEpisode()!!, anime, source).asFlow().first()
episodeId = currentEpisode.id!!
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { e.message ?: "Error getting links." }
@ -286,7 +286,7 @@ class PlayerViewModel(
return withIOContext {
try {
val currentEpisode = currentEpisode ?: throw Exception("No episode loaded.")
currentVideoList = EpisodeLoader.getLinks(currentEpisode, anime, source).asFlow().first()
currentVideoList = EpisodeLoader.getLinks(currentEpisode.toDomainEpisode()!!, anime, source).asFlow().first()
episodeId = currentEpisode.id!!
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { e.message ?: "Error getting links." }
@ -337,8 +337,8 @@ class PlayerViewModel(
val currentEpisode = currentEpisode ?: return
val nextEpisode = episodeList[getCurrentEpisodeIndex() + 1]
viewModelScope.launchIO {
if (EpisodeLoader.isDownloaded(currentEpisode, anime) &&
EpisodeLoader.isDownloaded(nextEpisode, anime)
if (EpisodeLoader.isDownloaded(currentEpisode.toDomainEpisode()!!, anime) &&
EpisodeLoader.isDownloaded(nextEpisode.toDomainEpisode()!!, anime)
) {
return@launchIO
}

View file

@ -11,6 +11,7 @@ import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.anime.interactor.GetAnime
import eu.kanade.domain.items.episode.interactor.GetEpisode
import eu.kanade.domain.items.episode.interactor.SetSeenStatus
@ -59,6 +60,7 @@ class AnimeUpdatesScreenModel(
private val getAnime: GetAnime = Injekt.get(),
private val getEpisode: GetEpisode = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
internal val downloadPreferences: DownloadPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
uiPreferences: UiPreferences = Injekt.get(),
) : StateScreenModel<AnimeUpdatesState>(AnimeUpdatesState()) {
@ -175,12 +177,6 @@ class AnimeUpdatesScreenModel(
val episodeId = items.singleOrNull()?.update?.episodeId ?: return@launch
startDownloadingNow(episodeId)
}
EpisodeDownloadAction.START_ALT -> {
downloadEpisodes(items, alt = true)
if (items.any { it.downloadStateProvider() == AnimeDownload.State.ERROR }) {
AnimeDownloadService.start(Injekt.get<Application>())
}
}
EpisodeDownloadAction.CANCEL -> {
val episodeId = items.singleOrNull()?.update?.episodeId ?: return@launch
cancelDownload(episodeId)
@ -188,6 +184,10 @@ class AnimeUpdatesScreenModel(
EpisodeDownloadAction.DELETE -> {
deleteEpisodes(items)
}
EpisodeDownloadAction.SHOW_OPTIONS -> {
val update = items.singleOrNull()?.update ?: return@launch
showOptionsDialog(update)
}
}
toggleAllSelection(false)
}
@ -276,6 +276,10 @@ class AnimeUpdatesScreenModel(
setDialog(Dialog.DeleteConfirmation(updatesItem))
}
private fun showOptionsDialog(update: AnimeUpdatesWithRelations) {
setDialog(Dialog.Options(update.episodeId, update.animeId, update.sourceId))
}
fun toggleSelection(
item: AnimeUpdatesItem,
selected: Boolean,
@ -375,6 +379,7 @@ class AnimeUpdatesScreenModel(
sealed class Dialog {
data class DeleteConfirmation(val toDelete: List<AnimeUpdatesItem>) : Dialog()
data class Options(val episodeId: Long, val animeId: Long, val sourceId: Long) : Dialog()
}
sealed class Event {

View file

@ -17,7 +17,10 @@ import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
import eu.kanade.presentation.entries.anime.onDismissEpisodeOptionsDialogScreen
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
import eu.kanade.presentation.updates.anime.AnimeUpdateScreen
import eu.kanade.tachiyomi.R
@ -97,6 +100,18 @@ fun Screen.animeUpdatesTab(
isManga = false,
)
}
is AnimeUpdatesScreenModel.Dialog.Options -> {
onDismissEpisodeOptionsDialogScreen = onDismissDialog
NavigatorAdaptiveSheet(
screen = EpisodeOptionsDialogScreen(
episodeId = dialog.episodeId,
animeId = dialog.animeId,
sourceId = dialog.sourceId,
useExternalDownloader = screenModel.downloadPreferences.useExternalDownloader().get(),
),
onDismissRequest = onDismissDialog,
)
}
null -> {}
}

View file

@ -285,4 +285,6 @@
<string name="label_migration_manga">Migrate Manga</string>
<string name="label_migration_anime">Migrate Anime</string>
<string name="settings">Settings</string>
<string name="copied_video_link_to_clipboard">Copied video quality link to clipboard</string>
<string name="choose_video_quality">Choose video quality:</string>
</resources>