chore: Implement translation callbacks to flutter

This commit is contained in:
PartyDonut 2025-10-17 18:43:00 +02:00
parent e902e2034a
commit 29917bb59f
16 changed files with 681 additions and 43 deletions

View file

@ -3,6 +3,7 @@ package nl.jknaapen.fladder
import NativeVideoActivity
import PlayerSettingsPigeon
import StartResult
import TranslationsPigeon
import VideoPlayerApi
import VideoPlayerControlsCallback
import VideoPlayerListenerCallback
@ -12,6 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import com.ryanheise.audioservice.AudioServiceFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.TranslationsMessenger
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.leanBackEnabled
@ -37,6 +39,9 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity {
videoPlayerHost.videoPlayerControls =
VideoPlayerControlsCallback(flutterEngine.dartExecutor.binaryMessenger)
TranslationsMessenger.translation =
TranslationsPigeon(flutterEngine.dartExecutor.binaryMessenger)
PlayerSettingsPigeon.setUp(
flutterEngine.dartExecutor.binaryMessenger,
api = PlayerSettingsObject
@ -54,9 +59,9 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity {
StartResult(resultValue = "Cancelled")
}
callback?.invoke(Result.success(startResult))
VideoPlayerObject.implementation.player?.stop()
VideoPlayerObject.implementation.player?.release()
callback?.invoke(Result.success(startResult))
}
}

View file

@ -0,0 +1,217 @@
// Autogenerated from Pigeon (v26.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object TranslationsPigeonPigeonUtils {
fun createConnectionError(channelName: String): FlutterError {
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
}
private open class TranslationsPigeonPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
super.writeValue(stream, value)
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class TranslationsPigeon(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by TranslationsPigeon. */
val codec: MessageCodec<Any?> by lazy {
TranslationsPigeonPigeonCodec()
}
}
fun next(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.next$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun nextVideo(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextVideo$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun close(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.close$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun skip(nameArg: String, callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.skip$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(nameArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun subtitles(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.subtitles$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun off(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.off$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun chapters(countArg: Long, callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.chapters$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(countArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun nextUpInSeconds(secondsArg: Long, callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextUpInSeconds$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(secondsArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
fun endsAt(timeArg: String, callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.endsAt$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timeArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
} else {
val output = it[0] as String
callback(Result.success(output))
}
} else {
callback(Result.failure(TranslationsPigeonPigeonUtils.createConnectionError(channelName)))
}
}
}
}

View file

@ -66,6 +66,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.media3.exoplayer.ExoPlayer
import kotlinx.coroutines.delay
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.capitalize
import nl.jknaapen.fladder.utility.formatTime
@ -122,20 +124,23 @@ internal fun ProgressBar(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val progressBarTopLabel = listOf(
playableData?.currentItem?.subTitle,
endTimeString,
)
val label = progressBarTopLabel.joinToString(separator = " - ")
if (label.isNotBlank()) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.copy(
color = Color.White,
fontWeight = FontWeight.Bold
),
Translate({ Localized.endsAt(endTimeString ?: "", it) }, endTimeString) { translation ->
val progressBarTopLabel = listOf(
playableData?.currentItem?.subTitle,
translation,
)
val label = progressBarTopLabel.filterNot { it.isNullOrBlank() }
.joinToString(separator = " - ")
if (label.isNotBlank()) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.copy(
color = Color.White,
fontWeight = FontWeight.Bold
),
)
}
}
Spacer(modifier = Modifier.weight(1f))

View file

@ -34,7 +34,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.defaultSelected
import nl.jknaapen.fladder.utility.leanBackEnabled
@ -135,11 +137,18 @@ internal fun BoxScope.SegmentSkipOverlay(
}
}
}
activeSegment?.let {
Text(
"Skip ${it.name.lowercase()}",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
)
activeSegment?.let { segment ->
Translate({ cb ->
Localized.skip(
segment.name.lowercase(),
cb
)
}) { translation ->
Text(
translation,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
)
}
}
}
}

View file

@ -17,6 +17,8 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.clearAudioTrack
import nl.jknaapen.fladder.utility.setInternalAudioTrack
@ -82,7 +84,9 @@ fun AudioPicker(
},
selected = selectedOff
) {
Text("Off")
Translate(Localized::off) {
Text(it)
}
}
}

View file

@ -34,6 +34,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.highlightOnFocus
@ -78,11 +80,14 @@ internal fun ChapterSelectionSheet(
.wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Chapters",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Translate({ Localized.chapters(chapters.size.toLong(), it) }) {
Text(
it,
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
}
LazyRow(
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(8.dp)

View file

@ -18,6 +18,8 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.clearSubtitleTrack
import nl.jknaapen.fladder.utility.setInternalSubtitleTrack
@ -79,9 +81,9 @@ fun SubtitlePicker(
},
selected = selectedOff
) {
Text(
text = "Off",
)
Translate(Localized::off) {
Text(it)
}
}
}
internalSubTracks.forEachIndexed { index, subtitle ->

View file

@ -45,7 +45,9 @@ import io.github.rabehx.iconsax.filled.Next
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import nl.jknaapen.fladder.composables.controls.CustomButton
import nl.jknaapen.fladder.objects.Localized
import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.Translate
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.conditional
import nl.jknaapen.fladder.utility.highlightOnFocus
@ -169,13 +171,24 @@ internal fun NextUpOverlay(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
"Next-up in $timeUntilNextVideo seconds",
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface
)
Translate(
{
Localized.nextUpInSeconds(
timeUntilNextVideo.toLong(),
callback = it
)
},
key = timeUntilNextVideo,
) {
Text(
it,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface
)
}
Box(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
@ -199,7 +212,9 @@ internal fun NextUpOverlay(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Next")
Translate(Localized::next) { value ->
Text(value)
}
Icon(Iconsax.Filled.Next, contentDescription = "Play next video")
}
}
@ -213,7 +228,9 @@ internal fun NextUpOverlay(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Close")
Translate(Localized::close) {
Text(it)
}
Icon(Iconsax.Filled.CloseCircle, contentDescription = "Close Icon")
}
}

View file

@ -0,0 +1,33 @@
package nl.jknaapen.fladder.objects
import TranslationsPigeon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
val Localized
get() = TranslationsMessenger.translation
internal object TranslationsMessenger {
lateinit var translation: TranslationsPigeon
}
@Composable
internal fun Translate(
callback: ((Result<String>) -> Unit) -> Unit,
key: Any? = Unit,
content: @Composable (String) -> Unit
) {
var value by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key) {
callback { result ->
value = result.getOrNull()
}
}
content(value.orEmpty())
}

View file

@ -14,7 +14,6 @@ import nl.jknaapen.fladder.VideoPlayerActivity
import nl.jknaapen.fladder.messengers.VideoPlayerImplementation
import nl.jknaapen.fladder.utility.InternalTrack
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.toJavaInstant
@ -43,9 +42,8 @@ object VideoPlayerObject {
val remainingMs = (dur - pos).coerceAtLeast(0L)
val endInstant = startInstant.toJavaInstant().plusMillis(remainingMs)
val endZoned = endInstant.atZone(zone)
val formatter = DateTimeFormatter.ofPattern("hh:mm a")
"ends at ${endZoned.format(formatter)}"
endZoned.toOffsetDateTime().toString()
}
val currentSubtitleTrackIndex =