feat: initial tablet support

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-08-02 23:10:41 +02:00
parent 6a7cbc335d
commit 5dd89fa070
5 changed files with 365 additions and 239 deletions

View File

@ -126,6 +126,7 @@ dependencies {
implementation "androidx.compose.ui:ui-util" implementation "androidx.compose.ui:ui-util"
implementation 'androidx.compose.material:material' implementation 'androidx.compose.material:material'
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material3:material3-window-size-class'
implementation "androidx.compose.ui:ui-tooling-preview" implementation "androidx.compose.ui:ui-tooling-preview"
implementation "androidx.compose.runtime:runtime-livedata" implementation "androidx.compose.runtime:runtime-livedata"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

View File

@ -10,6 +10,9 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -45,6 +48,7 @@ import io.sentry.android.core.SentryAndroid
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -56,7 +60,8 @@ class MainActivity : FragmentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
AppEntrypoint() val windowSizeClass = calculateWindowSizeClass(this)
AppEntrypoint(windowSizeClass)
} }
} }
} }
@ -67,7 +72,7 @@ val RevoltTweenDp: FiniteAnimationSpec<Dp> = tween(400, easing = EaseInOutExpo)
val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOutExpo) val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOutExpo)
@Composable @Composable
fun AppEntrypoint() { fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
val navController = rememberNavController() val navController = rememberNavController()
RevoltTheme( RevoltTheme(
@ -126,7 +131,7 @@ fun AppEntrypoint() {
} }
composable("register/onboarding") { OnboardingScreen(navController) } composable("register/onboarding") { OnboardingScreen(navController) }
composable("chat") { ChatRouterScreen(navController) } composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
composable("settings") { SettingsScreen(navController) } composable("settings") { SettingsScreen(navController) }
composable("settings/appearance") { AppearanceSettingsScreen(navController) } composable("settings/appearance") { AppearanceSettingsScreen(navController) }

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -31,6 +32,7 @@ fun ChannelHeader(
channel: Channel, channel: Channel,
onChannelClick: (String) -> Unit, onChannelClick: (String) -> Unit,
onToggleDrawer: () -> Unit, onToggleDrawer: () -> Unit,
useDrawer: Boolean,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -41,6 +43,7 @@ fun ChannelHeader(
.padding(vertical = 4.dp, horizontal = 4.dp), .padding(vertical = 4.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
if (useDrawer) {
IconButton(onClick = { IconButton(onClick = {
onToggleDrawer() onToggleDrawer()
}) { }) {
@ -51,6 +54,14 @@ fun ChannelHeader(
} }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
} else {
// Compensate for the IconButton not increasing our height
Spacer(
modifier = Modifier
.height(48.dp)
.width(12.dp)
)
}
channel.channelType?.let { channel.channelType?.let {
ChannelIcon( ChannelIcon(

View File

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -19,6 +20,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DismissibleDrawerSheet import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -27,6 +29,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -46,6 +50,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog import androidx.navigation.compose.dialog
@ -181,7 +186,11 @@ class ChatRouterViewModel @Inject constructor(
ExperimentalMaterial3Api::class ExperimentalMaterial3Api::class
) )
@Composable @Composable
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) { fun ChatRouterScreen(
topNav: NavController,
windowSizeClass: WindowSizeClass,
viewModel: ChatRouterViewModel = hiltViewModel()
) {
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@ -206,6 +215,8 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
var userContextSheetTarget by remember { mutableStateOf("") } var userContextSheetTarget by remember { mutableStateOf("") }
var userContextSheetServer by remember { mutableStateOf<String?>(null) } var userContextSheetServer by remember { mutableStateOf<String?>(null) }
var useTabletAwareUI by remember { mutableStateOf(false) }
val drawerBackHandler = remember { val drawerBackHandler = remember {
{ {
scope.launch { scope.launch {
@ -270,6 +281,14 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
} }
} }
LaunchedEffect(windowSizeClass) {
snapshotFlow { windowSizeClass }
.distinctUntilChanged()
.collect { sizeClass ->
useTabletAwareUI = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded
}
}
if (showSidebarSpark.value) { if (showSidebarSpark.value) {
AlertDialog( AlertDialog(
onDismissRequest = {}, onDismissRequest = {},
@ -408,13 +427,99 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
}) })
} }
if (useTabletAwareUI) {
Row {
Sidebar(
viewModel = viewModel,
navController = navController,
onShowStatusSheet = {
showStatusSheet = true
},
onShowServerContextSheet = {
serverContextSheetTarget = it
showServerContextSheet = true
},
onShowAddServerSheet = {
showAddServerSheet = true
},
useDrawer = false,
)
ChannelNavigator(
navController = navController,
topNav = topNav,
useDrawer = false,
drawerBackHandler = {
drawerBackHandler()
},
onShowUserContextSheet = { target, server ->
userContextSheetTarget = target
userContextSheetServer = server
showUserContextSheet = true
},
)
}
} else {
DismissibleNavigationDrawer( DismissibleNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
DismissibleDrawerSheet( DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent, drawerContainerColor = Color.Transparent,
) { ) {
Column(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Sidebar(
viewModel = viewModel,
navController = navController,
onShowStatusSheet = {
showStatusSheet = true
},
onShowServerContextSheet = {
serverContextSheetTarget = it
showServerContextSheet = true
},
onShowAddServerSheet = {
showAddServerSheet = true
},
drawerState = drawerState,
useDrawer = true,
)
}
}
},
content = {
Row(Modifier.fillMaxSize()) {
ChannelNavigator(
navController = navController,
topNav = topNav,
useDrawer = true,
drawerBackHandler = {
drawerBackHandler()
},
drawerState = drawerState,
onShowUserContextSheet = { target, server ->
userContextSheetTarget = target
userContextSheetServer = server
showUserContextSheet = true
},
)
}
})
}
}
}
@Composable
fun RowScope.Sidebar(
viewModel: ChatRouterViewModel,
navController: NavHostController,
drawerState: DrawerState? = null,
onShowStatusSheet: () -> Unit,
onShowServerContextSheet: (String) -> Unit,
onShowAddServerSheet: () -> Unit,
useDrawer: Boolean = false,
) {
val scope = rememberCoroutineScope()
Column(Modifier.then(if (useDrawer) Modifier.fillMaxWidth() else Modifier.weight(0.3f))) {
Row { Row {
Column( Column(
modifier = Modifier modifier = Modifier
@ -440,9 +545,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
onClick = { onClick = {
viewModel.navigateToServer("home", navController) viewModel.navigateToServer("home", navController)
}, },
onLongClick = { onLongClick = onShowStatusSheet,
showStatusSheet = true
},
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.size(48.dp) .size(48.dp)
@ -538,8 +641,9 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
server.id server.id
), ),
onLongClick = { onLongClick = {
serverContextSheetTarget = server.id /*serverContextSheetTarget = server.id
showServerContextSheet = true showServerContextSheet = true*/
onShowServerContextSheet(server.id)
}, },
) { ) {
viewModel.navigateToServer( viewModel.navigateToServer(
@ -550,9 +654,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
} }
DrawerServerlikeIcon( DrawerServerlikeIcon(
onClick = { onClick = onShowAddServerSheet,
showAddServerSheet = true
}
) { ) {
Icon( Icon(
Icons.Default.Add, Icons.Default.Add,
@ -572,34 +674,43 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
currentChannel = viewModel.currentChannel, currentChannel = viewModel.currentChannel,
onChannelClick = { channelId -> onChannelClick = { channelId ->
viewModel.navigateToChannel(channelId, navController) viewModel.navigateToChannel(channelId, navController)
scope.launch { drawerState.close() } scope.launch { drawerState?.close() }
}, },
onSpecialClick = { destination -> onSpecialClick = { destination ->
viewModel.navigateToSpecial(destination, navController) viewModel.navigateToSpecial(destination, navController)
scope.launch { drawerState.close() } scope.launch { drawerState?.close() }
}, },
onServerSheetOpenFor = { target -> onServerSheetOpenFor = { target ->
serverContextSheetTarget = target onShowServerContextSheet(target)
showServerContextSheet = true
}, },
) )
} }
} }
} }
} }
},
content = { @Composable
Column(Modifier.fillMaxSize()) { fun RowScope.ChannelNavigator(
navController: NavHostController,
topNav: NavController,
useDrawer: Boolean,
drawerBackHandler: () -> Unit,
drawerState: DrawerState? = null,
onShowUserContextSheet: (String, String?) -> Unit,
) {
val scope = rememberCoroutineScope()
Column(Modifier.then(if (useDrawer) Modifier.fillMaxSize() else Modifier.weight(0.7f))) {
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { composable("home") {
BackHandler { BackHandler(enabled = useDrawer) {
drawerBackHandler() drawerBackHandler()
} }
HomeScreen(navController = topNav) HomeScreen(navController = topNav)
} }
composable("channel/{channelId}") { backStackEntry -> composable("channel/{channelId}") { backStackEntry ->
BackHandler { BackHandler(enabled = useDrawer) {
drawerBackHandler() drawerBackHandler()
} }
@ -610,22 +721,20 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
channelId = channelId, channelId = channelId,
onToggleDrawer = { onToggleDrawer = {
scope.launch { scope.launch {
if (drawerState.isOpen) drawerState.close() if (drawerState?.isOpen == true) drawerState.close()
else drawerState.open() else drawerState?.open()
} }
}, },
onUserSheetOpenFor = { target, server -> onUserSheetOpenFor = { target, server ->
userContextSheetTarget = target onShowUserContextSheet(target, server)
userContextSheetServer = server
showUserContextSheet = true
}, },
useDrawer = useDrawer
) )
} }
} }
composable("no_current_channel") { composable("no_current_channel") {
BackHandler { BackHandler(enabled = useDrawer) {
drawerBackHandler() drawerBackHandler()
} }
@ -643,6 +752,4 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
} }
} }
} }
})
}
} }

View File

@ -101,6 +101,7 @@ fun ChannelScreen(
channelId: String, channelId: String,
onToggleDrawer: () -> Unit, onToggleDrawer: () -> Unit,
onUserSheetOpenFor: (String, String?) -> Unit, onUserSheetOpenFor: (String, String?) -> Unit,
useDrawer: Boolean,
viewModel: ChannelScreenViewModel = viewModel() viewModel: ChannelScreenViewModel = viewModel()
) { ) {
val channel = viewModel.activeChannel val channel = viewModel.activeChannel
@ -238,7 +239,8 @@ fun ChannelScreen(
onChannelClick = { onChannelClick = {
channelInfoSheetShown = true channelInfoSheetShown = true
}, },
onToggleDrawer = onToggleDrawer onToggleDrawer = onToggleDrawer,
useDrawer = useDrawer,
) )
val isScrolledToBottom = remember(lazyListState) { val isScrolledToBottom = remember(lazyListState) {