mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-15 02:33:15 -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 {
|
||||
for (i in 0 until assignments.length()) {
|
||||
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 remoteUrl = item.optString("remote_url", null)
|
||||
|
||||
|
|
@ -416,6 +421,17 @@ class MainActivity : AppCompatActivity() {
|
|||
private fun playItem(item: PlaylistItem) {
|
||||
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
|
||||
if (item.mimeType == "video/youtube" && !item.remoteUrl.isNullOrEmpty()) {
|
||||
Log.i("MainActivity", "Playing YouTube: ${item.remoteUrl}")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class MediaPlayerManager(
|
|||
private var exoPlayer: ExoPlayer? = null
|
||||
private var currentType: MediaType = MediaType.NONE
|
||||
|
||||
enum class MediaType { NONE, VIDEO, IMAGE, YOUTUBE }
|
||||
enum class MediaType { NONE, VIDEO, IMAGE, YOUTUBE, WIDGET }
|
||||
|
||||
init {
|
||||
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) {
|
||||
Log.i("MediaPlayerManager", "Streaming video from URL: $url (muted=$muted)")
|
||||
currentType = MediaType.VIDEO
|
||||
|
|
|
|||
|
|
@ -17,9 +17,13 @@ data class PlaylistItem(
|
|||
val sortOrder: Int,
|
||||
val enabled: Boolean = true,
|
||||
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()
|
||||
// Widget assignments have a widget_id and no downloadable content file.
|
||||
val isWidget: Boolean get() = !widgetId.isNullOrEmpty()
|
||||
}
|
||||
|
||||
class PlaylistController(
|
||||
|
|
@ -51,7 +55,8 @@ class PlaylistController(
|
|||
newItems.add(
|
||||
PlaylistItem(
|
||||
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"),
|
||||
mimeType = obj.optString("mime_type", "video/mp4"),
|
||||
filepath = obj.optString("filepath", ""),
|
||||
|
|
@ -60,14 +65,17 @@ class PlaylistController(
|
|||
sortOrder = obj.optInt("sort_order", 0),
|
||||
enabled = obj.optInt("enabled", 1) == 1,
|
||||
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
|
||||
val oldContentIds = items.map { it.contentId }
|
||||
val newContentIds = newItems.map { it.contentId }
|
||||
// Check if playlist actually changed (key on content OR widget id, since
|
||||
// widget items share an empty contentId).
|
||||
val oldContentIds = items.map { it.contentId + "|" + (it.widgetId ?: "") }
|
||||
val newContentIds = newItems.map { it.contentId + "|" + (it.widgetId ?: "") }
|
||||
val playlistChanged = oldContentIds != newContentIds
|
||||
|
||||
if (!playlistChanged && items.isNotEmpty()) {
|
||||
|
|
@ -169,8 +177,9 @@ class PlaylistController(
|
|||
Log.i("PlaylistController", "Playing: ${item.filename} (index $currentIndex)")
|
||||
onItemChanged(item)
|
||||
|
||||
// For images, auto-advance after duration. For videos, wait for completion callback.
|
||||
if (item.mimeType.startsWith("image/")) {
|
||||
// For images and widgets, auto-advance after duration. For videos, wait
|
||||
// for the completion callback.
|
||||
if (item.mimeType.startsWith("image/") || item.isWidget) {
|
||||
scheduleAdvance(item.durationSec * 1000L)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue