mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-28 09:15:12 +03:00
add video quality dialog to episode downloads
This commit is contained in:
parent
24e9e1846d
commit
1d1aa51de3
15 changed files with 369 additions and 174 deletions
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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."))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 -> {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue