Skip to content

Commit 8f03455

Browse files
feat: Implement full-screen media viewer and download
This commit introduces a full-screen media viewer for both images and videos within the chat screen, along with the ability to download them. - **feat(media): Add full-screen Image and Video viewers** - Created `ImageViewer.kt` and `VideoPlayer.kt` to provide immersive, full-screen dialogs for viewing media. - Integrated `ExoPlayer` (Media3) for robust video playback, including standard player controls. - Added `media3` dependencies to `build.gradle.kts`. - **feat(media): Implement download functionality** - Added a download button to both the image and video viewers. - Utilizes Android's `DownloadManager` to save files to the `Downloads/Intra/` directory. - Added `WRITE_EXTERNAL_STORAGE` permission to `AndroidManifest.xml` for this purpose. - **refactor(ChatScreen): Integrate media viewers** - Clicking on an image or video thumbnail/message now opens the corresponding full-screen viewer instead of an external app. - The `MessageBubble` composable was updated to handle clicks on images and videos, triggering the new dialogs.
1 parent e1ecb1b commit 8f03455

5 files changed

Lines changed: 402 additions & 71 deletions

File tree

app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ dependencies {
8989
//Ya fir ye wala (jo standard hai):
9090
implementation("com.mesibo.api:webrtc:1.0.5")
9191

92+
// Video Player (Media3 ExoPlayer)
93+
implementation("androidx.media3:media3-exoplayer:1.2.0")
94+
implementation("androidx.media3:media3-ui:1.2.0")
95+
implementation("androidx.media3:media3-common:1.2.0")
96+
9297
// ... (बाकी libs.androidx.core.ktx, etc.)
9398
val room_version = "2.6.1"
9499
// Room Libraries

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
2424
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
2525
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
26+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2627

2728
<application
2829
android:name=".MyApplication"

app/src/main/java/com/example/intra/ChatScreen.kt

Lines changed: 124 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,94 @@ package com.example.intra
22

33
import android.content.Intent
44
import android.net.Uri
5+
import android.os.Build
56
import android.util.Log
7+
import android.util.Patterns
8+
import androidx.compose.animation.core.LinearEasing
9+
import androidx.compose.animation.core.RepeatMode
10+
import androidx.compose.animation.core.animateFloat
11+
import androidx.compose.animation.core.infiniteRepeatable
12+
import androidx.compose.animation.core.rememberInfiniteTransition
13+
import androidx.compose.animation.core.tween
614
import androidx.compose.foundation.background
7-
import androidx.compose.foundation.layout.*
15+
import androidx.compose.foundation.border
16+
import androidx.compose.foundation.clickable
17+
import androidx.compose.foundation.isSystemInDarkTheme
18+
import androidx.compose.foundation.layout.Arrangement
19+
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.PaddingValues
22+
import androidx.compose.foundation.layout.Row
23+
import androidx.compose.foundation.layout.Spacer
24+
import androidx.compose.foundation.layout.fillMaxSize
25+
import androidx.compose.foundation.layout.fillMaxWidth
26+
import androidx.compose.foundation.layout.height
27+
import androidx.compose.foundation.layout.heightIn
28+
import androidx.compose.foundation.layout.padding
29+
import androidx.compose.foundation.layout.size
30+
import androidx.compose.foundation.layout.width
31+
import androidx.compose.foundation.layout.widthIn
832
import androidx.compose.foundation.lazy.LazyColumn
933
import androidx.compose.foundation.lazy.items
1034
import androidx.compose.foundation.lazy.rememberLazyListState
1135
import androidx.compose.foundation.shape.CircleShape
1236
import androidx.compose.foundation.shape.RoundedCornerShape
37+
import androidx.compose.foundation.text.ClickableText
1338
import androidx.compose.material.icons.Icons
1439
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1540
import androidx.compose.material.icons.filled.AttachFile
16-
import androidx.compose.material.icons.filled.Send
41+
import androidx.compose.material.icons.filled.Call
1742
import androidx.compose.material.icons.filled.Download
1843
import androidx.compose.material.icons.filled.Image
19-
import androidx.compose.material.icons.filled.VideoLibrary
2044
import androidx.compose.material.icons.filled.InsertDriveFile
21-
import androidx.compose.material3.*
22-
import androidx.compose.runtime.*
45+
import androidx.compose.material.icons.filled.PlayCircle
46+
import androidx.compose.material.icons.filled.Send
47+
import androidx.compose.material.icons.filled.VideoLibrary
48+
import androidx.compose.material3.Button
49+
import androidx.compose.material3.ButtonDefaults
50+
import androidx.compose.material3.Card
51+
import androidx.compose.material3.CardDefaults
52+
import androidx.compose.material3.ExperimentalMaterial3Api
53+
import androidx.compose.material3.Icon
54+
import androidx.compose.material3.IconButton
55+
import androidx.compose.material3.LocalTextStyle
56+
import androidx.compose.material3.MaterialTheme
57+
import androidx.compose.material3.OutlinedTextField
58+
import androidx.compose.material3.Scaffold
59+
import androidx.compose.material3.Text
60+
import androidx.compose.material3.TopAppBar
61+
import androidx.compose.runtime.Composable
62+
import androidx.compose.runtime.DisposableEffect
63+
import androidx.compose.runtime.LaunchedEffect
64+
import androidx.compose.runtime.SideEffect
65+
import androidx.compose.runtime.getValue
66+
import androidx.compose.runtime.mutableStateOf
67+
import androidx.compose.runtime.remember
68+
import androidx.compose.runtime.setValue
2369
import androidx.compose.ui.Alignment
2470
import androidx.compose.ui.Modifier
71+
import androidx.compose.ui.draw.alpha
72+
import androidx.compose.ui.draw.clip
73+
import androidx.compose.ui.draw.rotate
2574
import androidx.compose.ui.graphics.Color
75+
import androidx.compose.ui.graphics.toArgb
76+
import androidx.compose.ui.layout.ContentScale
2677
import androidx.compose.ui.platform.LocalContext
27-
import androidx.compose.ui.text.font.FontStyle
28-
import androidx.compose.material.icons.filled.Call
29-
import androidx.compose.ui.unit.dp
30-
import androidx.compose.ui.unit.sp
31-
import androidx.compose.ui.draw.clip
32-
import androidx.compose.foundation.text.ClickableText
78+
import androidx.compose.ui.platform.LocalView
3379
import androidx.compose.ui.text.SpanStyle
3480
import androidx.compose.ui.text.buildAnnotatedString
81+
import androidx.compose.ui.text.font.FontStyle
3582
import androidx.compose.ui.text.style.TextDecoration
36-
import android.util.Patterns
37-
import androidx.compose.ui.layout.ContentScale
38-
import kotlinx.coroutines.delay
39-
import java.text.SimpleDateFormat
40-
import java.util.*
83+
import androidx.compose.ui.unit.dp
84+
import androidx.compose.ui.unit.sp
85+
import androidx.core.view.WindowCompat
4186
import coil.compose.AsyncImage
4287
import coil.request.ImageRequest
43-
import androidx.compose.animation.core.*
44-
import androidx.compose.foundation.border
45-
import androidx.compose.material.icons.filled.PlayCircle
46-
import androidx.compose.ui.draw.rotate
47-
import androidx.compose.ui.graphics.RectangleShape
48-
import androidx.compose.ui.draw.alpha
4988
import coil.request.videoFrameMillis
50-
import androidx.compose.ui.graphics.toArgb
51-
import androidx.compose.ui.platform.LocalView
52-
import androidx.core.view.WindowCompat
53-
import androidx.compose.foundation.isSystemInDarkTheme
54-
import android.os.Build
89+
import java.text.SimpleDateFormat
90+
import java.util.Date
91+
import java.util.Locale
92+
import kotlinx.coroutines.delay
5593

5694

5795
@OptIn(ExperimentalMaterial3Api::class)
@@ -64,12 +102,13 @@ fun ChatScreen(
64102
onStartCall: () -> Unit,
65103
) {
66104
val listState = rememberLazyListState()
105+
var videoUrlToPlay by remember { mutableStateOf<String?>(null) }
106+
var imageUrlToView by remember { mutableStateOf<String?>(null) } // 👈 YE ADD HUA
67107

68108
// Set Status Bar Color
69109
val view = LocalView.current
70110
val isDark = isSystemInDarkTheme()
71111
val backgroundColor = MaterialTheme.colorScheme.background
72-
val primaryDarkColor = Color(0xFF512DA8) // Dark Purple (Aapka theme color)
73112

74113
if (!view.isInEditMode) {
75114
SideEffect {
@@ -80,8 +119,8 @@ fun ChatScreen(
80119
window.statusBarColor = backgroundColor.toArgb()
81120
insetsController.isAppearanceLightStatusBars = !isDark
82121
} else {
83-
if (!isDark) {
84-
window.statusBarColor = Color.Black.toArgb()
122+
if (!isDark) {
123+
window.statusBarColor = Color.Black.toArgb()
85124
} else {
86125
window.statusBarColor = backgroundColor.toArgb()
87126
}
@@ -118,6 +157,22 @@ fun ChatScreen(
118157
}
119158
}
120159

160+
if (videoUrlToPlay != null) {
161+
VideoPlayerDialog(
162+
videoUrl = videoUrlToPlay!!,
163+
onDismiss = { videoUrlToPlay = null }
164+
)
165+
}
166+
167+
// 👈 YE CODE ADD HUA:
168+
if (imageUrlToView != null) {
169+
ImageViewerDialog(
170+
imageUrl = imageUrlToView!!,
171+
onDismiss = { imageUrlToView = null }
172+
)
173+
}
174+
175+
121176
Scaffold(
122177
topBar = {
123178
TopAppBar(
@@ -163,7 +218,6 @@ fun ChatScreen(
163218
.padding(padding)
164219
.background(MaterialTheme.colorScheme.background)
165220
) {
166-
// 🔥 FIX 3: Messages bottom se start honge (reverseLayout hataya)
167221
LazyColumn(
168222
state = listState,
169223
modifier = Modifier.weight(1f).fillMaxWidth(),
@@ -175,10 +229,14 @@ fun ChatScreen(
175229
)
176230
) {
177231
items(viewModel.messages) { msg ->
178-
MessageBubble(msg)
232+
// 👈 YE LINE UPDATE HUI
233+
MessageBubble(
234+
message = msg,
235+
onVideoClick = { url -> videoUrlToPlay = url },
236+
onImageClick = { url -> imageUrlToView = url }
237+
)
179238
}
180239

181-
// Typing Indicator inside the list
182240
if (isTyping) {
183241
item {
184242
TypingIndicatorUI(receiverName)
@@ -274,7 +332,11 @@ fun TypingIndicatorUI(name: String) {
274332
}
275333

276334
@Composable
277-
fun MessageBubble(message: ChatMessage) {
335+
fun MessageBubble(
336+
message: ChatMessage,
337+
onVideoClick: (String) -> Unit = {},
338+
onImageClick: (String) -> Unit = {} // 👈 YE ADD HUA
339+
) {
278340
val context = LocalContext.current
279341

280342
Row(
@@ -296,42 +358,31 @@ fun MessageBubble(message: ChatMessage) {
296358
) {
297359
Column(modifier = Modifier.padding(10.dp)) {
298360

299-
// File extensions detect karo
300361
val fileName = message.fileName ?: "File"
301362
val fileExtension = fileName.substringAfterLast(".", "").lowercase()
302363

303364
val isImage = fileExtension in listOf("jpg", "jpeg", "png", "gif", "webp")
304-
// 🔥 Video detect karne ke liye
305365
val isVideo = fileExtension in listOf("mp4", "mkv", "avi", "mov", "webm")
306366

307-
// ==========================================
308-
// CASE 1: UPLOADING STAGE (Loader + Thumbnail)
309-
// ==========================================
310367
if (message.isLoading) {
311-
312-
// Agar Image YA Video hai, toh thumbnail dikhao
313-
if (message.localUri != null && (isImage || isVideo)) { // 👈 '|| isVideo' add kiya
368+
if (message.localUri != null && (isImage || isVideo)) {
314369
Box(contentAlignment = Alignment.Center) {
315-
// 1. Thumbnail
316370
AsyncImage(
317371
model = ImageRequest.Builder(context)
318-
.data(message.localUri) // Video path
372+
.data(message.localUri)
319373
.crossfade(true)
320374
.build(),
321375
contentDescription = "Uploading Preview",
322376
modifier = Modifier
323377
.fillMaxWidth()
324378
.heightIn(max = 200.dp)
325379
.clip(RoundedCornerShape(8.dp))
326-
.alpha(0.6f), // Thoda dhundhla taaki loader dikhe
380+
.alpha(0.6f),
327381
contentScale = ContentScale.Crop
328382
)
329-
330-
// 2. 🔥 Aapka Favorite Square Loader (Center mein)
331383
UniqueLoader()
332384
}
333385
} else {
334-
// Non-media file uploading (Doc/PDF etc)
335386
Row(verticalAlignment = Alignment.CenterVertically) {
336387
UniqueLoader(Modifier.size(24.dp))
337388
Spacer(Modifier.width(8.dp))
@@ -342,31 +393,32 @@ fun MessageBubble(message: ChatMessage) {
342393
)
343394
}
344395
}
345-
}
396+
} else if (message.type == "file" && message.fileUrl != null) {
346397

347-
// ==========================================
348-
// CASE 2: FILE SENT / RECEIVED (Thumbnail + Play Icon)
349-
// ==========================================
350-
else if (message.type == "file" && message.fileUrl != null) {
351-
352-
// Agar Image YA Video hai
353-
if (isImage || isVideo) { // 👈 Yahan bhi Video allow kiya
398+
if (isImage || isVideo) {
354399

355400
val settingsManager = remember { SettingsManager(context) }
356401
val baseUrl = settingsManager.getBaseUrl().removeSuffix("/")
357402

358-
// URL banao
359403
val fullUrl = if (message.fileUrl.startsWith("http"))
360404
message.fileUrl
361405
else
362406
baseUrl + message.fileUrl
363407

364-
Box(contentAlignment = Alignment.Center) {
365-
// 1. Thumbnail Load karo
408+
Box(
409+
contentAlignment = Alignment.Center,
410+
modifier = Modifier.clickable {
411+
if (isVideo) {
412+
onVideoClick(fullUrl)
413+
} else if (isImage) {
414+
onImageClick(fullUrl)
415+
}
416+
}
417+
) {
366418
AsyncImage(
367419
model = ImageRequest.Builder(context)
368420
.data(fullUrl)
369-
.videoFrameMillis(2000) // 👈 Video ke 2nd second ka frame lega (better thumbnail)
421+
.videoFrameMillis(2000)
370422
.crossfade(true)
371423
.build(),
372424
contentDescription = fileName,
@@ -377,10 +429,9 @@ fun MessageBubble(message: ChatMessage) {
377429
contentScale = ContentScale.Crop
378430
)
379431

380-
// 2. 🔥 Video hai toh PLAY icon dikhao
381432
if (isVideo) {
382433
Icon(
383-
imageVector = Icons.Default.PlayCircle, // Ensure icon import
434+
imageVector = Icons.Default.PlayCircle,
384435
contentDescription = "Play",
385436
tint = Color.White.copy(alpha = 0.8f),
386437
modifier = Modifier.size(48.dp)
@@ -392,12 +443,10 @@ fun MessageBubble(message: ChatMessage) {
392443
Spacer(Modifier.height(6.dp))
393444
}
394445

395-
// 📁 File Info Row
396446
Row(
397447
verticalAlignment = Alignment.CenterVertically,
398448
modifier = Modifier.fillMaxWidth()
399449
) {
400-
// File Icon based on type
401450
Icon(
402451
imageVector = when {
403452
isImage -> Icons.Default.Image
@@ -427,7 +476,6 @@ fun MessageBubble(message: ChatMessage) {
427476

428477
Spacer(Modifier.height(8.dp))
429478

430-
// 🔥 Modern Action Button
431479
Button(
432480
onClick = {
433481
try {
@@ -437,9 +485,16 @@ fun MessageBubble(message: ChatMessage) {
437485
message.fileUrl
438486
else
439487
baseUrl + message.fileUrl
440-
441-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(finalUrl))
442-
context.startActivity(intent)
488+
489+
// 👈 YE LOGIC UPDATE HUA
490+
if (isVideo) {
491+
onVideoClick(finalUrl)
492+
} else if (isImage) {
493+
onImageClick(finalUrl)
494+
} else {
495+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(finalUrl))
496+
context.startActivity(intent)
497+
}
443498
} catch (e: Exception) {
444499
Log.e("Chat", "File open error", e)
445500
}
@@ -465,7 +520,6 @@ fun MessageBubble(message: ChatMessage) {
465520
}
466521

467522
} else {
468-
// 💬 TEXT MESSAGE (With Clickable Links)
469523
val textColor = if (message.isSelf)
470524
MaterialTheme.colorScheme.onPrimary
471525
else
@@ -514,8 +568,7 @@ fun MessageBubble(message: ChatMessage) {
514568
}
515569
)
516570
}
517-
518-
// 🕒 TIMESTAMP
571+
519572
message.timestamp?.let {
520573
Spacer(Modifier.height(4.dp))
521574
Text(

0 commit comments

Comments
 (0)