mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-21 13:42:38 -06:00
fix(android): render widgets in fullscreen / single-zone layouts
Widgets worked in multi-zone layouts (ZoneManager renders them in a WebView) but
were broken in "default fullscreen" (no layout) and the fullscreen template (a
single-zone layout) - both take the single-zone PlaylistController path, which:
1) called getString("content_id"), throwing on a widget assignment (no
content_id) - in both the playlist builder AND the pre-download loop, which
could break the whole fullscreen playlist; and
2) had no widget render case in playItem (so a widget never displayed).
Fix:
- PlaylistItem gains widgetId/widgetType + isWidget; the builder reads them and
tolerates a missing content_id.
- playItem renders a widget fullscreen via MediaPlayerManager.showWidget() (loads
/api/widgets/:id/render in the full-screen WebView, mirroring ZoneManager).
- Widgets auto-advance on their duration like images.
- Pre-download loop skips widget assignments (no file to fetch).
Compile-checked; signed APK builds. Needs on-device check: a widget plays in
default-fullscreen and the fullscreen template, and mixed widget+media playlists
advance correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50ad1f670b
commit
911cd07951
|
|
@ -260,7 +260,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
thread {
|
thread {
|
||||||
for (i in 0 until assignments.length()) {
|
for (i in 0 until assignments.length()) {
|
||||||
val item = assignments.getJSONObject(i)
|
val item = assignments.getJSONObject(i)
|
||||||
val contentId = item.getString("content_id")
|
// Widget assignments have no downloadable content file - skip
|
||||||
|
// (also avoids getString throwing on a null content_id).
|
||||||
|
val widgetId = if (item.isNull("widget_id")) "" else item.optString("widget_id", "")
|
||||||
|
if (widgetId.isNotEmpty()) continue
|
||||||
|
val contentId = if (item.isNull("content_id")) "" else item.optString("content_id", "")
|
||||||
|
if (contentId.isEmpty()) continue
|
||||||
val filename = item.optString("filename", "content")
|
val filename = item.optString("filename", "content")
|
||||||
val remoteUrl = item.optString("remote_url", null)
|
val remoteUrl = item.optString("remote_url", null)
|
||||||
|
|
||||||
|
|
@ -416,6 +421,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun playItem(item: PlaylistItem) {
|
private fun playItem(item: PlaylistItem) {
|
||||||
hideStatus()
|
hideStatus()
|
||||||
|
|
||||||
|
// Widget content - render fullscreen in a WebView (single-zone / fullscreen
|
||||||
|
// layouts; multi-zone widgets go through ZoneManager). Previously unhandled,
|
||||||
|
// so widgets were blank/broken in default-fullscreen and the fullscreen template.
|
||||||
|
if (item.isWidget) {
|
||||||
|
val url = "${config.serverUrl}/api/widgets/${item.widgetId}/render"
|
||||||
|
Log.i("MainActivity", "Playing widget fullscreen: $url")
|
||||||
|
mediaPlayer.showWidget(url)
|
||||||
|
wsService?.sendPlaybackState(item.contentId.ifEmpty { item.widgetId ?: "" }, 0f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// YouTube content - play in WebView
|
// YouTube content - play in WebView
|
||||||
if (item.mimeType == "video/youtube" && !item.remoteUrl.isNullOrEmpty()) {
|
if (item.mimeType == "video/youtube" && !item.remoteUrl.isNullOrEmpty()) {
|
||||||
Log.i("MainActivity", "Playing YouTube: ${item.remoteUrl}")
|
Log.i("MainActivity", "Playing YouTube: ${item.remoteUrl}")
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class MediaPlayerManager(
|
||||||
private var exoPlayer: ExoPlayer? = null
|
private var exoPlayer: ExoPlayer? = null
|
||||||
private var currentType: MediaType = MediaType.NONE
|
private var currentType: MediaType = MediaType.NONE
|
||||||
|
|
||||||
enum class MediaType { NONE, VIDEO, IMAGE, YOUTUBE }
|
enum class MediaType { NONE, VIDEO, IMAGE, YOUTUBE, WIDGET }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupExoPlayer()
|
setupExoPlayer()
|
||||||
|
|
@ -65,6 +65,29 @@ class MediaPlayerManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fullscreen widget render (single-zone / "fullscreen" layouts). Reuses the
|
||||||
|
// full-screen WebView; ZoneManager handles widgets in multi-zone layouts.
|
||||||
|
fun showWidget(url: String) {
|
||||||
|
Log.i("MediaPlayerManager", "Showing widget: $url")
|
||||||
|
currentType = MediaType.WIDGET
|
||||||
|
|
||||||
|
playerView.visibility = android.view.View.GONE
|
||||||
|
imageView.visibility = android.view.View.GONE
|
||||||
|
youtubeWebView?.visibility = android.view.View.VISIBLE
|
||||||
|
|
||||||
|
exoPlayer?.stop()
|
||||||
|
|
||||||
|
youtubeWebView?.apply {
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.domStorageEnabled = true
|
||||||
|
settings.mediaPlaybackRequiresUserGesture = false
|
||||||
|
webViewClient = WebViewClient()
|
||||||
|
webChromeClient = WebChromeClient()
|
||||||
|
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||||
|
loadUrl(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun playVideoFromUrl(url: String, muted: Boolean = false) {
|
fun playVideoFromUrl(url: String, muted: Boolean = false) {
|
||||||
Log.i("MediaPlayerManager", "Streaming video from URL: $url (muted=$muted)")
|
Log.i("MediaPlayerManager", "Streaming video from URL: $url (muted=$muted)")
|
||||||
currentType = MediaType.VIDEO
|
currentType = MediaType.VIDEO
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,13 @@ data class PlaylistItem(
|
||||||
val sortOrder: Int,
|
val sortOrder: Int,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
val remoteUrl: String? = null,
|
val remoteUrl: String? = null,
|
||||||
val muted: Boolean = false
|
val muted: Boolean = false,
|
||||||
|
val widgetId: String? = null,
|
||||||
|
val widgetType: String? = null
|
||||||
) {
|
) {
|
||||||
val isRemote: Boolean get() = !remoteUrl.isNullOrEmpty()
|
val isRemote: Boolean get() = !remoteUrl.isNullOrEmpty()
|
||||||
|
// Widget assignments have a widget_id and no downloadable content file.
|
||||||
|
val isWidget: Boolean get() = !widgetId.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaylistController(
|
class PlaylistController(
|
||||||
|
|
@ -51,7 +55,8 @@ class PlaylistController(
|
||||||
newItems.add(
|
newItems.add(
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
assignmentId = obj.optInt("id", 0),
|
assignmentId = obj.optInt("id", 0),
|
||||||
contentId = obj.getString("content_id"),
|
// Tolerant: widget assignments have no content_id (getString threw).
|
||||||
|
contentId = if (obj.isNull("content_id")) "" else obj.optString("content_id", ""),
|
||||||
filename = obj.optString("filename", "unknown"),
|
filename = obj.optString("filename", "unknown"),
|
||||||
mimeType = obj.optString("mime_type", "video/mp4"),
|
mimeType = obj.optString("mime_type", "video/mp4"),
|
||||||
filepath = obj.optString("filepath", ""),
|
filepath = obj.optString("filepath", ""),
|
||||||
|
|
@ -60,14 +65,17 @@ class PlaylistController(
|
||||||
sortOrder = obj.optInt("sort_order", 0),
|
sortOrder = obj.optInt("sort_order", 0),
|
||||||
enabled = obj.optInt("enabled", 1) == 1,
|
enabled = obj.optInt("enabled", 1) == 1,
|
||||||
remoteUrl = if (obj.isNull("remote_url")) null else obj.optString("remote_url", "").ifEmpty { null },
|
remoteUrl = if (obj.isNull("remote_url")) null else obj.optString("remote_url", "").ifEmpty { null },
|
||||||
muted = obj.optInt("muted", 0) == 1
|
muted = obj.optInt("muted", 0) == 1,
|
||||||
|
widgetId = if (obj.isNull("widget_id")) null else obj.optString("widget_id", "").ifEmpty { null },
|
||||||
|
widgetType = if (obj.isNull("widget_type")) null else obj.optString("widget_type", "").ifEmpty { null }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if playlist actually changed
|
// Check if playlist actually changed (key on content OR widget id, since
|
||||||
val oldContentIds = items.map { it.contentId }
|
// widget items share an empty contentId).
|
||||||
val newContentIds = newItems.map { it.contentId }
|
val oldContentIds = items.map { it.contentId + "|" + (it.widgetId ?: "") }
|
||||||
|
val newContentIds = newItems.map { it.contentId + "|" + (it.widgetId ?: "") }
|
||||||
val playlistChanged = oldContentIds != newContentIds
|
val playlistChanged = oldContentIds != newContentIds
|
||||||
|
|
||||||
if (!playlistChanged && items.isNotEmpty()) {
|
if (!playlistChanged && items.isNotEmpty()) {
|
||||||
|
|
@ -169,8 +177,9 @@ class PlaylistController(
|
||||||
Log.i("PlaylistController", "Playing: ${item.filename} (index $currentIndex)")
|
Log.i("PlaylistController", "Playing: ${item.filename} (index $currentIndex)")
|
||||||
onItemChanged(item)
|
onItemChanged(item)
|
||||||
|
|
||||||
// For images, auto-advance after duration. For videos, wait for completion callback.
|
// For images and widgets, auto-advance after duration. For videos, wait
|
||||||
if (item.mimeType.startsWith("image/")) {
|
// for the completion callback.
|
||||||
|
if (item.mimeType.startsWith("image/") || item.isWidget) {
|
||||||
scheduleAdvance(item.durationSec * 1000L)
|
scheduleAdvance(item.durationSec * 1000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue