Skip to content

Commit 8c8a4d5

Browse files
committed
PARTIAL USER PFP UPLOAD
1 parent 8c7c11f commit 8c8a4d5

7 files changed

Lines changed: 494 additions & 95 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ui/* linguist-detectable=false

app/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ dependencies {
9494
// Coil
9595
implementation("io.coil-kt:coil-compose:2.4.0")
9696

97+
// Add these dependencies for image handling
98+
implementation("io.coil-kt:coil-compose:2.4.0")
99+
implementation("com.google.accompanist:accompanist-permissions:0.30.1")
100+
101+
// For image cropping
102+
implementation("com.vanniktech:android-image-cropper:4.5.0")
103+
104+
// For camera and gallery (if using Jetpack CameraX)
105+
implementation("androidx.camera:camera-camera2:1.3.0")
106+
implementation("androidx.camera:camera-lifecycle:1.3.0")
107+
implementation("androidx.camera:camera-view:1.3.0")
108+
97109
// Testing
98110
testImplementation("junit:junit:4.13.2")
99111
androidTestImplementation("androidx.test.ext:junit:1.1.5")

app/src/main/AndroidManifest.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<!-- Required Permissions -->
6+
<uses-feature
7+
android:name="android.hardware.camera"
8+
android:required="false" />
9+
610
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
711
<uses-permission android:name="android.permission.WAKE_LOCK" />
812
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
913
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1014
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1115
<uses-permission android:name="android.permission.VIBRATE" />
16+
<uses-permission android:name="android.permission.CAMERA" />
17+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
18+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
1219

1320
<!-- Optional but useful permissions -->
1421
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
@@ -67,6 +74,16 @@
6774
android:exported="false"
6875
android:foregroundServiceType="shortService" />
6976

77+
<provider
78+
android:name="androidx.core.content.FileProvider"
79+
android:authorities="${applicationId}.provider"
80+
android:exported="false"
81+
android:grantUriPermissions="true">
82+
<meta-data
83+
android:name="android.support.FILE_PROVIDER_PATHS"
84+
android:resource="@xml/file_paths" />
85+
</provider>
86+
7087
</application>
7188

7289
</manifest>
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
// ui/components/ProfilePicturePicker.kt
2+
package com.example.dosezy.ui.components
3+
4+
import android.Manifest
5+
import android.content.Context
6+
import android.content.pm.PackageManager
7+
import android.net.Uri
8+
import android.os.Build
9+
import androidx.activity.compose.rememberLauncherForActivityResult
10+
import androidx.activity.result.contract.ActivityResultContracts
11+
import androidx.compose.foundation.BorderStroke
12+
import androidx.compose.foundation.clickable
13+
import androidx.compose.foundation.layout.Box
14+
import androidx.compose.foundation.layout.Column
15+
import androidx.compose.foundation.layout.Spacer
16+
import androidx.compose.foundation.layout.height
17+
import androidx.compose.foundation.layout.padding
18+
import androidx.compose.foundation.layout.size
19+
import androidx.compose.foundation.shape.CircleShape
20+
import androidx.compose.material.icons.Icons
21+
import androidx.compose.material.icons.filled.Add
22+
import androidx.compose.material.icons.filled.CameraAlt
23+
import androidx.compose.material.icons.filled.PhotoLibrary
24+
import androidx.compose.material3.AlertDialog
25+
import androidx.compose.material3.Icon
26+
import androidx.compose.material3.MaterialTheme
27+
import androidx.compose.material3.Surface
28+
import androidx.compose.material3.Text
29+
import androidx.compose.material3.TextButton
30+
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.getValue
32+
import androidx.compose.runtime.mutableStateOf
33+
import androidx.compose.runtime.remember
34+
import androidx.compose.runtime.setValue
35+
import androidx.compose.ui.Alignment
36+
import androidx.compose.ui.Modifier
37+
import androidx.compose.ui.draw.clip
38+
import androidx.compose.ui.graphics.Color
39+
import androidx.compose.ui.layout.ContentScale
40+
import androidx.compose.ui.platform.LocalContext
41+
import androidx.compose.ui.res.painterResource
42+
import androidx.compose.ui.unit.dp
43+
import androidx.core.content.ContextCompat
44+
import coil.compose.AsyncImage
45+
import coil.request.ImageRequest
46+
import com.example.dosezy.R
47+
import com.example.dosezy.utils.rememberImagePicker
48+
import java.io.File
49+
50+
@Composable
51+
fun ProfilePicturePicker(
52+
profilePicPath: String?,
53+
onProfilePictureSelected: (String?) -> Unit,
54+
modifier: Modifier = Modifier
55+
) {
56+
val context = LocalContext.current
57+
var showImageSourceDialog by remember { mutableStateOf(false) }
58+
var showPermissionDialog by remember { mutableStateOf(false) }
59+
var permissionMessage by remember { mutableStateOf("") }
60+
var pendingAction by remember { mutableStateOf<(() -> Unit)?>(null) }
61+
62+
val imagePicker = rememberImagePicker(context) { uri ->
63+
if (uri != null) {
64+
val savedPath = saveImageToInternalStorage(context, uri)
65+
onProfilePictureSelected(savedPath)
66+
}
67+
}
68+
69+
// Camera Permission Launcher
70+
val cameraPermissionLauncher = rememberLauncherForActivityResult(
71+
contract = ActivityResultContracts.RequestPermission(),
72+
onResult = { isGranted ->
73+
if (isGranted) {
74+
pendingAction?.invoke()
75+
} else {
76+
permissionMessage = "Camera permission is required to take photos. Please enable it in app settings."
77+
showPermissionDialog = true
78+
}
79+
pendingAction = null
80+
}
81+
)
82+
83+
// Storage Permission Launcher (for Android 12 and below, for Android 13+ use READ_MEDIA_IMAGES)
84+
val storagePermissionLauncher = rememberLauncherForActivityResult(
85+
contract = ActivityResultContracts.RequestPermission(),
86+
onResult = { isGranted ->
87+
if (isGranted) {
88+
pendingAction?.invoke()
89+
} else {
90+
permissionMessage = "Storage permission is required to access photos. Please enable it in app settings."
91+
showPermissionDialog = true
92+
}
93+
pendingAction = null
94+
}
95+
)
96+
97+
// Check and request permissions
98+
@Composable
99+
fun checkCameraPermission(action: @Composable () -> Unit) {
100+
val hasPermission = ContextCompat.checkSelfPermission(
101+
context,
102+
Manifest.permission.CAMERA
103+
) == PackageManager.PERMISSION_GRANTED
104+
105+
if (hasPermission) {
106+
action()
107+
} else {
108+
pendingAction = action as (() -> Unit)?
109+
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
110+
}
111+
}
112+
113+
@Composable
114+
fun checkStoragePermission(action: @Composable () -> Unit) {
115+
// For Android 13+, use READ_MEDIA_IMAGES instead of READ_EXTERNAL_STORAGE
116+
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
117+
Manifest.permission.READ_MEDIA_IMAGES
118+
} else {
119+
Manifest.permission.READ_EXTERNAL_STORAGE
120+
}
121+
122+
val hasPermission = ContextCompat.checkSelfPermission(
123+
context,
124+
permission
125+
) == PackageManager.PERMISSION_GRANTED
126+
127+
if (hasPermission) {
128+
action()
129+
} else {
130+
pendingAction = action as (() -> Unit)?
131+
storagePermissionLauncher.launch(permission)
132+
}
133+
}
134+
135+
Box(
136+
modifier = modifier
137+
.size(128.dp)
138+
.clickable { showImageSourceDialog = true },
139+
contentAlignment = Alignment.Center
140+
) {
141+
// Profile Image or Placeholder
142+
if (!profilePicPath.isNullOrEmpty()) {
143+
AsyncImage(
144+
model = ImageRequest.Builder(context)
145+
.data(File(profilePicPath))
146+
.crossfade(true)
147+
.build(),
148+
contentDescription = "Profile Picture",
149+
modifier = Modifier
150+
.size(128.dp)
151+
.clip(CircleShape),
152+
placeholder = painterResource(id = R.drawable.default_profile),
153+
error = painterResource(id = R.drawable.default_profile),
154+
contentScale = ContentScale.Crop
155+
)
156+
} else {
157+
Surface(
158+
shape = CircleShape,
159+
color = MaterialTheme.colorScheme.surfaceVariant,
160+
border = BorderStroke(2.dp, MaterialTheme.colorScheme.outline),
161+
modifier = Modifier.size(128.dp)
162+
) {
163+
Box(contentAlignment = Alignment.Center) {
164+
Icon(
165+
imageVector = Icons.Default.Add,
166+
contentDescription = "Add Profile Picture",
167+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
168+
modifier = Modifier.size(32.dp)
169+
)
170+
}
171+
}
172+
}
173+
174+
// Camera Icon Overlay
175+
Surface(
176+
shape = CircleShape,
177+
color = MaterialTheme.colorScheme.primary,
178+
modifier = Modifier
179+
.align(Alignment.BottomEnd)
180+
.size(40.dp)
181+
.clickable { showImageSourceDialog = true }
182+
) {
183+
Box(contentAlignment = Alignment.Center) {
184+
Icon(
185+
imageVector = Icons.Default.CameraAlt,
186+
contentDescription = "Change Profile Picture",
187+
tint = Color.White,
188+
modifier = Modifier.size(20.dp)
189+
)
190+
}
191+
}
192+
}
193+
194+
// Image Source Selection Dialog
195+
if (showImageSourceDialog) {
196+
ImageSourceDialog(
197+
onDismiss = { showImageSourceDialog = false },
198+
onCameraSelected = {
199+
showImageSourceDialog = false
200+
checkCameraPermission {
201+
val tempUri = imagePicker.prepareCameraIntent()
202+
imagePicker.getCameraLauncher().launch(tempUri)
203+
}
204+
},
205+
onGallerySelected = {
206+
showImageSourceDialog = false
207+
checkStoragePermission {
208+
imagePicker.getGalleryLauncher().launch("image/*")
209+
}
210+
}
211+
)
212+
}
213+
214+
// Permission Dialog
215+
if (showPermissionDialog) {
216+
PermissionAlertDialog(
217+
message = permissionMessage,
218+
onDismiss = { showPermissionDialog = false }
219+
)
220+
}
221+
}
222+
223+
@Composable
224+
fun ImageSourceDialog(
225+
onDismiss: () -> Unit,
226+
onCameraSelected: @Composable () -> Unit,
227+
onGallerySelected: @Composable () -> Unit
228+
) {
229+
AlertDialog(
230+
onDismissRequest = onDismiss,
231+
title = { Text("Select Image Source") },
232+
text = {
233+
Column {
234+
Text("Choose how you want to add your profile picture")
235+
Spacer(modifier = Modifier.height(8.dp))
236+
Text(
237+
"Camera: Take a new photo",
238+
style = MaterialTheme.typography.bodySmall,
239+
color = MaterialTheme.colorScheme.onSurfaceVariant
240+
)
241+
Text(
242+
"Gallery: Choose from your photos",
243+
style = MaterialTheme.typography.bodySmall,
244+
color = MaterialTheme.colorScheme.onSurfaceVariant
245+
)
246+
}
247+
},
248+
confirmButton = {
249+
TextButton(onClick = onGallerySelected as () -> Unit) {
250+
Icon(
251+
imageVector = Icons.Default.PhotoLibrary,
252+
contentDescription = null,
253+
modifier = Modifier.size(18.dp)
254+
)
255+
Text("Gallery", modifier = Modifier.padding(start = 8.dp))
256+
}
257+
},
258+
dismissButton = {
259+
TextButton(onClick = onCameraSelected as () -> Unit) {
260+
Icon(
261+
imageVector = Icons.Default.CameraAlt,
262+
contentDescription = null,
263+
modifier = Modifier.size(18.dp)
264+
)
265+
Text("Camera", modifier = Modifier.padding(start = 8.dp))
266+
}
267+
}
268+
)
269+
}
270+
271+
@Composable
272+
fun PermissionAlertDialog(
273+
message: String,
274+
onDismiss: () -> Unit
275+
) {
276+
AlertDialog(
277+
onDismissRequest = onDismiss,
278+
title = { Text("Permission Required") },
279+
text = { Text(message) },
280+
confirmButton = {
281+
TextButton(onClick = onDismiss) {
282+
Text("OK")
283+
}
284+
}
285+
)
286+
}
287+
288+
// Utility function to save image to internal storage
289+
private fun saveImageToInternalStorage(context: Context, uri: Uri): String {
290+
val inputStream = context.contentResolver.openInputStream(uri)
291+
val timeStamp = System.currentTimeMillis()
292+
val file = File(context.filesDir, "profile_$timeStamp.jpg")
293+
294+
inputStream?.use { input ->
295+
file.outputStream().use { output ->
296+
input.copyTo(output)
297+
}
298+
}
299+
300+
return file.absolutePath
301+
}

0 commit comments

Comments
 (0)