A cheap attempt at a native Bluesky client for Android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

SkeetView: recordWithMedia, abstract Embed composable

geesawra bc62d77e a3c69ea1

+177 -115
+175 -113
app/src/main/java/industries/geesawra/monarch/SkeetView.kt
··· 1 1 package industries.geesawra.monarch 2 2 3 + import android.content.Context 4 + import android.net.Uri 3 5 import androidx.browser.customtabs.CustomTabsIntent 4 6 import androidx.compose.foundation.clickable 5 7 import androidx.compose.foundation.layout.Arrangement ··· 30 32 import androidx.compose.ui.unit.dp 31 33 import androidx.core.net.toUri 32 34 import androidx.media3.common.MimeTypes 35 + import app.bsky.embed.ExternalViewExternal 36 + import app.bsky.embed.ImagesViewImage 33 37 import app.bsky.embed.RecordView 34 38 import app.bsky.embed.RecordViewRecordUnion 39 + import app.bsky.embed.RecordWithMediaView 40 + import app.bsky.embed.RecordWithMediaViewMediaUnion 35 41 import app.bsky.feed.FeedViewPostReasonUnion 36 42 import app.bsky.feed.PostViewEmbedUnion 37 43 import app.bsky.feed.ReplyRefParentUnion ··· 137 143 return 138 144 } 139 145 140 - val embed = skeet.embed 146 + Embeds(context, nested, skeet.embed) 147 + } 141 148 149 + @Composable 150 + fun Embeds(context: Context, nested: Boolean, embed: PostViewEmbedUnion?) { 142 151 when (embed) { 143 152 is PostViewEmbedUnion.ImagesView -> { 144 - val img = embed.value.images 145 - 146 - Card( 147 - modifier = Modifier 148 - .fillMaxWidth() 149 - .padding(top = 8.dp, bottom = 8.dp), 150 - ) { 151 - PostImageGallery( 152 - modifier = Modifier 153 - .fillMaxSize() 154 - .padding(8.dp), 155 - images = img.map { 156 - Image(url = it.thumb.uri, alt = it.alt) 157 - }, 158 - ) 159 - } 153 + ImageView(embed.value.images) 160 154 } 161 155 162 156 is PostViewEmbedUnion.VideoView -> { 163 - Card( 164 - modifier = Modifier 165 - .heightIn(max = 500.dp) 166 - .fillMaxWidth() 167 - .padding(top = 8.dp, bottom = 8.dp), 168 - ) { 169 - VideoPlayer( 170 - mediaItems = listOf( 171 - VideoPlayerMediaItem.NetworkMediaItem( 172 - url = embed.value.playlist.uri, 173 - mimeType = MimeTypes.APPLICATION_M3U8, 174 - ) 175 - ), 176 - handleLifecycle = false, 177 - autoPlay = false, 178 - usePlayerController = true, 179 - enablePip = false, 180 - handleAudioFocus = true, 181 - controllerConfig = VideoPlayerControllerConfig( 182 - showSpeedAndPitchOverlay = false, 183 - showSubtitleButton = false, 184 - showCurrentTimeAndTotalTime = true, 185 - showBufferingProgress = false, 186 - showForwardIncrementButton = true, 187 - showBackwardIncrementButton = true, 188 - showBackTrackButton = false, 189 - showNextTrackButton = false, 190 - showRepeatModeButton = true, 191 - controllerShowTimeMilliSeconds = 5_000, 192 - controllerAutoShow = true, 193 - showFullScreenButton = true, 194 - ), 195 - volume = 0.5f, // volume 0.0f to 1.0f 196 - repeatMode = RepeatMode.NONE, // or RepeatMode.ALL, RepeatMode.ONE 197 - modifier = Modifier 198 - .fillMaxSize() 199 - .padding(8.dp), 200 - ) 201 - } 157 + VideoView(embed.value.playlist.uri.toUri()) 202 158 } 203 159 204 160 is PostViewEmbedUnion.ExternalView -> { 205 - val ev = embed.value.external 206 - 207 - OutlinedCard( 208 - modifier = Modifier 209 - .padding(top = 8.dp) 210 - ) { 211 - Column( 212 - modifier = Modifier 213 - .fillMaxSize() 214 - .clickable { 215 - val customTabsIntent = CustomTabsIntent.Builder() 216 - .setShowTitle(true) 217 - .setUrlBarHidingEnabled(true) 218 - .build() 219 - customTabsIntent.launchUrl(context, ev.uri.uri.toUri()) 220 - } 221 - ) { 222 - ev.thumb?.let { 223 - AsyncImage( 224 - model = ImageRequest.Builder(LocalContext.current) 225 - .data(it.uri) 226 - .crossfade(true) 227 - .build(), 228 - contentScale = ContentScale.Crop, 229 - alignment = Alignment.Center, 230 - contentDescription = "External link thumbnail", 231 - modifier = Modifier 232 - .height(180.dp) 233 - .fillMaxWidth() 234 - ) 235 - 236 - HorizontalDivider( 237 - color = MaterialTheme.colorScheme.outlineVariant, 238 - ) 239 - } 240 - Text( 241 - text = ev.title, 242 - style = MaterialTheme.typography.titleSmall, 243 - fontWeight = FontWeight.Bold, 244 - modifier = Modifier 245 - .fillMaxSize() 246 - .padding(top = 8.dp, bottom = 4.dp, start = 8.dp, end = 8.dp), 247 - maxLines = 3 248 - ) 249 - Text( 250 - text = ev.description, 251 - style = MaterialTheme.typography.bodyMedium, 252 - modifier = Modifier 253 - .fillMaxSize() 254 - .padding(bottom = 8.dp, start = 8.dp, end = 8.dp), 255 - maxLines = 8 256 - ) 257 - } 258 - 259 - } 161 + ExternalView(context, embed.value.external) 260 162 } 261 163 262 164 is PostViewEmbedUnion.RecordView -> run { ··· 275 177 } 276 178 277 179 is PostViewEmbedUnion.RecordWithMediaView -> run { 278 - // TODO: map this 279 - // probably better to wrap this thing in a function that we can call recursively 180 + OutlinedCard( 181 + modifier = Modifier.padding(top = 8.dp) 182 + ) { 183 + RecordWithMediaView( 184 + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp), 185 + embed.value 186 + ) 187 + } 280 188 } 281 189 282 190 else -> {} 283 191 } 192 + } 284 193 194 + @Composable 195 + private fun ImageView(img: List<ImagesViewImage>) { 196 + Card( 197 + modifier = Modifier 198 + .fillMaxWidth() 199 + .padding(top = 8.dp, bottom = 8.dp), 200 + ) { 201 + PostImageGallery( 202 + modifier = Modifier 203 + .fillMaxSize() 204 + .padding(8.dp), 205 + images = img.map { 206 + Image(url = it.thumb.uri, alt = it.alt) 207 + }, 208 + ) 209 + } 210 + } 211 + 212 + @Composable 213 + private fun VideoView(uri: Uri) { 214 + Card( 215 + modifier = Modifier 216 + .heightIn(max = 500.dp) 217 + .fillMaxWidth() 218 + .padding(top = 8.dp, bottom = 8.dp), 219 + ) { 220 + VideoPlayer( 221 + mediaItems = listOf( 222 + VideoPlayerMediaItem.NetworkMediaItem( 223 + url = uri.toString(), 224 + mimeType = MimeTypes.APPLICATION_M3U8, 225 + ) 226 + ), 227 + handleLifecycle = false, 228 + autoPlay = false, 229 + usePlayerController = true, 230 + enablePip = false, 231 + handleAudioFocus = true, 232 + controllerConfig = VideoPlayerControllerConfig( 233 + showSpeedAndPitchOverlay = false, 234 + showSubtitleButton = false, 235 + showCurrentTimeAndTotalTime = true, 236 + showBufferingProgress = false, 237 + showForwardIncrementButton = true, 238 + showBackwardIncrementButton = true, 239 + showBackTrackButton = false, 240 + showNextTrackButton = false, 241 + showRepeatModeButton = true, 242 + controllerShowTimeMilliSeconds = 5_000, 243 + controllerAutoShow = true, 244 + showFullScreenButton = true, 245 + ), 246 + volume = 0.5f, // volume 0.0f to 1.0f 247 + repeatMode = RepeatMode.NONE, // or RepeatMode.ALL, RepeatMode.ONE 248 + modifier = Modifier 249 + .fillMaxSize() 250 + .padding(8.dp), 251 + ) 252 + } 253 + } 254 + 255 + @Composable 256 + private fun ExternalView(context: Context, ev: ExternalViewExternal) { 257 + OutlinedCard( 258 + modifier = Modifier 259 + .padding(top = 8.dp) 260 + ) { 261 + Column( 262 + modifier = Modifier 263 + .fillMaxSize() 264 + .clickable { 265 + val customTabsIntent = CustomTabsIntent.Builder() 266 + .setShowTitle(true) 267 + .setUrlBarHidingEnabled(true) 268 + .build() 269 + customTabsIntent.launchUrl(context, ev.uri.uri.toUri()) 270 + } 271 + ) { 272 + ev.thumb?.let { 273 + AsyncImage( 274 + model = ImageRequest.Builder(LocalContext.current) 275 + .data(it.uri) 276 + .crossfade(true) 277 + .build(), 278 + contentScale = ContentScale.Crop, 279 + alignment = Alignment.Center, 280 + contentDescription = "External link thumbnail", 281 + modifier = Modifier 282 + .height(180.dp) 283 + .fillMaxWidth() 284 + ) 285 + 286 + HorizontalDivider( 287 + color = MaterialTheme.colorScheme.outlineVariant, 288 + ) 289 + } 290 + Text( 291 + text = ev.title, 292 + style = MaterialTheme.typography.titleSmall, 293 + fontWeight = FontWeight.Bold, 294 + modifier = Modifier 295 + .fillMaxSize() 296 + .padding(top = 8.dp, bottom = 4.dp, start = 8.dp, end = 8.dp), 297 + maxLines = 3 298 + ) 299 + Text( 300 + text = ev.description, 301 + style = MaterialTheme.typography.bodyMedium, 302 + modifier = Modifier 303 + .fillMaxSize() 304 + .padding(bottom = 8.dp, start = 8.dp, end = 8.dp), 305 + maxLines = 8 306 + ) 307 + } 308 + 309 + } 285 310 } 286 311 287 312 @Composable ··· 301 326 } 302 327 303 328 else -> {} 329 + } 330 + } 331 + 332 + @Composable 333 + private fun RecordWithMediaView( 334 + modifier: Modifier = Modifier, 335 + rv: RecordWithMediaView 336 + ) { 337 + val media = rv.media 338 + val embed = when (media) { 339 + is RecordWithMediaViewMediaUnion.ExternalView -> PostViewEmbedUnion.ExternalView(media.value) 340 + is RecordWithMediaViewMediaUnion.ImagesView -> PostViewEmbedUnion.ImagesView(media.value) 341 + is RecordWithMediaViewMediaUnion.Unknown -> PostViewEmbedUnion.Unknown(media.value) 342 + is RecordWithMediaViewMediaUnion.VideoView -> PostViewEmbedUnion.VideoView(media.value) 343 + } 344 + 345 + val rv = rv.record.record 346 + val record = when (rv) { 347 + is RecordViewRecordUnion.FeedGeneratorView -> null 348 + is RecordViewRecordUnion.GraphListView -> null 349 + is RecordViewRecordUnion.GraphStarterPackViewBasic -> null 350 + is RecordViewRecordUnion.LabelerLabelerView -> null 351 + is RecordViewRecordUnion.Unknown -> null 352 + is RecordViewRecordUnion.ViewBlocked -> null 353 + is RecordViewRecordUnion.ViewDetached -> null 354 + is RecordViewRecordUnion.ViewNotFound -> null 355 + is RecordViewRecordUnion.ViewRecord -> SkeetData.fromRecordView(rv.value) 356 + } 357 + 358 + record?.let { 359 + record.embed = embed 360 + SkeetView( 361 + modifier = modifier, 362 + viewModel = null, 363 + skeet = record, 364 + nested = true 365 + ) 304 366 } 305 367 } 306 368
+2 -2
app/src/main/java/industries/geesawra/monarch/datalayer/Models.kt
··· 31 31 val authorHandle: Handle? = null, 32 32 val authorLabels: List<Label> = listOf(), 33 33 val content: String = "", 34 - val embed: PostViewEmbedUnion? = null, 34 + var embed: PostViewEmbedUnion? = null, 35 35 val reason: FeedViewPostReasonUnion? = null, 36 36 val reply: ReplyRef? = null, 37 37 val createdAt: Timestamp? = null, ··· 124 124 ) 125 125 } 126 126 } 127 - 127 + 128 128 fun replyRef(): PostReplyRef { 129 129 val thisPostRef = StrongRef(this.uri, this.cid) 130 130