mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-01 09:09:29 +00:00
feat: libass
This commit is contained in:
parent
35428ca5fd
commit
bba83b13e4
10 changed files with 379 additions and 13 deletions
|
|
@ -210,6 +210,7 @@ kotlin {
|
|||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.google.code.gson:gson:2.11.0")
|
||||
implementation("io.github.peerless2012:ass-media:0.4.0-beta01")
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.androidx.media3.exoplayer.hls)
|
||||
implementation(libs.androidx.media3.exoplayer.dash)
|
||||
|
|
|
|||
|
|
@ -120,24 +120,47 @@ actual fun PlatformPlayerSurface(
|
|||
.setTsExtractorFlags(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)
|
||||
.setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE)
|
||||
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(
|
||||
PlatformPlaybackDataSourceFactory.create(
|
||||
val dataSourceFactory = PlatformPlaybackDataSourceFactory.create(
|
||||
defaultRequestHeaders = sanitizedSourceHeaders,
|
||||
defaultResponseHeaders = sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
|
||||
),
|
||||
extractorsFactory,
|
||||
)
|
||||
)
|
||||
|
||||
ExoPlayer.Builder(context)
|
||||
.setRenderersFactory(renderersFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.build().apply {
|
||||
val useLibass = playerSettings.useLibass
|
||||
val libassRenderType = runCatching {
|
||||
LibassRenderType.valueOf(playerSettings.libassRenderType)
|
||||
}.getOrDefault(LibassRenderType.CUES)
|
||||
|
||||
val player = if (useLibass) {
|
||||
ExoPlayer.Builder(context)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.buildWithAssSupportCompat(
|
||||
context = context,
|
||||
renderType = libassRenderType.toAssRenderType(),
|
||||
dataSourceFactory = dataSourceFactory,
|
||||
extractorsFactory = extractorsFactory,
|
||||
renderersFactory = renderersFactory
|
||||
)
|
||||
} else {
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
)
|
||||
|
||||
ExoPlayer.Builder(context)
|
||||
.setRenderersFactory(renderersFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.build()
|
||||
}
|
||||
|
||||
player.apply {
|
||||
if (!sourceAudioUrl.isNullOrBlank()) {
|
||||
val videoSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(sourceUrl))
|
||||
val audioSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
||||
val msf = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
val videoSource = msf.createMediaSource(MediaItem.fromUri(sourceUrl))
|
||||
val audioSource = msf.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
||||
setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
} else {
|
||||
setMediaItem(MediaItem.fromUri(sourceUrl))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ExtractorsFactory
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import androidx.media3.extractor.text.SubtitleParser
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.extractor.AssMatroskaExtractor
|
||||
import io.github.peerless2012.ass.media.kt.withAssSupport
|
||||
import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory
|
||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal fun ExoPlayer.Builder.buildWithAssSupportCompat(
|
||||
context: Context,
|
||||
renderType: AssRenderType = AssRenderType.CUES,
|
||||
dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(context),
|
||||
extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory(),
|
||||
renderersFactory: RenderersFactory = DefaultRenderersFactory(context)
|
||||
): ExoPlayer {
|
||||
val assHandler = AssHandler(renderType)
|
||||
val assSubtitleParserFactory = CompatAssSubtitleParserFactory(assHandler)
|
||||
val assExtractorsFactory = extractorsFactory.withAssMkvSupportCompat(
|
||||
subtitleParserFactory = assSubtitleParserFactory,
|
||||
assHandler = assHandler
|
||||
)
|
||||
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(
|
||||
dataSourceFactory,
|
||||
assExtractorsFactory
|
||||
)
|
||||
mediaSourceFactory.setSubtitleParserFactory(assSubtitleParserFactory)
|
||||
|
||||
val player = this
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setRenderersFactory(renderersFactory.withAssSupport(assHandler))
|
||||
.build()
|
||||
|
||||
assHandler.init(player)
|
||||
return player
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private class CompatAssSubtitleParserFactory(
|
||||
private val assHandler: AssHandler
|
||||
) : SubtitleParser.Factory {
|
||||
private val delegate = AssSubtitleParserFactory(assHandler)
|
||||
|
||||
override fun supportsFormat(format: Format): Boolean {
|
||||
return delegate.supportsFormat(normalizeSsaFormat(format))
|
||||
}
|
||||
|
||||
override fun getCueReplacementBehavior(format: Format): Int {
|
||||
return delegate.getCueReplacementBehavior(normalizeSsaFormat(format))
|
||||
}
|
||||
|
||||
override fun create(format: Format): SubtitleParser {
|
||||
return delegate.create(normalizeSsaFormat(format))
|
||||
}
|
||||
|
||||
private fun normalizeSsaFormat(format: Format): Format {
|
||||
val isSsaByCodecs = format.codecs == MimeTypes.TEXT_SSA
|
||||
val isSsaByMime = format.sampleMimeType == MimeTypes.TEXT_SSA
|
||||
if (isSsaByCodecs && !isSsaByMime) {
|
||||
return format.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.TEXT_SSA)
|
||||
.build()
|
||||
}
|
||||
return format
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun ExtractorsFactory.withAssMkvSupportCompat(
|
||||
subtitleParserFactory: SubtitleParser.Factory,
|
||||
assHandler: AssHandler
|
||||
): ExtractorsFactory {
|
||||
return ExtractorsFactory {
|
||||
val extractors = createExtractors()
|
||||
extractors.forEachIndexed { index, extractor ->
|
||||
if (extractor is MatroskaExtractor) {
|
||||
extractors[index] = AssMatroskaExtractor(subtitleParserFactory, assHandler)
|
||||
}
|
||||
}
|
||||
extractors
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||
|
||||
enum class LibassRenderType {
|
||||
CUES,
|
||||
EFFECTS_CANVAS,
|
||||
EFFECTS_OPEN_GL,
|
||||
OVERLAY_CANVAS,
|
||||
OVERLAY_OPEN_GL
|
||||
}
|
||||
|
||||
internal fun LibassRenderType.toAssRenderType(): AssRenderType {
|
||||
return when (this) {
|
||||
LibassRenderType.CUES -> AssRenderType.CUES
|
||||
LibassRenderType.EFFECTS_CANVAS -> AssRenderType.EFFECTS_CANVAS
|
||||
LibassRenderType.EFFECTS_OPEN_GL -> AssRenderType.EFFECTS_OPEN_GL
|
||||
LibassRenderType.OVERLAY_CANVAS -> AssRenderType.OVERLAY_CANVAS
|
||||
LibassRenderType.OVERLAY_OPEN_GL -> AssRenderType.OVERLAY_OPEN_GL
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,8 @@ actual object PlayerSettingsStorage {
|
|||
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
private const val useLibassKey = "use_libass"
|
||||
private const val libassRenderTypeKey = "libass_render_type"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
|
|
@ -460,4 +462,31 @@ actual object PlayerSettingsStorage {
|
|||
?.putFloat(ProfileScopedKey.of(nextEpisodeThresholdMinutesBeforeEndKey), minutes)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadUseLibass(): Boolean? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val key = ProfileScopedKey.of(useLibassKey)
|
||||
if (sharedPreferences.contains(key)) {
|
||||
sharedPreferences.getBoolean(key, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveUseLibass(enabled: Boolean) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putBoolean(ProfileScopedKey.of(useLibassKey), enabled)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadLibassRenderType(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(libassRenderTypeKey), null)
|
||||
|
||||
actual fun saveLibassRenderType(renderType: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(ProfileScopedKey.of(libassRenderTypeKey), renderType)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ data class PlayerSettingsUiState(
|
|||
val nextEpisodeThresholdMode: NextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE,
|
||||
val nextEpisodeThresholdPercent: Float = 99f,
|
||||
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
||||
val useLibass: Boolean = false,
|
||||
val libassRenderType: String = "CUES",
|
||||
)
|
||||
|
||||
object PlayerSettingsRepository {
|
||||
|
|
@ -66,6 +68,8 @@ object PlayerSettingsRepository {
|
|||
private var nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||
private var nextEpisodeThresholdPercent = 99f
|
||||
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
private var useLibass = false
|
||||
private var libassRenderType = "CUES"
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
|
|
@ -103,6 +107,8 @@ object PlayerSettingsRepository {
|
|||
nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||
nextEpisodeThresholdPercent = 99f
|
||||
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
useLibass = false
|
||||
libassRenderType = "CUES"
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +171,8 @@ object PlayerSettingsRepository {
|
|||
?: NextEpisodeThresholdMode.PERCENTAGE
|
||||
nextEpisodeThresholdPercent = PlayerSettingsStorage.loadNextEpisodeThresholdPercent() ?: 99f
|
||||
nextEpisodeThresholdMinutesBeforeEnd = PlayerSettingsStorage.loadNextEpisodeThresholdMinutesBeforeEnd() ?: 2f
|
||||
useLibass = PlayerSettingsStorage.loadUseLibass() ?: false
|
||||
libassRenderType = PlayerSettingsStorage.loadLibassRenderType() ?: "CUES"
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +385,22 @@ object PlayerSettingsRepository {
|
|||
PlayerSettingsStorage.saveNextEpisodeThresholdMinutesBeforeEnd(minutes)
|
||||
}
|
||||
|
||||
fun setUseLibass(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
if (useLibass == enabled) return
|
||||
useLibass = enabled
|
||||
publish()
|
||||
PlayerSettingsStorage.saveUseLibass(enabled)
|
||||
}
|
||||
|
||||
fun setLibassRenderType(renderType: String) {
|
||||
ensureLoaded()
|
||||
if (libassRenderType == renderType) return
|
||||
libassRenderType = renderType
|
||||
publish()
|
||||
PlayerSettingsStorage.saveLibassRenderType(renderType)
|
||||
}
|
||||
|
||||
private fun publish() {
|
||||
_uiState.value = PlayerSettingsUiState(
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
|
|
@ -404,6 +428,8 @@ object PlayerSettingsRepository {
|
|||
nextEpisodeThresholdMode = nextEpisodeThresholdMode,
|
||||
nextEpisodeThresholdPercent = nextEpisodeThresholdPercent,
|
||||
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,8 @@ internal expect object PlayerSettingsStorage {
|
|||
fun saveNextEpisodeThresholdPercent(percent: Float)
|
||||
fun loadNextEpisodeThresholdMinutesBeforeEnd(): Float?
|
||||
fun saveNextEpisodeThresholdMinutesBeforeEnd(minutes: Float)
|
||||
fun loadUseLibass(): Boolean?
|
||||
fun saveUseLibass(enabled: Boolean)
|
||||
fun loadLibassRenderType(): String?
|
||||
fun saveLibassRenderType(renderType: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ internal fun LazyListScope.playbackSettingsContent(
|
|||
decoderPriority: Int,
|
||||
mapDV7ToHevc: Boolean,
|
||||
tunnelingEnabled: Boolean,
|
||||
useLibass: Boolean,
|
||||
libassRenderType: String,
|
||||
) {
|
||||
item {
|
||||
PlaybackSettingsSection(
|
||||
|
|
@ -84,6 +86,8 @@ internal fun LazyListScope.playbackSettingsContent(
|
|||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +105,8 @@ private fun PlaybackSettingsSection(
|
|||
decoderPriority: Int,
|
||||
mapDV7ToHevc: Boolean,
|
||||
tunnelingEnabled: Boolean,
|
||||
useLibass: Boolean,
|
||||
libassRenderType: String,
|
||||
) {
|
||||
var showPreferredAudioDialog by remember { mutableStateOf(false) }
|
||||
var showSecondaryAudioDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -108,6 +114,7 @@ private fun PlaybackSettingsSection(
|
|||
var showSecondarySubtitleDialog by remember { mutableStateOf(false) }
|
||||
var showReuseCacheDurationDialog by remember { mutableStateOf(false) }
|
||||
var showDecoderPriorityDialog by remember { mutableStateOf(false) }
|
||||
var showLibassRenderTypeDialog by remember { mutableStateOf(false) }
|
||||
var showAutoPlayModeDialog by remember { mutableStateOf(false) }
|
||||
var showAutoPlaySourceDialog by remember { mutableStateOf(false) }
|
||||
var showAutoPlayAddonSelectionDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -376,6 +383,39 @@ private fun PlaybackSettingsSection(
|
|||
}
|
||||
}
|
||||
|
||||
if (!isIos) {
|
||||
SettingsSection(
|
||||
title = "SUBTITLE RENDERING",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
SettingsGroup(isTablet = isTablet) {
|
||||
SettingsSwitchRow(
|
||||
title = "Enable libass",
|
||||
description = "Use libass for ASS/SSA subtitle rendering instead of the default renderer.",
|
||||
checked = useLibass,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setUseLibass,
|
||||
)
|
||||
if (useLibass) {
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = "Render Type",
|
||||
description = when (libassRenderType) {
|
||||
"OVERLAY_OPEN_GL" -> "Overlay OpenGL"
|
||||
"OVERLAY_CANVAS" -> "Overlay Canvas"
|
||||
"EFFECTS_OPEN_GL" -> "Effects OpenGL"
|
||||
"EFFECTS_CANVAS" -> "Effects Canvas"
|
||||
"CUES" -> "Standard (Cues)"
|
||||
else -> "Standard (Cues)"
|
||||
},
|
||||
isTablet = isTablet,
|
||||
onClick = { showLibassRenderTypeDialog = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title = "SKIP SEGMENTS",
|
||||
isTablet = isTablet,
|
||||
|
|
@ -673,6 +713,17 @@ private fun PlaybackSettingsSection(
|
|||
)
|
||||
}
|
||||
|
||||
if (showLibassRenderTypeDialog) {
|
||||
LibassRenderTypeDialog(
|
||||
selectedRenderType = libassRenderType,
|
||||
onRenderTypeSelected = { renderType ->
|
||||
PlayerSettingsRepository.setLibassRenderType(renderType)
|
||||
showLibassRenderTypeDialog = false
|
||||
},
|
||||
onDismiss = { showLibassRenderTypeDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAutoPlayModeDialog) {
|
||||
StreamAutoPlayModeDialog(
|
||||
selectedMode = autoPlayPlayerSettings.streamAutoPlayMode,
|
||||
|
|
@ -1029,6 +1080,99 @@ private fun DecoderPriorityDialog(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun LibassRenderTypeDialog(
|
||||
selectedRenderType: String,
|
||||
onRenderTypeSelected: (String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val options = listOf(
|
||||
"OVERLAY_OPEN_GL" to "Overlay OpenGL",
|
||||
"OVERLAY_CANVAS" to "Overlay Canvas",
|
||||
"EFFECTS_OPEN_GL" to "Effects OpenGL",
|
||||
"EFFECTS_CANVAS" to "Effects Canvas",
|
||||
"CUES" to "Standard (Cues)",
|
||||
)
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Render Type",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
options.forEach { (value, label) ->
|
||||
val isSelected = value == selectedRenderType
|
||||
val containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onRenderTypeSelected(value) },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = containerColor,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = "Tap outside to close",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun StreamAutoPlayModeDialog(
|
||||
|
|
|
|||
|
|
@ -161,6 +161,8 @@ fun SettingsScreen(
|
|||
decoderPriority = playerSettingsUiState.decoderPriority,
|
||||
mapDV7ToHevc = playerSettingsUiState.mapDV7ToHevc,
|
||||
tunnelingEnabled = playerSettingsUiState.tunnelingEnabled,
|
||||
useLibass = playerSettingsUiState.useLibass,
|
||||
libassRenderType = playerSettingsUiState.libassRenderType,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = ThemeSettingsRepository::setTheme,
|
||||
amoledEnabled = amoledEnabled,
|
||||
|
|
@ -191,6 +193,8 @@ fun SettingsScreen(
|
|||
decoderPriority = playerSettingsUiState.decoderPriority,
|
||||
mapDV7ToHevc = playerSettingsUiState.mapDV7ToHevc,
|
||||
tunnelingEnabled = playerSettingsUiState.tunnelingEnabled,
|
||||
useLibass = playerSettingsUiState.useLibass,
|
||||
libassRenderType = playerSettingsUiState.libassRenderType,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = ThemeSettingsRepository::setTheme,
|
||||
amoledEnabled = amoledEnabled,
|
||||
|
|
@ -231,6 +235,8 @@ private fun MobileSettingsScreen(
|
|||
decoderPriority: Int,
|
||||
mapDV7ToHevc: Boolean,
|
||||
tunnelingEnabled: Boolean,
|
||||
useLibass: Boolean,
|
||||
libassRenderType: String,
|
||||
selectedTheme: AppTheme,
|
||||
onThemeSelected: (AppTheme) -> Unit,
|
||||
amoledEnabled: Boolean,
|
||||
|
|
@ -289,6 +295,8 @@ private fun MobileSettingsScreen(
|
|||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = false,
|
||||
|
|
@ -365,6 +373,8 @@ private fun TabletSettingsScreen(
|
|||
decoderPriority: Int,
|
||||
mapDV7ToHevc: Boolean,
|
||||
tunnelingEnabled: Boolean,
|
||||
useLibass: Boolean,
|
||||
libassRenderType: String,
|
||||
selectedTheme: AppTheme,
|
||||
onThemeSelected: (AppTheme) -> Unit,
|
||||
amoledEnabled: Boolean,
|
||||
|
|
@ -486,6 +496,8 @@ private fun TabletSettingsScreen(
|
|||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = true,
|
||||
|
|
|
|||
|
|
@ -392,4 +392,12 @@ actual object PlayerSettingsStorage {
|
|||
actual fun saveNextEpisodeThresholdMinutesBeforeEnd(minutes: Float) {
|
||||
NSUserDefaults.standardUserDefaults.setFloat(minutes, forKey = ProfileScopedKey.of(nextEpisodeThresholdMinutesBeforeEndKey))
|
||||
}
|
||||
|
||||
actual fun loadUseLibass(): Boolean? = null
|
||||
|
||||
actual fun saveUseLibass(enabled: Boolean) {}
|
||||
|
||||
actual fun loadLibassRenderType(): String? = null
|
||||
|
||||
actual fun saveLibassRenderType(renderType: String) {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue