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.

*: modularize Timeline in a view, make so navbar destination holds everything

No need to manually hide/unhide bars.

Also use scaffolded bottom bar.

geesawra c266b413 f92a672f

+222 -191
+2 -2
app/src/main/java/industries/geesawra/jerryno/ComposeView.kt
··· 94 94 Button( 95 95 enabled = text.isNotBlank() && text.length <= maxChars, 96 96 onClick = { 97 - timelineViewModel.post(content = text, then = { 97 + timelineViewModel.post(text) { 98 98 modalSheetState.hide() 99 - }) 99 + } 100 100 }, 101 101 modifier = Modifier.padding(4.dp) 102 102 ) {
+55 -186
app/src/main/java/industries/geesawra/jerryno/MainActivity.kt
··· 5 5 import androidx.activity.ComponentActivity 6 6 import androidx.activity.compose.setContent 7 7 import androidx.activity.enableEdgeToEdge 8 - import androidx.annotation.StringRes 9 8 import androidx.compose.animation.EnterTransition 10 9 import androidx.compose.animation.ExperimentalSharedTransitionApi 11 10 import androidx.compose.animation.scaleOut 12 - import androidx.compose.foundation.clickable 13 11 import androidx.compose.foundation.layout.Box 14 12 import androidx.compose.foundation.layout.fillMaxSize 15 13 import androidx.compose.foundation.layout.padding 16 - import androidx.compose.foundation.lazy.rememberLazyListState 17 - import androidx.compose.material.icons.Icons 18 - import androidx.compose.material.icons.filled.Create 19 - import androidx.compose.material.icons.filled.Home 20 - import androidx.compose.material.icons.filled.Notifications 21 14 import androidx.compose.material3.ExperimentalMaterial3Api 22 - import androidx.compose.material3.FloatingActionButton 23 - import androidx.compose.material3.Icon 24 15 import androidx.compose.material3.MaterialTheme 25 - import androidx.compose.material3.MediumTopAppBar 26 - import androidx.compose.material3.Scaffold 27 - import androidx.compose.material3.SheetValue 28 16 import androidx.compose.material3.Surface 29 17 import androidx.compose.material3.Text 30 - import androidx.compose.material3.TopAppBarColors 31 - import androidx.compose.material3.TopAppBarDefaults 32 - import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold 33 - import androidx.compose.material3.rememberModalBottomSheetState 34 - import androidx.compose.material3.rememberTopAppBarState 35 18 import androidx.compose.runtime.Composable 36 19 import androidx.compose.runtime.DisposableEffect 37 20 import androidx.compose.runtime.LaunchedEffect 38 - import androidx.compose.runtime.getValue 39 - import androidx.compose.runtime.mutableStateOf 40 21 import androidx.compose.runtime.remember 41 22 import androidx.compose.runtime.rememberCoroutineScope 42 - import androidx.compose.runtime.saveable.rememberSaveable 43 - import androidx.compose.runtime.setValue 44 23 import androidx.compose.ui.Alignment 45 24 import androidx.compose.ui.Modifier 46 - import androidx.compose.ui.focus.FocusRequester 47 25 import androidx.compose.ui.graphics.TransformOrigin 48 - import androidx.compose.ui.graphics.vector.ImageVector 49 - import androidx.compose.ui.input.nestedscroll.nestedScroll 50 26 import androidx.compose.ui.platform.LocalContext 51 - import androidx.compose.ui.res.stringResource 52 27 import androidx.compose.ui.unit.dp 53 28 import androidx.compose.ui.viewinterop.AndroidView 54 29 import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel ··· 69 44 import industries.geesawra.jerryno.datalayer.BlueskyConn 70 45 import industries.geesawra.jerryno.datalayer.TimelineViewModel 71 46 import industries.geesawra.jerryno.ui.theme.JerryNoTheme 72 - import kotlinx.coroutines.launch 73 47 74 48 75 49 @HiltAndroidApp ··· 81 55 Compose 82 56 } 83 57 84 - enum class TabBarDestinations( 85 - @param:StringRes val label: Int, 86 - val icon: ImageVector, 87 - @param:StringRes val contentDescription: Int 88 - ) { 89 - HOME(R.string.timeline, Icons.Filled.Home, R.string.timeline), 90 - NOTIFICATIONS(R.string.notifications, Icons.Filled.Notifications, R.string.notifications) 91 - } 92 - 93 58 @AndroidEntryPoint 94 59 @OptIn(ExperimentalMaterial3Api::class) 95 60 class MainActivity : ComponentActivity() { ··· 118 83 .build() 119 84 } 120 85 121 - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 122 - rememberTopAppBarState() 123 - ) 124 86 val conn = BlueskyConn(LocalContext.current) 125 87 val timelineViewModel = hiltViewModel<TimelineViewModel, TimelineViewModel.Factory>( 126 88 creationCallback = { factory -> 127 89 factory.create(conn) 128 90 } 129 91 ) 130 - var currentDestination by rememberSaveable { mutableStateOf(TabBarDestinations.HOME) } 131 92 val navController = rememberNavController() 132 - val loggingIn = remember { mutableStateOf(true) } 133 - val modalSheetState = rememberModalBottomSheetState( 134 - skipPartiallyExpanded = true, 135 - confirmValueChange = { sv -> 136 - sv != SheetValue.PartiallyExpanded 137 - } 138 - ) 139 - var showBottomSheet by remember { mutableStateOf(false) } 140 - val focusRequester = remember { FocusRequester() } 141 - val listState = rememberLazyListState() 142 - val coroutineScope = rememberCoroutineScope() 143 - 144 93 145 94 Surface( 146 95 modifier = Modifier.fillMaxSize(), 147 96 color = MaterialTheme.colorScheme.background 148 97 ) { 149 - // TODO: maybe move this inside the timeline view? 150 - NavigationSuiteScaffold( 151 - navigationSuiteItems = { 152 - if (loggingIn.value) { 153 - return@NavigationSuiteScaffold 154 - } 155 98 156 - TabBarDestinations.entries.forEach { 157 - item( 158 - icon = { 159 - Icon( 160 - it.icon, 161 - contentDescription = stringResource(it.contentDescription) 162 - ) 163 - }, 164 - label = { Text(stringResource(it.label)) }, 165 - selected = it == currentDestination, 166 - onClick = { currentDestination = it } 167 - ) 168 - } 99 + timelineViewModel.loadSession() 100 + if (!timelineViewModel.uiState.sessionChecked) { 101 + Box( 102 + modifier = Modifier.fillMaxSize(), // Make the Box take the full available space 103 + contentAlignment = Alignment.Center // Align content (LoginView) to the center 104 + ) { 169 105 } 170 - ) { 171 - Scaffold( 172 - containerColor = MaterialTheme.colorScheme.background, 173 - modifier = Modifier 174 - .fillMaxSize() 175 - .nestedScroll(scrollBehavior.nestedScrollConnection), 176 - topBar = { 177 - if (loggingIn.value) { 178 - return@Scaffold 179 - } 180 106 181 - MediumTopAppBar( 182 - colors = TopAppBarColors( 183 - containerColor = MaterialTheme.colorScheme.background, 184 - scrolledContainerColor = MaterialTheme.colorScheme.background, 185 - navigationIconContentColor = MaterialTheme.colorScheme.onBackground, // Ensuring correct contrast 186 - titleContentColor = MaterialTheme.colorScheme.onBackground, 187 - actionIconContentColor = MaterialTheme.colorScheme.onBackground 188 - ), 189 - title = { 190 - Text(text = "Jerry No") 191 - }, 192 - scrollBehavior = scrollBehavior, 193 - modifier = Modifier.clickable { 194 - coroutineScope.launch { 195 - listState.animateScrollToItem(0) 196 - } 197 - } 198 - ) 199 - }, 200 - floatingActionButton = { 201 - if (loggingIn.value) { 202 - return@Scaffold 203 - } 107 + return@Surface 108 + } 109 + 110 + val initialRoute = 111 + if (timelineViewModel.uiState.authenticated) TimelineScreen.Timeline.name else TimelineScreen.Login.name 204 112 205 - FloatingActionButton( 206 - onClick = { 207 - navController.navigate(TimelineScreen.Compose.name) 208 - }, 209 - ) { 210 - Icon(Icons.Filled.Create, "Post") 211 - } 212 - }, 213 - ) { values -> 214 - timelineViewModel.loadSession() 215 - if (!timelineViewModel.uiState.sessionChecked) { 113 + NavHost( 114 + navController = navController, 115 + startDestination = initialRoute, 116 + modifier = Modifier.fillMaxSize(), 117 + popExitTransition = { 118 + scaleOut( 119 + targetScale = 0.9f, 120 + transformOrigin = TransformOrigin(0.5f, 0.5f) 121 + ) 122 + }, 123 + popEnterTransition = { 124 + EnterTransition.None 125 + }, 126 + ) { 127 + composable(route = TimelineScreen.Timeline.name) { 128 + TimelineView( 129 + timelineViewModel = timelineViewModel, 130 + coroutineScope = rememberCoroutineScope() 131 + ) 132 + } 133 + composable(route = TimelineScreen.Compose.name) { 134 + Surface( 135 + modifier = Modifier.fillMaxSize(), 136 + color = MaterialTheme.colorScheme.background // Set compose screen background 137 + ) { 216 138 Box( 217 - modifier = Modifier.fillMaxSize(), // Make the Box take the full available space 218 - contentAlignment = Alignment.Center // Align content (LoginView) to the center 139 + modifier = Modifier.fillMaxSize(), 140 + contentAlignment = Alignment.Center, 219 141 ) { 142 + Text( 143 + "Compose", 144 + modifier = Modifier 145 + .fillMaxSize() 146 + .padding(10.dp) 147 + ) 220 148 } 221 - 222 - return@Scaffold 223 149 } 224 - 225 - val initialRoute = 226 - if (timelineViewModel.uiState.authenticated) TimelineScreen.Timeline.name else TimelineScreen.Login.name 227 - 228 - NavHost( 229 - navController = navController, 230 - startDestination = initialRoute, 231 - modifier = Modifier.padding(values), 232 - popExitTransition = { 233 - scaleOut( 234 - targetScale = 0.9f, 235 - transformOrigin = TransformOrigin(0.5f, 0.5f) 236 - ) 237 - }, 238 - popEnterTransition = { 239 - EnterTransition.None 240 - }, 150 + } 151 + composable(route = TimelineScreen.Login.name) { 152 + Surface( 153 + modifier = Modifier.fillMaxSize(), 154 + color = MaterialTheme.colorScheme.background // Set login screen background 241 155 ) { 242 - composable(route = TimelineScreen.Timeline.name) { 243 - loggingIn.value = false 244 - timelineViewModel.create() 245 - 246 - ShowSkeets( 247 - viewModel = timelineViewModel, 248 - state = listState 249 - ) 250 - 251 - if (showBottomSheet) { 252 - ComposeView( 253 - modalSheetState = modalSheetState, 254 - focusRequester = focusRequester, 255 - timelineViewModel = timelineViewModel, 256 - onDismissRequest = { 257 - showBottomSheet = false 258 - } 259 - ) 260 - } 261 - } 262 - composable(route = TimelineScreen.Compose.name) { 263 - Surface( 264 - modifier = Modifier.fillMaxSize(), 265 - color = MaterialTheme.colorScheme.background // Set compose screen background 266 - ) { 267 - Box( 268 - modifier = Modifier.fillMaxSize(), 269 - contentAlignment = Alignment.Center, 270 - ) { 271 - Text( 272 - "Compose", 273 - modifier = Modifier 274 - .fillMaxSize() 275 - .padding(10.dp) 276 - ) 277 - } 278 - } 279 - } 280 - composable(route = TimelineScreen.Login.name) { 281 - Surface( 282 - modifier = Modifier.fillMaxSize(), 283 - color = MaterialTheme.colorScheme.background // Set login screen background 284 - ) { 285 - Box( 286 - modifier = Modifier.fillMaxSize(), 287 - contentAlignment = Alignment.Center 288 - ) { 289 - LoginView { 290 - navController.navigate(TimelineScreen.Timeline.name) 291 - } 292 - } 156 + Box( 157 + modifier = Modifier.fillMaxSize(), 158 + contentAlignment = Alignment.Center 159 + ) { 160 + LoginView { 161 + navController.navigate(TimelineScreen.Timeline.name) 293 162 } 294 163 } 295 164 }
+3 -3
app/src/main/java/industries/geesawra/jerryno/ShowSkeets.kt
··· 22 22 23 23 @Composable 24 24 fun ShowSkeets( 25 + modifier: Modifier = Modifier, 25 26 viewModel: TimelineViewModel, 26 - state: LazyListState = rememberLazyListState() 27 + state: LazyListState = rememberLazyListState(), 27 28 ) { 28 29 LaunchedEffect(key1 = viewModel.uiState.skeets.isEmpty()) { 29 30 if (viewModel.uiState.skeets.isEmpty()) { ··· 34 35 35 36 LazyColumn( 36 37 state = state, 37 - modifier = Modifier 38 - .fillMaxSize(), 38 + modifier = modifier.fillMaxSize(), 39 39 verticalArrangement = Arrangement.spacedBy(8.dp), 40 40 ) { 41 41
+162
app/src/main/java/industries/geesawra/jerryno/TimelineView.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import androidx.annotation.StringRes 4 + import androidx.compose.foundation.clickable 5 + import androidx.compose.foundation.layout.fillMaxSize 6 + import androidx.compose.foundation.layout.padding 7 + import androidx.compose.foundation.lazy.rememberLazyListState 8 + import androidx.compose.material.icons.Icons 9 + import androidx.compose.material.icons.filled.Create 10 + import androidx.compose.material.icons.filled.Home 11 + import androidx.compose.material.icons.filled.Notifications 12 + import androidx.compose.material3.ExperimentalMaterial3Api 13 + import androidx.compose.material3.FloatingActionButton 14 + import androidx.compose.material3.HorizontalDivider 15 + import androidx.compose.material3.Icon 16 + import androidx.compose.material3.MaterialTheme 17 + import androidx.compose.material3.MediumTopAppBar 18 + import androidx.compose.material3.ModalDrawerSheet 19 + import androidx.compose.material3.ModalNavigationDrawer 20 + import androidx.compose.material3.NavigationBar 21 + import androidx.compose.material3.NavigationBarItem 22 + import androidx.compose.material3.NavigationDrawerItem 23 + import androidx.compose.material3.Scaffold 24 + import androidx.compose.material3.SheetValue 25 + import androidx.compose.material3.Text 26 + import androidx.compose.material3.TopAppBarColors 27 + import androidx.compose.material3.TopAppBarDefaults 28 + import androidx.compose.material3.rememberModalBottomSheetState 29 + import androidx.compose.material3.rememberTopAppBarState 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.saveable.rememberSaveable 35 + import androidx.compose.runtime.setValue 36 + import androidx.compose.ui.Modifier 37 + import androidx.compose.ui.focus.FocusRequester 38 + import androidx.compose.ui.graphics.vector.ImageVector 39 + import androidx.compose.ui.input.nestedscroll.nestedScroll 40 + import androidx.compose.ui.res.stringResource 41 + import androidx.compose.ui.unit.dp 42 + import industries.geesawra.jerryno.datalayer.TimelineViewModel 43 + import kotlinx.coroutines.CoroutineScope 44 + import kotlinx.coroutines.launch 45 + 46 + enum class TabBarDestinations( 47 + @param:StringRes val label: Int, 48 + val icon: ImageVector, 49 + @param:StringRes val contentDescription: Int 50 + ) { 51 + HOME(R.string.timeline, Icons.Filled.Home, R.string.timeline), 52 + NOTIFICATIONS(R.string.notifications, Icons.Filled.Notifications, R.string.notifications) 53 + } 54 + 55 + 56 + @OptIn(ExperimentalMaterial3Api::class) 57 + @Composable 58 + fun TimelineView( 59 + timelineViewModel: TimelineViewModel, 60 + coroutineScope: CoroutineScope 61 + ) { 62 + var showBottomSheet by remember { mutableStateOf(false) } 63 + var currentDestination by rememberSaveable { mutableStateOf(TabBarDestinations.HOME) } 64 + val modalSheetState = rememberModalBottomSheetState( 65 + skipPartiallyExpanded = true, 66 + confirmValueChange = { sv -> 67 + sv != SheetValue.PartiallyExpanded 68 + } 69 + ) 70 + val focusRequester = remember { FocusRequester() } 71 + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 72 + rememberTopAppBarState() 73 + ) 74 + val listState = rememberLazyListState() 75 + 76 + 77 + ModalNavigationDrawer( 78 + drawerContent = { 79 + ModalDrawerSheet { 80 + Text("Drawer title", modifier = Modifier.padding(16.dp)) 81 + HorizontalDivider() 82 + NavigationDrawerItem( 83 + label = { Text(text = "Drawer Item") }, 84 + selected = false, 85 + onClick = { /*TODO*/ } 86 + ) 87 + } 88 + } 89 + ) { 90 + Scaffold( 91 + containerColor = MaterialTheme.colorScheme.background, 92 + modifier = Modifier 93 + .fillMaxSize() 94 + .nestedScroll(scrollBehavior.nestedScrollConnection), 95 + topBar = { 96 + MediumTopAppBar( 97 + colors = TopAppBarColors( 98 + containerColor = MaterialTheme.colorScheme.background, 99 + scrolledContainerColor = MaterialTheme.colorScheme.background, 100 + navigationIconContentColor = MaterialTheme.colorScheme.onBackground, // Ensuring correct contrast 101 + titleContentColor = MaterialTheme.colorScheme.onBackground, 102 + actionIconContentColor = MaterialTheme.colorScheme.onBackground 103 + ), 104 + title = { 105 + Text(text = "Jerry No") 106 + }, 107 + scrollBehavior = scrollBehavior, 108 + modifier = Modifier.clickable { 109 + coroutineScope.launch { 110 + listState.animateScrollToItem(0) 111 + } 112 + } 113 + ) 114 + }, 115 + floatingActionButton = { 116 + FloatingActionButton( 117 + onClick = { 118 + showBottomSheet = true 119 + }, 120 + ) { 121 + Icon(Icons.Filled.Create, "Post") 122 + } 123 + }, 124 + bottomBar = { 125 + NavigationBar { 126 + TabBarDestinations.entries.forEach { 127 + NavigationBarItem( 128 + icon = { 129 + Icon( 130 + it.icon, 131 + contentDescription = stringResource(it.contentDescription) 132 + ) 133 + }, 134 + label = { Text(stringResource(it.label)) }, 135 + selected = it == currentDestination, 136 + onClick = { currentDestination = it } 137 + ) 138 + } 139 + } 140 + } 141 + ) { values -> 142 + timelineViewModel.create() 143 + 144 + ShowSkeets( 145 + viewModel = timelineViewModel, 146 + state = listState, 147 + modifier = Modifier.padding(values) 148 + ) 149 + 150 + if (showBottomSheet) { 151 + ComposeView( 152 + modalSheetState = modalSheetState, 153 + focusRequester = focusRequester, 154 + timelineViewModel = timelineViewModel, 155 + onDismissRequest = { 156 + showBottomSheet = false 157 + } 158 + ) 159 + } 160 + } 161 + } 162 + }