mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
Merge branch 'master' into MR
This commit is contained in:
commit
563efef3c9
11 changed files with 320 additions and 230 deletions
|
@ -21,7 +21,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "xyz.jmir.tachiyomi.mi"
|
||||
|
||||
versionCode = 105
|
||||
versionCode = 106
|
||||
versionName = "0.14.6"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
|
|
|
@ -55,7 +55,7 @@ import eu.kanade.tachiyomi.ui.player.settings.dialogs.SkipIntroLengthDialog
|
|||
import eu.kanade.tachiyomi.ui.player.settings.dialogs.SpeedPickerDialog
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.PlayerSettingsSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.ScreenshotOptionsSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.TracksCatalogSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.StreamsCatalogSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.VideoChaptersSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle.SubtitleSettingsSheet
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle.toHexString
|
||||
|
@ -138,7 +138,13 @@ class PlayerActivity : BaseActivity() {
|
|||
setInitialEpisodeError(exception)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch { setVideoList(qualityIndex = 0, initResult.first!!) }
|
||||
lifecycleScope.launch {
|
||||
setVideoList(
|
||||
qualityIndex = initResult.first.videoIndex,
|
||||
videos = initResult.first.videoList,
|
||||
position = initResult.first.position,
|
||||
)
|
||||
}
|
||||
}
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
@ -247,22 +253,19 @@ class PlayerActivity : BaseActivity() {
|
|||
|
||||
private val animationHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val streams: PlayerViewModel.VideoStreams
|
||||
get() = viewModel.state.value.videoStreams
|
||||
|
||||
private var currentVideoList: List<Video>? = null
|
||||
set(list) {
|
||||
field = list
|
||||
streams.quality.tracks = field?.map { Track("", it.quality) }?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
|
||||
private var playerIsDestroyed = true
|
||||
|
||||
private var selectedQualityIndex = 0
|
||||
|
||||
private var subtitleTracks: Array<Track> = emptyArray()
|
||||
|
||||
private var selectedSubtitleIndex = 0
|
||||
|
||||
private var hadPreviousSubs = false
|
||||
|
||||
private var audioTracks: Array<Track> = emptyArray()
|
||||
|
||||
private var selectedAudioIndex = 0
|
||||
|
||||
private var hadPreviousAudio = false
|
||||
|
||||
private var videoChapters: List<VideoChapter> = emptyList()
|
||||
|
@ -403,23 +406,24 @@ class PlayerActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
is PlayerViewModel.Sheet.TracksCatalog -> {
|
||||
val qualityTracks = currentVideoList?.map { Track("", it.quality) }?.toTypedArray()?.takeUnless { it.isEmpty() }
|
||||
val subtitleTracks = subtitleTracks.takeUnless { it.isEmpty() }
|
||||
val audioTracks = audioTracks.takeUnless { it.isEmpty() }
|
||||
is PlayerViewModel.Sheet.StreamsCatalog -> {
|
||||
val qualityTracks = streams.quality.tracks.takeUnless { it.isEmpty() }
|
||||
val subtitleTracks = streams.subtitle.tracks.takeUnless { it.isEmpty() }
|
||||
val audioTracks = streams.audio.tracks.takeUnless { it.isEmpty() }
|
||||
|
||||
if (qualityTracks != null && subtitleTracks != null && audioTracks != null) {
|
||||
fun onQualitySelected(qualityIndex: Int) {
|
||||
if (playerIsDestroyed) return
|
||||
if (selectedQualityIndex == qualityIndex) return
|
||||
if (streams.quality.index == qualityIndex) return
|
||||
showLoadingIndicator(true)
|
||||
viewModel.qualityIndex = qualityIndex
|
||||
logcat(LogPriority.INFO) { "Changing quality" }
|
||||
setVideoList(qualityIndex, currentVideoList)
|
||||
}
|
||||
|
||||
fun onSubtitleSelected(index: Int) {
|
||||
if (selectedSubtitleIndex == index || selectedSubtitleIndex > subtitleTracks.lastIndex) return
|
||||
selectedSubtitleIndex = index
|
||||
if (streams.subtitle.index == index || streams.subtitle.index > subtitleTracks.lastIndex) return
|
||||
streams.subtitle.index = index
|
||||
if (index == 0) {
|
||||
player.sid = -1
|
||||
return
|
||||
|
@ -434,8 +438,8 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
fun onAudioSelected(index: Int) {
|
||||
if (selectedAudioIndex == index || selectedAudioIndex > audioTracks.lastIndex) return
|
||||
selectedAudioIndex = index
|
||||
if (streams.audio.index == index || streams.audio.index > audioTracks.lastIndex) return
|
||||
streams.audio.index = index
|
||||
if (index == 0) {
|
||||
player.aid = -1
|
||||
return
|
||||
|
@ -449,14 +453,10 @@ class PlayerActivity : BaseActivity() {
|
|||
?: MPVLib.command(arrayOf("audio-add", audioTracks[index].url, "select", audioTracks[index].url))
|
||||
}
|
||||
|
||||
TracksCatalogSheet(
|
||||
StreamsCatalogSheet(
|
||||
isEpisodeOnline = viewModel.isEpisodeOnline(),
|
||||
qualityTracks = qualityTracks,
|
||||
subtitleTracks = subtitleTracks,
|
||||
audioTracks = audioTracks,
|
||||
selectedQualityIndex = selectedQualityIndex,
|
||||
selectedSubtitleIndex = selectedSubtitleIndex,
|
||||
selectedAudioIndex = selectedAudioIndex,
|
||||
videoStreams = viewModel.state.collectAsState().value.videoStreams,
|
||||
openContentFd = ::openContentFd,
|
||||
onQualitySelected = ::onQualitySelected,
|
||||
onSubtitleSelected = ::onSubtitleSelected,
|
||||
onAudioSelected = ::onAudioSelected,
|
||||
|
@ -468,7 +468,7 @@ class PlayerActivity : BaseActivity() {
|
|||
|
||||
is PlayerViewModel.Sheet.SubtitleSettings -> {
|
||||
SubtitleSettingsSheet(
|
||||
screenModel = PlayerSettingsScreenModel(viewModel.playerPreferences, subtitleTracks.size > 1),
|
||||
screenModel = PlayerSettingsScreenModel(viewModel.playerPreferences, streams.subtitle.tracks.size > 1),
|
||||
onDismissRequest = pauseForDialogSheet(fadeControls = true),
|
||||
)
|
||||
}
|
||||
|
@ -1381,28 +1381,32 @@ class PlayerActivity : BaseActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
private fun setVideoList(qualityIndex: Int, videos: List<Video>?, fromStart: Boolean = false) {
|
||||
private fun setVideoList(qualityIndex: Int, videos: List<Video>?, fromStart: Boolean = false, position: Long? = null) {
|
||||
if (playerIsDestroyed) return
|
||||
currentVideoList = videos
|
||||
currentVideoList?.getOrNull(qualityIndex)?.let {
|
||||
selectedQualityIndex = qualityIndex
|
||||
streams.quality.index = qualityIndex
|
||||
setHttpOptions(it)
|
||||
if (viewModel.state.value.isLoadingEpisode) {
|
||||
viewModel.currentEpisode?.let { episode ->
|
||||
val preservePos = playerPreferences.preserveWatchingPosition().get()
|
||||
if ((episode.seen && !preservePos) || fromStart) {
|
||||
episode.last_second_seen = 1L
|
||||
val resumePosition = if (position != null) {
|
||||
position
|
||||
} else if ((episode.seen && !preservePos) || fromStart) {
|
||||
0L
|
||||
} else {
|
||||
episode.last_second_seen
|
||||
}
|
||||
MPVLib.command(arrayOf("set", "start", "${episode.last_second_seen / 1000F}"))
|
||||
playerControls.updatePlaybackDuration(episode.total_seconds.toInt() / 1000)
|
||||
MPVLib.command(arrayOf("set", "start", "${resumePosition / 1000F}"))
|
||||
playerControls.updatePlaybackDuration(resumePosition.toInt() / 1000)
|
||||
}
|
||||
} else {
|
||||
player.timePos?.let {
|
||||
MPVLib.command(arrayOf("set", "start", "${player.timePos}"))
|
||||
}
|
||||
}
|
||||
subtitleTracks = arrayOf(Track("nothing", "None")) + it.subtitleTracks.toTypedArray()
|
||||
audioTracks = arrayOf(Track("nothing", "None")) + it.audioTracks.toTypedArray()
|
||||
streams.subtitle.tracks = arrayOf(Track("nothing", "None")) + it.subtitleTracks.toTypedArray()
|
||||
streams.audio.tracks = arrayOf(Track("nothing", "None")) + it.audioTracks.toTypedArray()
|
||||
MPVLib.command(arrayOf("loadfile", parseVideoUrl(it.videoUrl)))
|
||||
}
|
||||
refreshUi()
|
||||
|
@ -1487,20 +1491,20 @@ class PlayerActivity : BaseActivity() {
|
|||
val localLangName = LocaleHelper.getSimpleLocaleDisplayName()
|
||||
clearTracks()
|
||||
player.loadTracks()
|
||||
subtitleTracks += player.tracks.getOrElse("sub") { emptyList() }
|
||||
streams.subtitle.tracks += player.tracks.getOrElse("sub") { emptyList() }
|
||||
.drop(1).map { track ->
|
||||
Track(track.mpvId.toString(), track.name)
|
||||
}.toTypedArray()
|
||||
audioTracks += player.tracks.getOrElse("audio") { emptyList() }
|
||||
streams.audio.tracks += player.tracks.getOrElse("audio") { emptyList() }
|
||||
.drop(1).map { track ->
|
||||
Track(track.mpvId.toString(), track.name)
|
||||
}.toTypedArray()
|
||||
if (hadPreviousSubs) {
|
||||
subtitleTracks.getOrNull(selectedSubtitleIndex)?.let { sub ->
|
||||
streams.subtitle.tracks.getOrNull(streams.subtitle.index)?.let { sub ->
|
||||
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
|
||||
}
|
||||
} else {
|
||||
currentVideoList?.getOrNull(selectedQualityIndex)
|
||||
currentVideoList?.getOrNull(streams.quality.index)
|
||||
?.subtitleTracks?.let { tracks ->
|
||||
val langIndex = tracks.indexOfFirst {
|
||||
it.lang.contains(localLangName)
|
||||
|
@ -1508,23 +1512,23 @@ class PlayerActivity : BaseActivity() {
|
|||
val requestedLanguage = if (langIndex == -1) 0 else langIndex
|
||||
tracks.getOrNull(requestedLanguage)?.let { sub ->
|
||||
hadPreviousSubs = true
|
||||
selectedSubtitleIndex = requestedLanguage + 1
|
||||
streams.subtitle.index = requestedLanguage + 1
|
||||
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
|
||||
}
|
||||
} ?: run {
|
||||
val mpvSub = player.tracks.getOrElse("sub") { emptyList() }
|
||||
.firstOrNull { player.sid == it.mpvId }
|
||||
selectedSubtitleIndex = mpvSub?.let {
|
||||
subtitleTracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
|
||||
streams.subtitle.index = mpvSub?.let {
|
||||
streams.subtitle.tracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
|
||||
}?.coerceAtLeast(0) ?: 0
|
||||
}
|
||||
}
|
||||
if (hadPreviousAudio) {
|
||||
audioTracks.getOrNull(selectedAudioIndex)?.let { audio ->
|
||||
streams.audio.tracks.getOrNull(streams.audio.index)?.let { audio ->
|
||||
MPVLib.command(arrayOf("audio-add", audio.url, "select", audio.url))
|
||||
}
|
||||
} else {
|
||||
currentVideoList?.getOrNull(selectedQualityIndex)
|
||||
currentVideoList?.getOrNull(streams.quality.index)
|
||||
?.audioTracks?.let { tracks ->
|
||||
val langIndex = tracks.indexOfFirst {
|
||||
it.lang.contains(localLangName)
|
||||
|
@ -1532,14 +1536,14 @@ class PlayerActivity : BaseActivity() {
|
|||
val requestedLanguage = if (langIndex == -1) 0 else langIndex
|
||||
tracks.getOrNull(requestedLanguage)?.let { audio ->
|
||||
hadPreviousAudio = true
|
||||
selectedAudioIndex = requestedLanguage + 1
|
||||
streams.audio.index = requestedLanguage + 1
|
||||
MPVLib.command(arrayOf("audio-add", audio.url, "select", audio.url))
|
||||
}
|
||||
} ?: run {
|
||||
val mpvAudio = player.tracks.getOrElse("audio") { emptyList() }
|
||||
.firstOrNull { player.aid == it.mpvId }
|
||||
selectedAudioIndex = mpvAudio?.let {
|
||||
audioTracks.indexOfFirst { it.url == mpvAudio.mpvId.toString() }
|
||||
streams.audio.index = mpvAudio?.let {
|
||||
streams.audio.tracks.indexOfFirst { it.url == mpvAudio.mpvId.toString() }
|
||||
}?.coerceAtLeast(0) ?: 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import eu.kanade.domain.track.anime.store.DelayedAnimeTrackingStore
|
|||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.database.models.anime.Episode
|
||||
|
@ -48,12 +49,7 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -81,8 +77,8 @@ import uy.kohesive.injekt.api.get
|
|||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
|
||||
class PlayerViewModel(
|
||||
private val savedState: SavedStateHandle = SavedStateHandle(),
|
||||
class PlayerViewModel @JvmOverloads constructor(
|
||||
private val savedState: SavedStateHandle,
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
||||
private val imageSaver: ImageSaver = Injekt.get(),
|
||||
|
@ -139,10 +135,28 @@ class PlayerViewModel(
|
|||
val currentSource: AnimeSource?
|
||||
get() = state.value.source
|
||||
|
||||
/**
|
||||
* The position in the current video. Used to restore from process kill.
|
||||
*/
|
||||
private var episodePosition = savedState.get<Long>("episode_position") ?: 0L
|
||||
set(value) {
|
||||
savedState["episode_position"] = value
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* The current video's quality index. Used to restore from process kill.
|
||||
*/
|
||||
var qualityIndex = savedState.get<Int>("quality_index") ?: 0
|
||||
set(value) {
|
||||
savedState["quality_index"] = value
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* The episode id of the currently loaded episode. Used to restore from process kill.
|
||||
*/
|
||||
private var savedEpisodeId = savedState.get<Long>("episode_id") ?: -1L
|
||||
private var episodeId = savedState.get<Long>("episode_id") ?: -1L
|
||||
set(value) {
|
||||
savedState["episode_id"] = value
|
||||
field = value
|
||||
|
@ -152,12 +166,10 @@ class PlayerViewModel(
|
|||
|
||||
private var currentVideoList: List<Video>? = null
|
||||
|
||||
private var requestedSecond: Long = 0L
|
||||
|
||||
private fun filterEpisodeList(episodes: List<Episode>): List<Episode> {
|
||||
val anime = currentAnime ?: return episodes
|
||||
val selectedEpisode = episodes.find { it.id == savedEpisodeId }
|
||||
?: error("Requested episode of id $savedEpisodeId not found in episode list")
|
||||
val selectedEpisode = episodes.find { it.id == episodeId }
|
||||
?: error("Requested episode of id $episodeId not found in episode list")
|
||||
|
||||
val episodesForPlayer = episodes.filterNot {
|
||||
anime.unseenFilterRaw == Anime.EPISODE_SHOW_SEEN && !it.seen ||
|
||||
|
@ -168,7 +180,7 @@ class PlayerViewModel(
|
|||
anime.bookmarkedFilterRaw == Anime.EPISODE_SHOW_NOT_BOOKMARKED && it.bookmark
|
||||
}.toMutableList()
|
||||
|
||||
if (episodesForPlayer.all { it.id != savedEpisodeId }) {
|
||||
if (episodesForPlayer.all { it.id != episodeId }) {
|
||||
episodesForPlayer += listOf(selectedEpisode)
|
||||
}
|
||||
|
||||
|
@ -209,38 +221,26 @@ class PlayerViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// To save state
|
||||
state.map { currentEpisode }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach { currentEpisode ->
|
||||
if (!currentEpisode.seen) {
|
||||
requestedSecond = currentEpisode.last_second_seen
|
||||
}
|
||||
savedEpisodeId = currentEpisode.id!!
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this presenter is initialized yet.
|
||||
*/
|
||||
private fun needsInit(animeId: Long, episodeId: Long): Boolean {
|
||||
return animeId != currentAnime?.id || episodeId != currentEpisode?.id
|
||||
private fun needsInit(): Boolean {
|
||||
return currentAnime == null || currentEpisode == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this presenter with the given [animeId] and [episodeId]. This method will
|
||||
* Initializes this presenter with the given [animeId] and [initialEpisodeId]. This method will
|
||||
* fetch the anime from the database and initialize the episode.
|
||||
*/
|
||||
suspend fun init(animeId: Long, episodeId: Long): Pair<List<Video>?, Result<Boolean>> {
|
||||
if (!needsInit(animeId, episodeId)) return Pair(currentVideoList, Result.success(true))
|
||||
suspend fun init(animeId: Long, initialEpisodeId: Long): Pair<InitResult, Result<Boolean>> {
|
||||
val defaultResult = InitResult(currentVideoList, 0, null)
|
||||
if (!needsInit()) return Pair(defaultResult, Result.success(true))
|
||||
return try {
|
||||
val anime = getAnime.await(animeId)
|
||||
if (anime != null) {
|
||||
if (episodeId == -1L) episodeId = initialEpisodeId
|
||||
|
||||
checkTrackers(anime)
|
||||
savedEpisodeId = episodeId
|
||||
|
||||
mutableState.update { it.copy(episodeList = initEpisodeList(anime)) }
|
||||
val episode = this.currentPlaylist.first { it.id == episodeId }
|
||||
|
@ -258,18 +258,28 @@ class PlayerViewModel(
|
|||
currentVideoList = null
|
||||
throw Exception("Video list is empty.")
|
||||
}
|
||||
savedEpisodeId = currentEp.id!!
|
||||
|
||||
Pair(currentVideoList, Result.success(true))
|
||||
val result = InitResult(
|
||||
videoList = currentVideoList,
|
||||
videoIndex = qualityIndex,
|
||||
position = episodePosition,
|
||||
)
|
||||
Pair(result, Result.success(true))
|
||||
} else {
|
||||
// Unlikely but okay
|
||||
Pair(currentVideoList, Result.success(false))
|
||||
Pair(defaultResult, Result.success(false))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Pair(currentVideoList, Result.failure(e))
|
||||
Pair(defaultResult, Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
data class InitResult(
|
||||
val videoList: List<Video>?,
|
||||
val videoIndex: Int,
|
||||
val position: Long?,
|
||||
)
|
||||
|
||||
private fun initEpisodeList(anime: Anime): List<Episode> {
|
||||
val episodes = runBlocking { getEpisodeByAnimeId.await(anime.id) }
|
||||
|
||||
|
@ -310,7 +320,7 @@ class PlayerViewModel(
|
|||
try {
|
||||
val currentEpisode = currentEpisode ?: throw Exception("No episode loaded.")
|
||||
currentVideoList = EpisodeLoader.getLinks(currentEpisode.toDomainEpisode()!!, anime, source).asFlow().first()
|
||||
savedEpisodeId = currentEpisode.id!!
|
||||
this@PlayerViewModel.episodeId = currentEpisode.id!!
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { e.message ?: "Error getting links" }
|
||||
}
|
||||
|
@ -326,7 +336,7 @@ class PlayerViewModel(
|
|||
fun onSecondReached(position: Int, duration: Int) {
|
||||
if (state.value.isLoadingEpisode) return
|
||||
val currentEp = currentEpisode ?: return
|
||||
if (savedEpisodeId == -1L) return
|
||||
if (episodeId == -1L) return
|
||||
|
||||
val seconds = position * 1000L
|
||||
val totalSeconds = duration * 1000L
|
||||
|
@ -334,6 +344,8 @@ class PlayerViewModel(
|
|||
currentEp.last_second_seen = seconds
|
||||
currentEp.total_seconds = totalSeconds
|
||||
|
||||
episodePosition = seconds
|
||||
|
||||
val progress = playerPreferences.progressPreference().get()
|
||||
val shouldTrack = !incognitoMode || hasTrackers
|
||||
if (seconds >= totalSeconds * progress && shouldTrack) {
|
||||
|
@ -712,8 +724,8 @@ class PlayerViewModel(
|
|||
mutableState.update { it.copy(sheet = Sheet.VideoChapters) }
|
||||
}
|
||||
|
||||
fun showTracksCatalog() {
|
||||
mutableState.update { it.copy(sheet = Sheet.TracksCatalog) }
|
||||
fun showStreamsCatalog() {
|
||||
mutableState.update { it.copy(sheet = Sheet.StreamsCatalog) }
|
||||
}
|
||||
|
||||
fun closeDialogSheet() {
|
||||
|
@ -726,11 +738,17 @@ class PlayerViewModel(
|
|||
val episode: Episode? = null,
|
||||
val anime: Anime? = null,
|
||||
val source: AnimeSource? = null,
|
||||
val videoStreams: VideoStreams = VideoStreams(),
|
||||
val isLoadingEpisode: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
val sheet: Sheet? = null,
|
||||
)
|
||||
|
||||
class VideoStreams(val quality: Stream, val subtitle: Stream, val audio: Stream) {
|
||||
constructor() : this(Stream(), Stream(), Stream())
|
||||
class Stream(var index: Int = 0, var tracks: Array<Track> = emptyArray())
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object EpisodeList : Dialog()
|
||||
object SpeedPicker : Dialog()
|
||||
|
@ -742,13 +760,12 @@ class PlayerViewModel(
|
|||
object ScreenshotOptions : Sheet()
|
||||
object PlayerSettings : Sheet()
|
||||
object VideoChapters : Sheet()
|
||||
object TracksCatalog : Sheet()
|
||||
object StreamsCatalog : Sheet()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
data class SetAnimeSkipIntro(val duration: Int) : Event()
|
||||
data class SetCoverResult(val result: SetAsCover) : Event()
|
||||
|
||||
data class SavedImage(val result: SaveImageResult) : Event()
|
||||
data class ShareImage(val uri: Uri, val seconds: String) : Event()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
package eu.kanade.tachiyomi.ui.player.settings.sheets
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerViewModel
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheetDialogPadding
|
||||
import `is`.xyz.mpv.MPVLib
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun StreamsCatalogSheet(
|
||||
isEpisodeOnline: Boolean?,
|
||||
videoStreams: PlayerViewModel.VideoStreams,
|
||||
openContentFd: (Uri) -> String?,
|
||||
onQualitySelected: (Int) -> Unit,
|
||||
onSubtitleSelected: (Int) -> Unit,
|
||||
onAudioSelected: (Int) -> Unit,
|
||||
onSettingsClicked: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val tabTitles = mutableListOf(
|
||||
stringResource(id = R.string.subtitle_dialog_header),
|
||||
stringResource(id = R.string.audio_dialog_header),
|
||||
)
|
||||
if (isEpisodeOnline == true) {
|
||||
tabTitles.add(0, stringResource(id = R.string.quality_dialog_header))
|
||||
}
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = tabTitles,
|
||||
onOverflowMenuClicked = onSettingsClicked,
|
||||
overflowIcon = Icons.Outlined.Settings,
|
||||
hideSystemBars = true,
|
||||
) { contentPadding, page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
@Composable fun QualityTracksPage() = StreamsPageBuilder(
|
||||
externalTrackCode = null,
|
||||
stream = videoStreams.quality,
|
||||
openContentFd = openContentFd,
|
||||
onTrackSelected = onQualitySelected,
|
||||
)
|
||||
|
||||
@Composable fun SubtitleTracksPage() = StreamsPageBuilder(
|
||||
externalTrackCode = "sub",
|
||||
stream = videoStreams.subtitle,
|
||||
openContentFd = openContentFd,
|
||||
onTrackSelected = onSubtitleSelected,
|
||||
)
|
||||
|
||||
@Composable fun AudioTracksPage() = StreamsPageBuilder(
|
||||
externalTrackCode = "audio",
|
||||
stream = videoStreams.audio,
|
||||
openContentFd = openContentFd,
|
||||
onTrackSelected = onAudioSelected,
|
||||
)
|
||||
|
||||
when (page) {
|
||||
0 -> if (isEpisodeOnline == true) QualityTracksPage() else SubtitleTracksPage()
|
||||
1 -> if (isEpisodeOnline == true) SubtitleTracksPage() else AudioTracksPage()
|
||||
2 -> if (isEpisodeOnline == true) AudioTracksPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StreamsPageBuilder(
|
||||
externalTrackCode: String?,
|
||||
stream: PlayerViewModel.VideoStreams.Stream,
|
||||
openContentFd: (Uri) -> String?,
|
||||
onTrackSelected: (Int) -> Unit,
|
||||
) {
|
||||
var tracks by remember { mutableStateOf(stream.tracks) }
|
||||
var index by remember { mutableStateOf(stream.index) }
|
||||
|
||||
val onSelected: (Int) -> Unit = {
|
||||
onTrackSelected(it)
|
||||
index = it
|
||||
stream.index = it
|
||||
}
|
||||
|
||||
if (externalTrackCode != null) {
|
||||
val addExternalTrack = rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
return if (externalTrackCode == "audio") {
|
||||
Intent.createChooser(intent, context.getString(R.string.player_add_external_audio_intent))
|
||||
} else {
|
||||
Intent.createChooser(intent, context.getString(R.string.player_add_external_subtitles_intent))
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (it != null) {
|
||||
val url = it.toString()
|
||||
val path = if (url.startsWith("content://")) {
|
||||
openContentFd(Uri.parse(url))
|
||||
} else {
|
||||
url
|
||||
} ?: return@rememberLauncherForActivityResult
|
||||
MPVLib.command(arrayOf("$externalTrackCode-add", path, "cached"))
|
||||
val title = File(path).name
|
||||
tracks += Track(path, title)
|
||||
stream.tracks += Track(path, title)
|
||||
index = tracks.lastIndex
|
||||
stream.index = tracks.lastIndex
|
||||
}
|
||||
}
|
||||
|
||||
val addTrackRes = if (externalTrackCode == "sub") R.string.player_add_external_subtitles else R.string.player_add_external_audio
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { addExternalTrack.launch("*/*") })
|
||||
.padding(sheetDialogPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = MaterialTheme.padding.tiny),
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = addTrackRes),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = addTrackRes),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tracks.forEachIndexed { i, track ->
|
||||
val selected = index == i
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onSelected(i) })
|
||||
.padding(sheetDialogPadding),
|
||||
) {
|
||||
Text(
|
||||
text = track.lang,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
|
||||
fontStyle = if (selected) FontStyle.Italic else FontStyle.Normal,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.player.settings.sheets
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.ui.player.settings.sheetDialogPadding
|
||||
|
||||
@Composable
|
||||
fun TracksCatalogSheet(
|
||||
isEpisodeOnline: Boolean?,
|
||||
qualityTracks: Array<Track>,
|
||||
subtitleTracks: Array<Track>,
|
||||
audioTracks: Array<Track>,
|
||||
selectedQualityIndex: Int,
|
||||
selectedSubtitleIndex: Int,
|
||||
selectedAudioIndex: Int,
|
||||
onQualitySelected: (Int) -> Unit,
|
||||
onSubtitleSelected: (Int) -> Unit,
|
||||
onAudioSelected: (Int) -> Unit,
|
||||
onSettingsClicked: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val tabTitles = mutableListOf(
|
||||
stringResource(id = R.string.subtitle_dialog_header),
|
||||
stringResource(id = R.string.audio_dialog_header),
|
||||
)
|
||||
if (isEpisodeOnline == true) {
|
||||
tabTitles.add(0, stringResource(id = R.string.quality_dialog_header))
|
||||
}
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = tabTitles,
|
||||
onOverflowMenuClicked = onSettingsClicked,
|
||||
overflowIcon = Icons.Outlined.Settings,
|
||||
hideSystemBars = true,
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
@Composable fun QualityTracksPage() = TracksPageBuilder(
|
||||
tracks = qualityTracks,
|
||||
selectedTrackIndex = selectedQualityIndex,
|
||||
onTrackSelected = onQualitySelected,
|
||||
)
|
||||
|
||||
@Composable fun SubtitleTracksPage() = TracksPageBuilder(
|
||||
tracks = subtitleTracks,
|
||||
selectedTrackIndex = selectedSubtitleIndex,
|
||||
onTrackSelected = onSubtitleSelected,
|
||||
)
|
||||
|
||||
@Composable fun AudioTracksPage() = TracksPageBuilder(
|
||||
tracks = audioTracks,
|
||||
selectedTrackIndex = selectedAudioIndex,
|
||||
onTrackSelected = onAudioSelected,
|
||||
)
|
||||
|
||||
when (page) {
|
||||
0 -> if (isEpisodeOnline == true) QualityTracksPage() else SubtitleTracksPage()
|
||||
1 -> if (isEpisodeOnline == true) SubtitleTracksPage() else AudioTracksPage()
|
||||
2 -> if (isEpisodeOnline == true) AudioTracksPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TracksPageBuilder(
|
||||
tracks: Array<Track>,
|
||||
selectedTrackIndex: Int,
|
||||
onTrackSelected: (Int) -> Unit,
|
||||
) {
|
||||
var selectedIndex by remember { mutableStateOf(selectedTrackIndex) }
|
||||
|
||||
val onSelected: (Int) -> Unit = { index ->
|
||||
onTrackSelected(index)
|
||||
selectedIndex = index
|
||||
}
|
||||
|
||||
tracks.forEachIndexed { index, track ->
|
||||
val selected = selectedIndex == index
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onSelected(index) })
|
||||
.padding(sheetDialogPadding),
|
||||
) {
|
||||
Text(
|
||||
text = track.lang,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
|
||||
fontStyle = if (selected) FontStyle.Italic else FontStyle.Normal,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import tachiyomi.presentation.core.components.OutlinedNumericChooser
|
|||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun SubtitleDelayPage(
|
||||
fun StreamsDelayPage(
|
||||
screenModel: PlayerSettingsScreenModel,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
|
||||
|
@ -31,13 +31,13 @@ fun SubtitleDelayPage(
|
|||
val subDelay by remember { mutableStateOf(screenModel.preferences.rememberSubtitlesDelay()) }
|
||||
var currentSubDelay by rememberSaveable {
|
||||
mutableStateOf(
|
||||
(MPVLib.getPropertyDouble(Tracks.SUBTITLES.mpvProperty) * 1000)
|
||||
(MPVLib.getPropertyDouble(Streams.SUBTITLES.mpvProperty) * 1000)
|
||||
.toInt(),
|
||||
)
|
||||
}
|
||||
var currentAudioDelay by rememberSaveable {
|
||||
mutableStateOf(
|
||||
(MPVLib.getPropertyDouble(Tracks.AUDIO.mpvProperty) * 1000)
|
||||
(MPVLib.getPropertyDouble(Streams.AUDIO.mpvProperty) * 1000)
|
||||
.toInt(),
|
||||
)
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ fun SubtitleDelayPage(
|
|||
value = currentAudioDelay,
|
||||
step = 100,
|
||||
onValueChanged = {
|
||||
MPVLib.setPropertyDouble(Tracks.AUDIO.mpvProperty, it / 1000.0)
|
||||
MPVLib.setPropertyDouble(Streams.AUDIO.mpvProperty, it / 1000.0)
|
||||
screenModel.preferences.audioDelay().set(it)
|
||||
currentAudioDelay = it
|
||||
},
|
||||
|
@ -86,7 +86,7 @@ fun SubtitleDelayPage(
|
|||
value = currentSubDelay,
|
||||
step = 100,
|
||||
onValueChanged = {
|
||||
MPVLib.setPropertyDouble(Tracks.SUBTITLES.mpvProperty, it / 1000.0)
|
||||
MPVLib.setPropertyDouble(Streams.SUBTITLES.mpvProperty, it / 1000.0)
|
||||
screenModel.preferences.subtitlesDelay().set(it)
|
||||
currentSubDelay = it
|
||||
},
|
||||
|
@ -95,7 +95,7 @@ fun SubtitleDelayPage(
|
|||
}
|
||||
}
|
||||
|
||||
private enum class Tracks(val mpvProperty: String) {
|
||||
private enum class Streams(val mpvProperty: String) {
|
||||
SUBTITLES("sub-delay"),
|
||||
AUDIO("audio-delay"),
|
||||
;
|
||||
|
|
|
@ -62,7 +62,7 @@ fun SubtitleSettingsSheet(
|
|||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> SubtitleDelayPage(screenModel)
|
||||
0 -> StreamsDelayPage(screenModel)
|
||||
1 -> SubtitleFontPage(screenModel)
|
||||
2 -> SubtitleColorPage(screenModel)
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
|
||||
binding.settingsBtn.setOnClickListener { activity.viewModel.showPlayerSettings() }
|
||||
|
||||
binding.tracksBtn.setOnClickListener { activity.viewModel.showTracksCatalog() }
|
||||
binding.streamsBtn.setOnClickListener { activity.viewModel.showStreamsCatalog() }
|
||||
|
||||
binding.chaptersBtn.setOnClickListener { activity.viewModel.showVideoChapters() }
|
||||
|
||||
|
|
|
@ -86,8 +86,8 @@ import java.util.Date
|
|||
/**
|
||||
* Presenter used by the activity to perform background operations.
|
||||
*/
|
||||
class ReaderViewModel(
|
||||
private val savedState: SavedStateHandle = SavedStateHandle(),
|
||||
class ReaderViewModel @JvmOverloads constructor(
|
||||
private val savedState: SavedStateHandle,
|
||||
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||
private val downloadManager: MangaDownloadManager = Injekt.get(),
|
||||
private val downloadProvider: MangaDownloadProvider = Injekt.get(),
|
||||
|
|
|
@ -117,13 +117,13 @@
|
|||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="Settings"
|
||||
android:src="@drawable/ic_overflow_20dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/tracksBtn"
|
||||
app:layout_constraintLeft_toRightOf="@id/streamsBtn"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/episodeListBtn"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tracksBtn"
|
||||
android:id="@+id/streamsBtn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
|
@ -143,8 +143,8 @@
|
|||
android:src="@drawable/ic_video_chapter_20dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toRightOf="@id/toggleAutoplay"
|
||||
app:layout_constraintRight_toLeftOf="@id/tracksBtn"
|
||||
app:layout_constraintTop_toTopOf="@id/tracksBtn"
|
||||
app:layout_constraintRight_toLeftOf="@id/streamsBtn"
|
||||
app:layout_constraintTop_toTopOf="@id/streamsBtn"
|
||||
app:tint="?attr/colorOnPrimarySurface" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
|
|
|
@ -334,6 +334,10 @@
|
|||
<string name="player_subtitle_settings_font_tab">Font</string>
|
||||
<string name="player_subtitle_settings_color_tab">Color</string>
|
||||
<string name="player_subtitle_settings">Subtitle settings</string>
|
||||
<string name="player_add_external_audio">Add external audio</string>
|
||||
<string name="player_add_external_audio_intent">Select an audio file.</string>
|
||||
<string name="player_add_external_subtitles">Add external subtitles</string>
|
||||
<string name="player_add_external_subtitles_intent">Select a subtitle file.</string>
|
||||
<string name="player_subtitle_empty_warning">Has no effect because there aren\'t any subtitle tracks in this video</string>
|
||||
<string name="player_override_ass_subtitles">Override ASS subtitles</string>
|
||||
<string name="player_reset_subtitles">Reset subtitles to default</string>
|
||||
|
|
Loading…
Reference in a new issue