Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a70818d
feat: Implement Jellyfin SyncPlay support
irican-f Jan 10, 2026
63d337f
feat: enhance SyncPlay integration and synchronized playback
irican-f Jan 10, 2026
1519be7
feat: Implement app lifecycle handling for SyncPlay
irican-f Jan 11, 2026
44af2de
fix: Set SyncPlay bottom sheet background to transparent
irican-f Jan 11, 2026
6402639
feat: Add visual indicators for SyncPlay command processing
irican-f Jan 13, 2026
f889b1d
refactor: Modularize SyncPlay logic and reorganize models
irican-f Jan 13, 2026
57c4dac
feat: Implement localization for SyncPlay
irican-f Jan 13, 2026
51741cc
fix: Ensure SyncPlay FAB is visible in side navigation bar
irican-f Jan 13, 2026
b64a1f1
feat: integrate SyncPlay with native Android player
fifty-filip Jan 17, 2026
0f30b28
feat: integrate SyncPlay-aware user actions and improve WebSocket log…
fifty-filip Jan 17, 2026
1a73ac0
chore: improve SyncPlay WebSocket logging
fifty-filip Jan 17, 2026
9c36c89
Merge branch 'develop' into syncplay
irican-f Jan 28, 2026
1cfdd41
feat: improve SyncPlay synchronization and playback reliability
irican-f Jan 28, 2026
d36990d
feat: improve SyncPlay synchronization and buffering logic
irican-f Feb 2, 2026
1969ec1
Merge branch 'develop' into syncplay
irican-f Feb 2, 2026
fa5960a
feat: implement SyncPlay command overlay for native Android player
irican-f Feb 2, 2026
cffcccc
feat: localize SyncPlay command overlay labels
irican-f Feb 2, 2026
92a62bc
Merge branch 'develop' into syncplay
Feb 11, 2026
8db485f
feat: improve SyncPlay synchronization and native player state inference
irican-f Feb 22, 2026
8689383
Merge branch 'develop' into syncplay
irican-f Feb 22, 2026
c697e8c
fix(*): merge conflicts
irican-f Feb 22, 2026
c66fdcc
Merge branch 'develop' into syncplay
irican-f Mar 21, 2026
2ab1978
feat: implement playback drift correction and enhance SyncPlay synchr…
irican-f Mar 21, 2026
887be69
feat: improve SyncPlay seek handling and fix local track-switch autoplay
irican-f Mar 21, 2026
aa61c04
lint: fix code formatting and cleanup in SyncPlay components
irican-f Mar 21, 2026
caea38f
feat: refactor SyncPlay command handling to use type-safe enums acros…
irican-f Mar 21, 2026
5fc0d82
feat: improve SyncPlay session management and video player route hand…
irican-f Mar 21, 2026
152dfef
feat: refactor video player route state management
irican-f Mar 21, 2026
465cd16
fix: revert changes to KotlinOptions for pigeons package imports
irican-f Mar 21, 2026
0df2192
fix: android build
irican-f Mar 21, 2026
a24855e
Merge branch 'develop' into syncplay
irican-f May 1, 2026
03274b4
feat: enhance SyncPlay stability, UI feedback, and track switching
irican-f May 1, 2026
bd249cd
fix(syncplay): reset transient lifecycle state on leave/disconnect
irican-f May 8, 2026
3a99f0b
fix(syncplay): abort _startPlayback when user leaves mid-flight
irican-f May 8, 2026
d15986c
fix(syncplay): reset buffering state on every loadPlaybackItem and _s…
irican-f May 8, 2026
bab529d
fix(syncplay): auto-load media when joining an active group
irican-f May 8, 2026
9963c48
fix(syncplay): pause locally when another client buffers
irican-f May 8, 2026
6e117d8
feat(syncplay): expose requestNextItem / requestPreviousItem
irican-f May 8, 2026
849f39d
refactor(syncplay): use NextItem/PreviousItem instead of SetNewQueue …
irican-f May 8, 2026
fb9942b
fix(syncplay): trigger startPlayback on PlayQueue NextItem/PreviousIt…
irican-f May 8, 2026
68bf896
feat(syncplay): show 'Switching item…' overlay during next-episode re…
irican-f May 8, 2026
197c6e7
fix(syncplay): use estimated live position when reloading for track s…
irican-f May 8, 2026
799d02f
fix(syncplay): stop stacking two FABs in the side navigation rail
irican-f May 8, 2026
8047919
docs(syncplay): document regression scenarios
irican-f May 8, 2026
4ef6786
chore: remove unnecessary braces in string interpolation
irican-f May 8, 2026
e5d8215
fix(syncplay): don't await openPlayer in _startPlayback
irican-f May 8, 2026
7d7aed9
fix(syncplay): fully stop playback when closing player while in a group
irican-f May 8, 2026
21105d6
fix(syncplay): pop player route immediately on close, don't await stop
irican-f May 8, 2026
e6c8dae
feat(syncplay): optimistic local load on next/previous episode
irican-f May 8, 2026
0226b08
feat(syncplay): optimistic local load for setNewQueue paths too
irican-f May 8, 2026
5a95e44
revert(syncplay): use setNewQueue for next/previous episode advance
irican-f May 8, 2026
0cd7458
fix(syncplay): don't override post-Seek Ready with isPlaying:false
irican-f May 8, 2026
05bb52a
fix(syncplay): send authoritative Ready(isPlaying:true) after initial…
irican-f May 8, 2026
28bace5
fix(syncplay): track switch no longer pauses other users
irican-f May 8, 2026
34bdae8
fix(syncplay): rejoin no longer rewinds the rest of the group
irican-f May 8, 2026
0246010
chore: format syncplay_correction_test.dart at 120-col line length
irican-f May 8, 2026
c30d765
fix(syncplay): Android-TV reliability + post-disconnect resilience
irican-f May 10, 2026
54b5f77
fix(style): lint
irican-f May 10, 2026
f8c697f
fix(syncplay): stop duplicate participants, false join failure & WS i…
irican-f May 17, 2026
a478a16
fix(syncplay): small fixes
irican-f May 17, 2026
808ece5
refactor(websocket): decouple shared Jellyfin WebSocket from SyncPlay
irican-f May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,102 +12,82 @@ import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer

