feat: youtube embed support
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
9961809730
commit
001e58bc3c
|
|
@ -82,7 +82,15 @@ data class Image(
|
|||
|
||||
@Serializable
|
||||
data class Special(
|
||||
val type: String? = null
|
||||
val type: String? = null,
|
||||
val id: String? = null,
|
||||
val timestamp: String? = null,
|
||||
@SerialName("content_type")
|
||||
val contentType: String? = null,
|
||||
@SerialName("album_id")
|
||||
val albumID: String? = null,
|
||||
@SerialName("track_id")
|
||||
val trackID: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
|
|
@ -28,6 +28,7 @@ import chat.revolt.api.internals.BrushCompat
|
|||
import chat.revolt.api.internals.solidColor
|
||||
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
|
||||
import chat.revolt.api.schemas.Embed
|
||||
import chat.revolt.components.chat.specialembeds.SpecialEmbedSwitch
|
||||
import chat.revolt.components.generic.RemoteImage
|
||||
import chat.revolt.components.markdown.RichMarkdown
|
||||
import chat.revolt.api.schemas.Embed as EmbedSchema
|
||||
|
|
@ -42,7 +43,7 @@ fun RegularEmbed(
|
|||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.Start)
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
// Stripe at the left side of the embed
|
||||
|
|
@ -58,7 +59,6 @@ fun RegularEmbed(
|
|||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column {
|
||||
|
|
@ -74,8 +74,7 @@ fun RegularEmbed(
|
|||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
embed.iconURL?.let {
|
||||
|
|
@ -133,6 +132,11 @@ fun RegularEmbed(
|
|||
description = null // decorative
|
||||
)
|
||||
}
|
||||
|
||||
// Special
|
||||
embed.special?.let {
|
||||
SpecialEmbedSwitch(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import androidx.compose.foundation.layout.aspectRatio
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
|
|
@ -90,6 +90,25 @@ fun ImageAttachment(attachment: AutumnResource) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoPlayButton() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.aspectRatio(1f)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = R.string.media_viewer_play),
|
||||
modifier = Modifier
|
||||
.width(32.dp)
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoAttachment(attachment: AutumnResource) {
|
||||
val url = "$REVOLT_FILES/attachments/${attachment.id}/${attachment.filename}"
|
||||
|
|
@ -114,22 +133,8 @@ fun VideoAttachment(attachment: AutumnResource) {
|
|||
),
|
||||
description = attachment.filename ?: "Video"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.aspectRatio(1f)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = R.string.media_viewer_play),
|
||||
modifier = Modifier
|
||||
.width(32.dp)
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
|
||||
VideoPlayButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package chat.revolt.components.chat.specialembeds
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.revolt.api.schemas.Special
|
||||
|
||||
@Composable
|
||||
fun SpecialEmbedSwitch(special: Special, modifier: Modifier = Modifier) {
|
||||
when (special.type) {
|
||||
"YouTube" -> YoutubeEmbedSwitch(special, modifier)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package chat.revolt.components.chat.specialembeds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.schemas.Special
|
||||
import chat.revolt.components.chat.VideoPlayButton
|
||||
import chat.revolt.components.generic.RemoteImage
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
@Language("HTML")
|
||||
private const val YOUTUBE_EMBED_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const ytParams = new URLSearchParams()
|
||||
ytParams.set("autoplay", "1")
|
||||
ytParams.set("fs", "0")
|
||||
if ({{useTimestamp}}) {
|
||||
ytParams.set("start", "{{timestamp}}")
|
||||
}
|
||||
|
||||
const frame = document.createElement("iframe")
|
||||
frame.setAttribute("src", `https://www.youtube.com/embed/{{videoId}}?${'$'}{ytParams.toString()}`)
|
||||
frame.setAttribute("width", window.innerWidth)
|
||||
frame.setAttribute("height", window.innerHeight)
|
||||
frame.setAttribute("frameborder", 0)
|
||||
frame.setAttribute("allow", "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share")
|
||||
frame.setAttribute("referrerpolicy", "no-referrer")
|
||||
frame.setAttribute("allowfullscreen", "allowfullscreen")
|
||||
frame.setAttribute("title", "YouTube video player")
|
||||
document.body.appendChild(frame)
|
||||
</script>
|
||||
</body>
|
||||
"""
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun YouTubeEmbed(special: Special, modifier: Modifier = Modifier) {
|
||||
if (special.id == null) return
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
WebView(ctx).apply {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
javaScriptCanOpenWindowsAutomatically = true
|
||||
builtInZoomControls = false
|
||||
loadWithOverviewMode = true
|
||||
useWideViewPort = true
|
||||
displayZoomControls = false
|
||||
setSupportZoom(false)
|
||||
isVerticalScrollBarEnabled = false
|
||||
isHorizontalScrollBarEnabled = false
|
||||
}
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.loadDataWithBaseURL(
|
||||
null,
|
||||
YOUTUBE_EMBED_TEMPLATE
|
||||
.replace("{{videoId}}", special.id)
|
||||
.replace("{{useTimestamp}}", (special.timestamp != null).toString())
|
||||
.replace("{{timestamp}}", special.timestamp ?: ""),
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.width(with(LocalDensity.current) { 1280.toDp() })
|
||||
.aspectRatio(16f / 9f)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A switch that displays the thumbnail of a YouTube video and switches to the embedded player when clicked.
|
||||
* This ensures that the video is only loaded when the user wants to watch it for bandwidth reasons.
|
||||
*/
|
||||
@Composable
|
||||
fun YoutubeEmbedSwitch(special: Special, modifier: Modifier = Modifier) {
|
||||
var embedEnabled by remember { mutableStateOf(false) }
|
||||
|
||||
Crossfade(targetState = embedEnabled, label = "embed enabled") {
|
||||
if (it) {
|
||||
YouTubeEmbed(special, modifier)
|
||||
} else {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable {
|
||||
embedEnabled = true
|
||||
}
|
||||
.width(with(LocalDensity.current) { 1280.toDp() })
|
||||
.aspectRatio(16f / 9f)
|
||||
.wrapContentHeight()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
RemoteImage(
|
||||
"https://i3.ytimg.com/vi/${special.id ?: "none"}/maxresdefault.jpg",
|
||||
stringResource(R.string.message_embed_special_youtube_switch_alt),
|
||||
width = 1280,
|
||||
height = 720,
|
||||
)
|
||||
|
||||
VideoPlayButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,6 +197,8 @@
|
|||
<string name="message_blocked">Blocked message</string>
|
||||
<string name="message_failed_to_send">Failed to send, long press for options</string>
|
||||
|
||||
<string name="message_embed_special_youtube_switch_alt">Tap to play video from YouTube</string>
|
||||
|
||||
<string name="system_message_ownership_changed_alt">Ownership changed</string>
|
||||
<string name="system_message_channel_icon_changed_alt">Channel icon changed</string>
|
||||
<string name="system_message_channel_description_changed_alt">Channel description changed</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue