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.

ComposeView: better experience, now it actually make sense

geesawra f8ae80ad 444ae5ae

+395 -205
+185 -141
app/src/main/java/industries/geesawra/jerryno/ComposeView.kt
··· 3 3 import android.content.Context 4 4 import android.net.Uri 5 5 import android.widget.Toast 6 + import androidx.activity.compose.ManagedActivityResultLauncher 6 7 import androidx.activity.compose.rememberLauncherForActivityResult 7 8 import androidx.activity.result.PickVisualMediaRequest 8 9 import androidx.activity.result.contract.ActivityResultContracts 10 + import androidx.compose.foundation.ScrollState 9 11 import androidx.compose.foundation.layout.Arrangement 10 12 import androidx.compose.foundation.layout.Box 11 13 import androidx.compose.foundation.layout.Column 12 14 import androidx.compose.foundation.layout.Row 13 15 import androidx.compose.foundation.layout.Spacer 14 16 import androidx.compose.foundation.layout.WindowInsets 17 + import androidx.compose.foundation.layout.add 15 18 import androidx.compose.foundation.layout.fillMaxSize 16 19 import androidx.compose.foundation.layout.fillMaxWidth 20 + import androidx.compose.foundation.layout.height 17 21 import androidx.compose.foundation.layout.heightIn 18 22 import androidx.compose.foundation.layout.ime 23 + import androidx.compose.foundation.layout.navigationBars 19 24 import androidx.compose.foundation.layout.padding 20 25 import androidx.compose.foundation.layout.size 21 26 import androidx.compose.foundation.layout.windowInsetsPadding 22 27 import androidx.compose.foundation.text.KeyboardActions 28 + import androidx.compose.foundation.verticalScroll 23 29 import androidx.compose.material.icons.Icons 24 30 import androidx.compose.material.icons.automirrored.filled.Send 25 31 import androidx.compose.material.icons.filled.CameraRoll ··· 36 42 import androidx.compose.material3.TextButton 37 43 import androidx.compose.runtime.Composable 38 44 import androidx.compose.runtime.LaunchedEffect 45 + import androidx.compose.runtime.MutableState 39 46 import androidx.compose.runtime.getValue 40 47 import androidx.compose.runtime.mutableIntStateOf 41 48 import androidx.compose.runtime.mutableStateOf ··· 58 65 context: Context, 59 66 coroutineScope: CoroutineScope, 60 67 timelineViewModel: TimelineViewModel, 61 - scaffoldState: BottomSheetScaffoldState 68 + scaffoldState: BottomSheetScaffoldState, 69 + scrollState: ScrollState 62 70 ) { 63 71 val focusRequester = remember { FocusRequester() } 64 72 val keyboardController = LocalSoftwareKeyboardController.current ··· 71 79 LaunchedEffect(scaffoldState.bottomSheetState.isVisible) { 72 80 if (scaffoldState.bottomSheetState.isVisible) { 73 81 focusRequester.requestFocus() 74 - keyboardController?.show() 75 82 } else { 76 83 keyboardController?.hide() 84 + // Reset state when sheet is hidden 85 + postText = "" 77 86 mediaSelected.value = mapOf() 87 + mediaSelectedIsVideo.value = false 88 + 78 89 } 79 90 } 80 91 ··· 84 95 return@rememberLauncherForActivityResult 85 96 } 86 97 87 - val urisMap = uris.associateWith { 88 - val mimeType: String? = context.contentResolver.getType(it) 98 + val urisMap = uris.associateWith { uri -> 99 + val mimeType: String? = context.contentResolver.getType(uri) 89 100 mimeType?.let { 90 - if (mimeType.startsWith("image/")) { 101 + if (it.startsWith("image/")) { 91 102 return@associateWith "image" 92 - } else if (mimeType.startsWith("video/")) { 103 + } else if (it.startsWith("video/")) { 93 104 return@associateWith "video" 94 105 } 95 106 } 96 - 97 - return@associateWith null 107 + null 98 108 } 99 109 100 - if (urisMap.size > 1 && urisMap.values.find { 101 - it?.let { 102 - return@find (it == "video") 103 - } 110 + val containsVideo = urisMap.values.any { it == "video" } 104 111 105 - return@find false 106 - } != null) { 112 + if (urisMap.size > 1 && containsVideo) { 107 113 Toast.makeText( 108 114 context, 109 - "Can only post up to 1 video or 4 pictures", 110 - Toast.LENGTH_SHORT 115 + "Can only post up to 1 video or 4 pictures. Video cannot be mixed with other media.", 116 + Toast.LENGTH_LONG 111 117 ).show() 112 - 113 118 return@rememberLauncherForActivityResult 114 119 } 115 - 116 - if (urisMap.size == 1 && urisMap.values.first() == "video") { 117 - mediaSelectedIsVideo.value = true 120 + if (containsVideo && urisMap.any { it.value == "image" }) { 121 + Toast.makeText( 122 + context, 123 + "Video cannot be mixed with other media.", 124 + Toast.LENGTH_LONG 125 + ).show() 126 + return@rememberLauncherForActivityResult 118 127 } 119 128 120 - mediaSelected.value = urisMap 129 + mediaSelectedIsVideo.value = containsVideo && urisMap.size == 1 130 + mediaSelected.value = urisMap.filterValues { it != null } 121 131 } 122 132 123 133 val uploadingPost = remember { mutableStateOf(false) } 134 + 135 + // Outer Box: Handles IME padding and general content padding for the whole sheet content 124 136 Box( 125 137 modifier = Modifier 126 - .fillMaxWidth() 127 - .windowInsetsPadding( 128 - WindowInsets.ime 129 - ) 130 - .padding(16.dp) // General content padding 138 + .fillMaxWidth() // Takes full width of the bottom sheet 139 + .windowInsetsPadding(WindowInsets.ime.add(WindowInsets.navigationBars)) 140 + .verticalScroll(scrollState) 141 + .padding(16.dp) // General padding around all content inside the sheet 131 142 ) { 132 - Column( 133 - modifier = Modifier 134 - .fillMaxWidth(), // Removed .fillMaxHeight() 135 - horizontalAlignment = Alignment.CenterHorizontally 136 - ) { 137 - Row { 138 - Text( 139 - "New Post", 140 - style = MaterialTheme.typography.titleLarge, 141 - modifier = Modifier.padding(bottom = 16.dp) 142 - ) 143 - } 144 - 145 - val charCount = remember { mutableIntStateOf(0) } 146 - val wasEdited = remember { mutableStateOf(false) } 147 - 148 - OutlinedTextField( 149 - keyboardActions = KeyboardActions( 150 - onDone = { 151 - this.defaultKeyboardAction(ImeAction.Done) 152 - keyboardController?.hide() 153 - } 154 - ), 155 - value = postText, 156 - onValueChange = { 157 - wasEdited.value = true 158 - postText = it 159 - charCount.intValue = it.length 160 - }, 143 + // Inner Box: Fills the space provided by Outer Box, used for layering scrollable content and fixed buttons 144 + Box(modifier = Modifier.fillMaxSize()) { 145 + // Scrollable Content Column 146 + Column( 161 147 modifier = Modifier 162 - .fillMaxWidth() 163 - .weight(1f) 164 - .focusRequester(focusRequester), 165 - label = { 166 - if (wasEdited.value) { 167 - Text( 168 - text = "${maxChars - charCount.intValue}", 169 - color = if (postText.length > maxChars) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface 170 - ) 171 - } else { 172 - Text( 173 - text = "Less cringe this time, okay?", 174 - ) 175 - } 176 - }, 177 - isError = postText.length > maxChars 178 - ) 148 + .fillMaxWidth(), // Takes full width of the Inner Box 149 + horizontalAlignment = Alignment.CenterHorizontally 150 + ) { 151 + Row { 152 + Text( 153 + "New Post", 154 + style = MaterialTheme.typography.titleLarge, 155 + modifier = Modifier.padding(bottom = 16.dp) 156 + ) 157 + } 179 158 180 - Spacer(modifier = Modifier.padding(4.dp)) 159 + val charCount = remember { mutableIntStateOf(0) } 160 + val wasEdited = remember { mutableStateOf(false) } 181 161 182 - if (mediaSelected.value.isNotEmpty()) { 183 - Card( 162 + OutlinedTextField( 163 + keyboardActions = KeyboardActions( 164 + onDone = { 165 + this.defaultKeyboardAction(ImeAction.Done) 166 + keyboardController?.hide() 167 + } 168 + ), 169 + value = postText, 170 + onValueChange = { 171 + wasEdited.value = true 172 + postText = it 173 + charCount.intValue = it.length 174 + }, 184 175 modifier = Modifier 185 - .heightIn(max = 180.dp) 186 176 .fillMaxWidth() 187 - .padding(8.dp) // This padding is for the Card itself, not the Box's content 188 - ) { 189 - PostImageGallery( 190 - modifier = Modifier 191 - .fillMaxSize() 192 - .padding(8.dp), 193 - images = mediaSelected.value.keys.map { 194 - Image( 195 - url = it.toString(), 196 - alt = "" 177 + .heightIn(min = 250.dp) // Adjusted min height for text field 178 + .focusRequester(focusRequester), 179 + label = { 180 + if (wasEdited.value) { 181 + Text( 182 + text = "${maxChars - charCount.intValue}", 183 + color = if (postText.length > maxChars) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface 197 184 ) 198 - }, 199 - ) 200 - } 201 - } 185 + } else { 186 + Text( 187 + text = "Less cringe this time, okay?", 188 + ) 189 + } 190 + }, 191 + isError = postText.length > maxChars, 192 + maxLines = 10 193 + ) 202 194 203 - Spacer(modifier = Modifier.padding(4.dp)) 195 + ActionRow( 196 + context, 197 + uploadingPost, 198 + pickMedia, 199 + postText, 200 + mediaSelected, 201 + mediaSelectedIsVideo, 202 + coroutineScope, 203 + maxChars, 204 + timelineViewModel, 205 + scaffoldState 206 + ) 204 207 205 - Row( 206 - modifier = Modifier.fillMaxWidth(), 207 - horizontalArrangement = Arrangement.SpaceBetween, 208 - verticalAlignment = Alignment.CenterVertically 209 - ) { 210 - TextButton( 211 - onClick = { 212 - pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) 208 + Spacer(modifier = Modifier.height(8.dp)) 209 + 210 + if (mediaSelected.value.isNotEmpty()) { 211 + Card( 212 + modifier = Modifier 213 + .fillMaxWidth() 214 + .padding(vertical = 8.dp) 215 + ) { 216 + PostImageGallery( 217 + modifier = Modifier 218 + .fillMaxWidth() // Gallery should fill card width 219 + .padding(8.dp), 220 + images = mediaSelected.value.keys.map { uri -> 221 + Image(url = uri.toString(), alt = "Selected media") 222 + }, 223 + ) 213 224 } 214 - ) { 215 - Icon(Icons.Default.CameraRoll, contentDescription = "Attach media") 216 225 } 217 226 218 - if (uploadingPost.value) { 219 - CircularProgressIndicator() 220 - } 227 + // Spacer at the end of scrollable content to prevent overlap with fixed buttons 228 + Spacer(modifier = Modifier.height(80.dp)) 229 + } 230 + } 231 + } 232 + } 233 + 234 + @OptIn(ExperimentalMaterial3Api::class) 235 + @Composable 236 + fun ActionRow( 237 + context: Context, 238 + uploadingPost: MutableState<Boolean>, 239 + pickMedia: ManagedActivityResultLauncher<PickVisualMediaRequest, List<@JvmSuppressWildcards Uri>>, 240 + postText: String, 241 + mediaSelected: MutableState<Map<Uri, String?>>, 242 + mediaSelectedIsVideo: MutableState<Boolean>, 243 + coroutineScope: CoroutineScope, 244 + maxChars: Int, 245 + timelineViewModel: TimelineViewModel, 246 + scaffoldState: BottomSheetScaffoldState 247 + ) { 248 + 249 + Row( 250 + modifier = Modifier 251 + .fillMaxWidth() 252 + .padding( 253 + vertical = 8.dp, 254 + ), // Internal padding for the button row 255 + horizontalArrangement = Arrangement.SpaceBetween, 256 + verticalAlignment = Alignment.CenterVertically 257 + ) { 258 + TextButton( 259 + onClick = { 260 + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) 261 + } 262 + ) { 263 + Icon(Icons.Default.CameraRoll, contentDescription = "Attach media") 264 + } 265 + 266 + if (uploadingPost.value) { 267 + CircularProgressIndicator() 268 + } 269 + 270 + val postButtonEnabled = remember(postText, mediaSelected.value) { 271 + (postText.isNotBlank() || mediaSelected.value.isNotEmpty()) && postText.length <= maxChars 272 + } 221 273 222 - val postButtonEnabled = remember { mutableStateOf(true) } 223 - Button( 224 - onClick = { 225 - if (postText.isNotBlank() && postText.length <= maxChars) { 274 + Button( 275 + onClick = { 276 + if (postText.isNotBlank() && postText.length <= maxChars) { 277 + coroutineScope.launch { 278 + uploadingPost.value = true // Show progress immediately 279 + timelineViewModel.post( 280 + postText, 281 + if (!mediaSelectedIsVideo.value) mediaSelected.value.keys.toList() 282 + .ifEmpty { null } else null, 283 + if (mediaSelectedIsVideo.value) mediaSelected.value.keys.firstOrNull() else null, 284 + ).onSuccess { 226 285 coroutineScope.launch { 227 - postButtonEnabled.value = false 228 - uploadingPost.value = true 229 - timelineViewModel.post( 230 - postText, 231 - if (!mediaSelectedIsVideo.value) mediaSelected.value.keys.toList() 232 - .ifEmpty { null } else null, 233 - if (mediaSelectedIsVideo.value) mediaSelected.value.keys.toList() 234 - .firstOrNull() 235 - else null, 236 - ).onSuccess { 237 - scaffoldState.bottomSheetState.hide() 238 - postText = "" 239 - wasEdited.value = false 240 - postButtonEnabled.value = true 241 - uploadingPost.value = false 242 - }.onFailure { 243 - postButtonEnabled.value = true 244 - Toast.makeText( 245 - context, 246 - "Could not post: ${it.message}", 247 - Toast.LENGTH_LONG 248 - ).show() 249 - uploadingPost.value = false 250 - postButtonEnabled.value = true 251 - } 286 + scaffoldState.bottomSheetState.hide() 287 + // State reset is now in LaunchedEffect for isVisible 252 288 } 289 + }.onFailure { 290 + Toast.makeText( 291 + context, 292 + "Could not post: ${it.message}", 293 + Toast.LENGTH_LONG 294 + ).show() 295 + }.also { 296 + uploadingPost.value = false // Hide progress after completion 253 297 } 254 - }, 255 - enabled = (postText.isNotBlank() || mediaSelected.value.isNotEmpty()) && postText.length <= maxChars && postButtonEnabled.value 256 - ) { 257 - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Post") 258 - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 259 - Text("Skeet") 298 + } 260 299 } 261 - } 300 + }, 301 + modifier = Modifier.padding(end = 8.dp), 302 + enabled = postButtonEnabled && !uploadingPost.value // Disable while uploading 303 + ) { 304 + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Post") 305 + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 306 + Text("Skeet") 262 307 } 263 308 } 264 - 265 309 }
+158 -29
app/src/main/java/industries/geesawra/jerryno/PostImageGallery.kt
··· 2 2 3 3 import androidx.compose.foundation.clickable 4 4 import androidx.compose.foundation.layout.Arrangement 5 + import androidx.compose.foundation.layout.Column 6 + import androidx.compose.foundation.layout.IntrinsicSize // Added import 5 7 import androidx.compose.foundation.layout.Row 8 + import androidx.compose.foundation.layout.RowScope 9 + import androidx.compose.foundation.layout.Spacer 10 + import androidx.compose.foundation.layout.aspectRatio 11 + import androidx.compose.foundation.layout.fillMaxHeight // Added import 6 12 import androidx.compose.foundation.layout.fillMaxWidth 13 + import androidx.compose.foundation.layout.height // Added import 7 14 import androidx.compose.foundation.shape.RoundedCornerShape 8 15 import androidx.compose.runtime.Composable 9 16 import androidx.compose.runtime.mutableStateOf ··· 30 37 val galleryVisible = remember { mutableStateOf<Int?>(null) } 31 38 32 39 galleryVisible.value?.let { 33 - GalleryViewer( 34 - imageUrls = images, 35 - initialPage = it 36 - ) { 37 - galleryVisible.value = null 40 + // Ensure the index is valid for the original images list 41 + if (it < images.size) { 42 + GalleryViewer( 43 + imageUrls = images, // Pass the full list to the viewer 44 + initialPage = it // 'it' is the index in the original images list 45 + ) { 46 + galleryVisible.value = null 47 + } 38 48 } 39 49 } 40 50 41 - Row( 42 - modifier = modifier.fillMaxWidth(), 43 - // This automatically adds 4.dp of space between each image 44 - horizontalArrangement = Arrangement.spacedBy(8.dp) 45 - ) { 46 - // We take the first 4 images and give them each a weight 47 - images.take(4).forEachIndexed { idx, image -> 48 - AsyncImage( 49 - model = ImageRequest.Builder(LocalContext.current) 50 - .data(image.url) 51 - .crossfade(true) 52 - .build(), 53 - contentDescription = image.alt, 54 - contentScale = ContentScale.Crop, // Fills the space 55 - modifier = Modifier 56 - .clickable { 57 - galleryVisible.value = idx 58 - } 59 - // 1. Give each image an equal share of the width 60 - .weight(1f) 61 - // 3. Apply rounded corners 62 - .clip(RoundedCornerShape(12.dp)) 63 - ) 51 + val imagesToDisplay = images.take(4) 52 + 53 + if (imagesToDisplay.isEmpty()) { 54 + return 55 + } 56 + 57 + when (imagesToDisplay.size) { 58 + 1 -> { 59 + Row( 60 + modifier = modifier 61 + .fillMaxWidth() 62 + ) { 63 + AsyncImage( 64 + model = ImageRequest.Builder(LocalContext.current) 65 + .data(imagesToDisplay[0].url) 66 + .crossfade(true) 67 + .build(), 68 + contentDescription = imagesToDisplay[0].alt, 69 + contentScale = ContentScale.Crop, 70 + modifier = Modifier 71 + .fillMaxWidth() 72 + .aspectRatio(1f) // Added aspect ratio for defined height 73 + .clip(RoundedCornerShape(12.dp)) 74 + .clickable { galleryVisible.value = 0 } // Index in original list 75 + ) 76 + } 77 + } 78 + 79 + 2 -> { 80 + Row( 81 + modifier = modifier.fillMaxWidth(), 82 + horizontalArrangement = Arrangement.spacedBy(8.dp) 83 + ) { 84 + GalleryImageCell( 85 + image = imagesToDisplay[0], 86 + originalIndex = 0, 87 + onImageClick = { galleryVisible.value = it }) 88 + GalleryImageCell( 89 + image = imagesToDisplay[1], 90 + originalIndex = 1, 91 + onImageClick = { galleryVisible.value = it }) 92 + } 93 + } 94 + 95 + 3 -> { 96 + Column( 97 + modifier = modifier.fillMaxWidth(), 98 + verticalArrangement = Arrangement.spacedBy(8.dp) 99 + ) { 100 + Row( 101 + modifier = Modifier.fillMaxWidth(), 102 + horizontalArrangement = Arrangement.spacedBy(8.dp) 103 + ) { 104 + GalleryImageCell( 105 + image = imagesToDisplay[0], 106 + originalIndex = 0, 107 + onImageClick = { galleryVisible.value = it }) 108 + GalleryImageCell( 109 + image = imagesToDisplay[1], 110 + originalIndex = 1, 111 + onImageClick = { galleryVisible.value = it }) 112 + } 113 + Row( 114 + modifier = Modifier 115 + .fillMaxWidth() 116 + .height(IntrinsicSize.Min), // Apply IntrinsicSize.Min to the Row 117 + horizontalArrangement = Arrangement.spacedBy(8.dp) 118 + ) { 119 + GalleryImageCell( 120 + image = imagesToDisplay[2], 121 + originalIndex = 2, 122 + onImageClick = { galleryVisible.value = it }) 123 + Spacer( 124 + Modifier 125 + .weight(1f) 126 + .fillMaxHeight() // Spacer fills the height of the intrinsically sized Row 127 + ) 128 + } 129 + } 130 + } 131 + 132 + else -> { // Handles 4 images 133 + Column( 134 + modifier = modifier.fillMaxWidth(), 135 + verticalArrangement = Arrangement.spacedBy(8.dp) 136 + ) { 137 + Row( 138 + modifier = Modifier.fillMaxWidth(), 139 + horizontalArrangement = Arrangement.spacedBy(8.dp) 140 + ) { 141 + GalleryImageCell( 142 + image = imagesToDisplay[0], 143 + originalIndex = 0, 144 + onImageClick = { galleryVisible.value = it }) 145 + GalleryImageCell( 146 + image = imagesToDisplay[1], 147 + originalIndex = 1, 148 + onImageClick = { galleryVisible.value = it }) 149 + } 150 + Row( 151 + modifier = Modifier.fillMaxWidth(), 152 + horizontalArrangement = Arrangement.spacedBy(8.dp) 153 + ) { 154 + GalleryImageCell( 155 + image = imagesToDisplay[2], 156 + originalIndex = 2, 157 + onImageClick = { galleryVisible.value = it }) 158 + GalleryImageCell( 159 + image = imagesToDisplay[3], 160 + originalIndex = 3, 161 + onImageClick = { galleryVisible.value = it }) 162 + } 163 + } 64 164 } 65 165 } 66 - } 166 + } 167 + 168 + @Composable 169 + private fun RowScope.GalleryImageCell( 170 + image: Image, 171 + originalIndex: Int, // Index in the original `images` list 172 + onImageClick: (Int) -> Unit 173 + ) { 174 + AsyncImage( 175 + model = ImageRequest.Builder(LocalContext.current) 176 + .data(image.url) 177 + .crossfade(true) 178 + .build(), 179 + contentDescription = image.alt, 180 + contentScale = ContentScale.Crop, 181 + modifier = Modifier 182 + .weight(1f) 183 + .aspectRatio(1f) // Changed from fillMaxSize() to make it square 184 + .clip(RoundedCornerShape(12.dp)) 185 + .clickable { onImageClick(originalIndex) } 186 + ) 187 + } 188 + 189 + // Placeholder for GalleryViewer - ensure it's defined elsewhere 190 + /* 191 + @Composable 192 + fun GalleryViewer(imageUrls: List<Image>, initialPage: Int, onDismiss: () -> Unit) { 193 + // ... your GalleryViewer implementation ... 194 + } 195 + */
+20 -12
app/src/main/java/industries/geesawra/jerryno/SkeetRowView.kt
··· 128 128 return 129 129 } 130 130 131 - Card( 132 - modifier = Modifier 133 - .heightIn(max = 250.dp) 134 - .fillMaxWidth() 135 - .padding(8.dp) 136 - ) { 137 - when (embed) { 138 - is PostViewEmbedUnion.ImagesView -> { 139 - val img = embed.value.images 131 + 132 + when (embed) { 133 + is PostViewEmbedUnion.ImagesView -> { 134 + val img = embed.value.images 140 135 136 + Card( 137 + modifier = Modifier 138 + .fillMaxWidth() 139 + .padding(8.dp) 140 + ) { 141 141 PostImageGallery( 142 142 modifier = Modifier 143 143 .fillMaxSize() ··· 147 147 }, 148 148 ) 149 149 } 150 + } 150 151 151 - is PostViewEmbedUnion.VideoView -> { 152 + is PostViewEmbedUnion.VideoView -> { 153 + Card( 154 + modifier = Modifier 155 + .heightIn(max = 500.dp) 156 + .fillMaxWidth() 157 + .padding(8.dp) 158 + ) { 152 159 VideoPlayer( 153 160 mediaItems = listOf( 154 161 VideoPlayerMediaItem.NetworkMediaItem( ··· 182 189 .padding(8.dp), 183 190 ) 184 191 } 192 + } 185 193 186 - else -> {} 187 - } 194 + else -> {} 188 195 } 196 + 189 197 } 190 198 191 199 @Composable
+32 -23
app/src/main/java/industries/geesawra/jerryno/TimelineView.kt
··· 8 8 import androidx.compose.foundation.layout.Row 9 9 import androidx.compose.foundation.layout.Spacer 10 10 import androidx.compose.foundation.layout.WindowInsets 11 - import androidx.compose.foundation.layout.displayCutout 12 11 import androidx.compose.foundation.layout.fillMaxSize 13 12 import androidx.compose.foundation.layout.padding 14 13 import androidx.compose.foundation.layout.size 14 + import androidx.compose.foundation.layout.statusBars 15 15 import androidx.compose.foundation.layout.windowInsetsPadding 16 16 import androidx.compose.foundation.lazy.rememberLazyListState 17 + import androidx.compose.foundation.rememberScrollState 17 18 import androidx.compose.foundation.shape.CircleShape 18 19 import androidx.compose.material.icons.Icons 19 20 import androidx.compose.material.icons.filled.Create 20 21 import androidx.compose.material.icons.filled.Home 21 22 import androidx.compose.material.icons.filled.Notifications 22 23 import androidx.compose.material.icons.filled.Tag 24 + import androidx.compose.material3.BottomSheetDefaults 23 25 import androidx.compose.material3.BottomSheetScaffold 24 26 import androidx.compose.material3.DrawerValue 25 27 import androidx.compose.material3.ExperimentalMaterial3Api ··· 82 84 coroutineScope: CoroutineScope, 83 85 onLoginError: () -> Unit, 84 86 ) { 87 + val scrollState = rememberScrollState() 85 88 val scaffoldState = rememberBottomSheetScaffoldState( 86 89 bottomSheetState = rememberModalBottomSheetState( 87 - skipPartiallyExpanded = true // Keep true if you only want fully expanded or hidden 88 - ), 90 + skipPartiallyExpanded = true, 91 + ) 89 92 ) 90 93 91 - val context = LocalContext.current 92 - 93 - 94 94 BottomSheetScaffold( 95 - modifier = Modifier.windowInsetsPadding(WindowInsets.displayCutout), 95 + modifier = Modifier 96 + .windowInsetsPadding(WindowInsets.statusBars), 96 97 scaffoldState = scaffoldState, 97 98 sheetPeekHeight = 0.dp, 99 + sheetDragHandle = { 100 + BottomSheetDefaults.DragHandle( 101 + ) 102 + }, 98 103 sheetContent = { 99 - ComposeView(context, coroutineScope, timelineViewModel, scaffoldState) 104 + ComposeView( 105 + LocalContext.current, 106 + coroutineScope, 107 + timelineViewModel, 108 + scaffoldState, 109 + scrollState 110 + ) 100 111 }, 101 112 content = { paddingValues -> 102 113 InnerTimelineView( ··· 159 170 onRefresh = { 160 171 isRefreshing.value = true 161 172 timelineViewModel.reset() 162 - timelineViewModel.fetchTimeline { 163 - isRefreshing.value = false 164 - } 173 + timelineViewModel.fetchTimeline { isRefreshing.value = false } 165 174 }, 166 175 ) { 167 176 ModalNavigationDrawer( ··· 171 180 FeedsDrawer( 172 181 { uri: String, displayName: String, avatar: String? -> 173 182 isRefreshing.value = true 174 - timelineViewModel.selectFeed(uri, displayName, avatar) { 175 - isRefreshing.value = false 176 - } 183 + timelineViewModel.selectFeed( 184 + uri, 185 + displayName, 186 + avatar 187 + ) { isRefreshing.value = false } 177 188 coroutineScope.launch { 178 189 drawerState.close() 179 190 } ··· 263 274 viewModel = timelineViewModel, 264 275 state = listState, 265 276 modifier = Modifier.padding(values) 266 - ) { 267 - isRefreshing.value = false 268 - } 277 + ) { isRefreshing.value = false } 269 278 } 270 279 } 271 280 } ··· 300 309 } 301 310 ) 302 311 303 - timelineViewModel.uiState.feeds.forEach { 312 + timelineViewModel.uiState.feeds.forEach { feed -> 304 313 NavigationDrawerItem( 305 314 label = { 306 315 Row( 307 316 horizontalArrangement = Arrangement.spacedBy(8.dp), 308 317 verticalAlignment = Alignment.CenterVertically 309 318 ) { 310 - if (it.avatar != null) { 319 + if (feed.avatar != null) { 311 320 AsyncImage( 312 - model = it.avatar?.uri, 321 + model = feed.avatar?.uri, 313 322 modifier = Modifier 314 323 .size(20.dp) 315 324 .clip(CircleShape), ··· 319 328 Spacer(modifier = Modifier.size(20.dp)) 320 329 } 321 330 322 - Text(text = it.displayName) 331 + Text(text = feed.displayName) 323 332 } 324 333 }, 325 - selected = timelineViewModel.uiState.selectedFeed == it.uri.atUri, 334 + selected = timelineViewModel.uiState.selectedFeed == feed.uri.atUri, 326 335 modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), 327 336 onClick = { 328 - selectFeed(it.uri.atUri, it.displayName, it.avatar?.uri) 337 + selectFeed(feed.uri.atUri, feed.displayName, feed.avatar?.uri) 329 338 } 330 339 ) 331 340 }