Merge branch 'master' into MR

This commit is contained in:
LuftVerbot 2023-11-15 21:11:51 +01:00
commit 563efef3c9
11 changed files with 320 additions and 230 deletions

View file

@ -21,7 +21,7 @@ android {
defaultConfig {
applicationId = "xyz.jmir.tachiyomi.mi"
versionCode = 105
versionCode = 106
versionName = "0.14.6"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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,
)
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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"),
;

View file

@ -62,7 +62,7 @@ fun SubtitleSettingsSheet(
.verticalScroll(rememberScrollState()),
) {
when (page) {
0 -> SubtitleDelayPage(screenModel)
0 -> StreamsDelayPage(screenModel)
1 -> SubtitleFontPage(screenModel)
2 -> SubtitleColorPage(screenModel)
}

View file

@ -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() }

View file

@ -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(),

View file

@ -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

View file

@ -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>