From 29917bb59f5215f327f53e1c2daa66dc3f5ebbaa Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Fri, 17 Oct 2025 18:43:00 +0200 Subject: [PATCH] chore: Implement translation callbacks to flutter --- .../nl/jknaapen/fladder/MainActivity.kt | 7 +- .../fladder/api/TranslationsPigeon.g.kt | 217 +++++++++++++++ .../composables/controls/ProgressBar.kt | 31 ++- .../composables/controls/SkipOverlay.kt | 19 +- .../composables/dialogs/AudioSelection.kt | 6 +- .../dialogs/ChapterSelectionSheet.kt | 15 +- .../composables/dialogs/SubtitlePicker.kt | 8 +- .../composables/overlays/NextUpOverlay.kt | 35 ++- .../jknaapen/fladder/objects/Translations.kt | 33 +++ .../fladder/objects/VideoPlayerObject.kt | 4 +- lib/l10n/app_en.arb | 10 +- lib/src/translations_pigeon.g.dart | 262 ++++++++++++++++++ .../item_base_model/play_item_helpers.dart | 3 + lib/util/localization_helper.dart | 40 +++ lib/wrappers/players/native_player.dart | 4 +- pigeons/translations_pigeon.dart | 30 ++ 16 files changed, 681 insertions(+), 43 deletions(-) create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/api/TranslationsPigeon.g.kt create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/objects/Translations.kt create mode 100644 lib/src/translations_pigeon.g.dart create mode 100644 pigeons/translations_pigeon.dart diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt index 593ab7a..49d3d8d 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt @@ -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)) } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/TranslationsPigeon.g.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/TranslationsPigeon.g.kt new file mode 100644 index 0000000..84a8b42 --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/TranslationsPigeon.g.kt @@ -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 by lazy { + TranslationsPigeonPigeonCodec() + } + } + fun next(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.next$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextVideo$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.close$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.skip$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.subtitles$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.off$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.chapters$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextUpInSeconds$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.endsAt$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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))) + } + } + } +} diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt index 6d05c4e..343f21d 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt @@ -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)) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt index bfb386c..8ba32ad 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt @@ -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) + ) + } } } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt index 713c84e..4557399 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt @@ -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) + } } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt index c0c4078..105aa24 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt @@ -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) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt index 6b69fce..50cd852 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt @@ -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 -> diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/overlays/NextUpOverlay.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/overlays/NextUpOverlay.kt index f8e4e7f..7722379 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/overlays/NextUpOverlay.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/overlays/NextUpOverlay.kt @@ -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") } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/Translations.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/Translations.kt new file mode 100644 index 0000000..2129deb --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/Translations.kt @@ -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) -> Unit) -> Unit, + key: Any? = Unit, + content: @Composable (String) -> Unit +) { + var value by remember { mutableStateOf(null) } + + LaunchedEffect(key) { + callback { result -> + value = result.getOrNull() + } + } + + content(value.orEmpty()) +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt index 80a019b..fa75063 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt @@ -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 = diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 86ab68b..97e6285 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1348,5 +1348,13 @@ "mediaTunnelingTitle": "Media tunneling", "mediaTunnelingDesc": "Enable media tunneling for native player", "clientSettingsUseSystemIMETitle": "Use system keyboard", - "clientSettingsUseSystemIMEDesc": "Use the built-in keyboard provided by your system" + "clientSettingsUseSystemIMEDesc": "Use the built-in keyboard provided by your system", + "nextUpInCount": "Next-up in {seconds}", + "@nextUpInCount": { + "placeholders": { + "seconds": { + "type": "int" + } + } + } } \ No newline at end of file diff --git a/lib/src/translations_pigeon.g.dart b/lib/src/translations_pigeon.g.dart new file mode 100644 index 0000000..f75bd4e --- /dev/null +++ b/lib/src/translations_pigeon.g.dart @@ -0,0 +1,262 @@ +// Autogenerated from Pigeon (v26.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TranslationsPigeon { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + String next(); + + String nextVideo(); + + String close(); + + String skip(String name); + + String subtitles(); + + String off(); + + String chapters(int count); + + String nextUpInSeconds(int seconds); + + String endsAt(String time); + + static void setUp(TranslationsPigeon? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.next$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final String output = api.next(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextVideo$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final String output = api.nextVideo(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.close$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final String output = api.close(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.skip$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.skip was null.'); + final List args = (message as List?)!; + final String? arg_name = (args[0] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.skip was null, expected non-null String.'); + try { + final String output = api.skip(arg_name!); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.subtitles$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final String output = api.subtitles(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.off$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final String output = api.off(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.chapters$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.chapters was null.'); + final List args = (message as List?)!; + final int? arg_count = (args[0] as int?); + assert(arg_count != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.chapters was null, expected non-null int.'); + try { + final String output = api.chapters(arg_count!); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextUpInSeconds$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextUpInSeconds was null.'); + final List args = (message as List?)!; + final int? arg_seconds = (args[0] as int?); + assert(arg_seconds != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.nextUpInSeconds was null, expected non-null int.'); + try { + final String output = api.nextUpInSeconds(arg_seconds!); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.endsAt$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.endsAt was null.'); + final List args = (message as List?)!; + final String? arg_time = (args[0] as String?); + assert(arg_time != null, + 'Argument for dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.endsAt was null, expected non-null String.'); + try { + final String output = api.endsAt(arg_time!); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index bc5a44e..e8f15f6 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -100,6 +102,7 @@ Future _playVideo( } if (context.mounted) { + log("Finished refreshing"); await context.refreshData(); } diff --git a/lib/util/localization_helper.dart b/lib/util/localization_helper.dart index e6ce622..e3b876b 100644 --- a/lib/util/localization_helper.dart +++ b/lib/util/localization_helper.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/providers/sync/background_download_provider.dart'; +import 'package:fladder/src/translations_pigeon.g.dart' as messenger; ///Only use for base translations, under normal circumstances ALWAYS use the widgets provided context final localizationContextProvider = StateProvider((ref) => null); @@ -26,6 +27,7 @@ class LocalizationContextWrapper extends ConsumerStatefulWidget { } class _LocalizationContextWrapperState extends ConsumerState { + _TranslationsMessgener? _messenger; @override void initState() { super.initState(); @@ -41,6 +43,11 @@ class _LocalizationContextWrapperState extends ConsumerState context); ref.read(backgroundDownloaderProvider.notifier).updateTranslations(context); @@ -58,3 +65,36 @@ extension LocaleDisplayCodeExtension on Locale { : languageCode.toUpperCase(); } } + +class _TranslationsMessgener extends messenger.TranslationsPigeon { + _TranslationsMessgener({required this.context}); + + final BuildContext context; + + @override + String chapters(int count) => context.localized.chapter(count); + + @override + String close() => context.localized.close; + + @override + String endsAt(String time) => context.localized.endsAt(DateTime.parse(time)); + + @override + String next() => context.localized.nextVideo; + + @override + String nextUpInSeconds(int seconds) => context.localized.nextUpInCount(seconds); + + @override + String nextVideo() => context.localized.nextVideo; + + @override + String off() => context.localized.off; + + @override + String skip(String name) => context.localized.skipButtonLabel(name); + + @override + String subtitles() => context.localized.subtitles; +} diff --git a/lib/wrappers/players/native_player.dart b/lib/wrappers/players/native_player.dart index 669a86e..d995db9 100644 --- a/lib/wrappers/players/native_player.dart +++ b/lib/wrappers/players/native_player.dart @@ -34,9 +34,9 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback { Future loadVideo(String url, bool play) async => player.open(url, play); @override - Future open(BuildContext newContext) async { + Future open(BuildContext newContext) async { nativeActivityStarted = true; - NativeVideoActivity().launchActivity(); + return NativeVideoActivity().launchActivity(); } @override diff --git a/pigeons/translations_pigeon.dart b/pigeons/translations_pigeon.dart new file mode 100644 index 0000000..2e74c79 --- /dev/null +++ b/pigeons/translations_pigeon.dart @@ -0,0 +1,30 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/translations_pigeon.g.dart', + dartOptions: DartOptions(), + kotlinOut: 'android/app/src/main/kotlin/nl/jknaapen/fladder/api/TranslationsPigeon.g.kt', + kotlinOptions: KotlinOptions( + includeErrorClass: false, + ), + dartPackageName: 'nl_jknaapen_fladder.settings', + ), +) +@FlutterApi() +abstract class TranslationsPigeon { + String next(); + String nextVideo(); + String close(); + + String skip(String name); + + String subtitles(); + + String off(); + String chapters(int count); + + String nextUpInSeconds(int seconds); + + String endsAt(String time); +}