private object BatteryOptimizationPigeonPigeonUtils {

fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}

fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
}

private open class BatteryOptimizationPigeonPigeonCodec : 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)
}
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 interface from Pigeon that represents a handler of messages from Flutter. */
interface BatteryOptimizationPigeon {
/** Returns whether the app is currently *ignored* from battery optimizations. */
fun isIgnoringBatteryOptimizations(): Boolean
fun isIgnoringBatteryOptimizations(): Boolean
fun openBatteryOptimizationSettings()

/** Opens the battery-optimization/settings screen for this app (Android). */
fun openBatteryOptimizationSettings()

companion object {
/** The codec used by BatteryOptimizationPigeon. */
val codec: MessageCodec<Any?> by lazy {
BatteryOptimizationPigeonPigeonCodec()
}

/** Sets up an instance of `BatteryOptimizationPigeon` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(
binaryMessenger: BinaryMessenger,
api: BatteryOptimizationPigeon?,
messageChannelSuffix: String = ""
) {
val separatedMessageChannelSuffix =
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.nl_jknaapen_fladder.settings.BatteryOptimizationPigeon.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix",
codec
)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isIgnoringBatteryOptimizations())
} catch (exception: Throwable) {
BatteryOptimizationPigeonPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
companion object {
/** The codec used by BatteryOptimizationPigeon. */
val codec: MessageCodec<Any?> by lazy {
BatteryOptimizationPigeonPigeonCodec()
}
/** Sets up an instance of `BatteryOptimizationPigeon` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: BatteryOptimizationPigeon?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.nl_jknaapen_fladder.settings.BatteryOptimizationPigeon.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isIgnoringBatteryOptimizations())
} catch (exception: Throwable) {
BatteryOptimizationPigeonPigeonUtils.wrapError(exception)
}
run {
val channel = BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.nl_jknaapen_fladder.settings.BatteryOptimizationPigeon.openBatteryOptimizationSettings$separatedMessageChannelSuffix",
codec
)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.openBatteryOptimizationSettings()
listOf(null)
} catch (exception: Throwable) {
BatteryOptimizationPigeonPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.nl_jknaapen_fladder.settings.BatteryOptimizationPigeon.openBatteryOptimizationSettings$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.openBatteryOptimizationSettings()
listOf(null)
} catch (exception: Throwable) {
BatteryOptimizationPigeonPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,124 @@ class TranslationsPigeon(private val binaryMessenger: BinaryMessenger, private v
}
}
}
fun syncPlaySyncingWithGroup(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlaySyncingWithGroup$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 syncPlayCommandPausing(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlayCommandPausing$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 syncPlayCommandPlaying(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlayCommandPlaying$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 syncPlayCommandSeeking(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlayCommandSeeking$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 syncPlayCommandStopping(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlayCommandStopping$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 syncPlayCommandSyncing(callback: (Result<String>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.nl_jknaapen_fladder.settings.TranslationsPigeon.syncPlayCommandSyncing$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)))
}
}
}
}
Loading
Loading