feat: basic implementation of voice channel overlay
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
aab658eccc
commit
14522fd6f3
|
|
@ -13,18 +13,33 @@ import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.EaseInOutExpo
|
import androidx.compose.animation.core.EaseInOutExpo
|
||||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -34,13 +49,21 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -394,6 +417,8 @@ val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOu
|
||||||
val NavTweenInt: FiniteAnimationSpec<IntOffset> = tween(350, easing = EaseInOutExpo)
|
val NavTweenInt: FiniteAnimationSpec<IntOffset> = tween(350, easing = EaseInOutExpo)
|
||||||
val NavTweenFloat: FiniteAnimationSpec<Float> = tween(350, easing = EaseInOutExpo)
|
val NavTweenFloat: FiniteAnimationSpec<Float> = tween(350, easing = EaseInOutExpo)
|
||||||
|
|
||||||
|
// This composable handles the main compose entrypoint of the app, provides the main navigation
|
||||||
|
// graph, and handles the animation and layout for the voice chat UI.
|
||||||
@Composable
|
@Composable
|
||||||
fun AppEntrypoint(
|
fun AppEntrypoint(
|
||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
|
|
@ -408,15 +433,42 @@ fun AppEntrypoint(
|
||||||
onRetryConnection: () -> Unit,
|
onRetryConnection: () -> Unit,
|
||||||
onUpdateNextDestination: (String) -> Unit = {}
|
onUpdateNextDestination: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
var showVoiceUI by remember { mutableStateOf(false) }
|
||||||
|
val chatUIScale by animateFloatAsState(
|
||||||
|
if (showVoiceUI) 0.8f else 1.0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chatUIOpacity by animateFloatAsState(
|
||||||
|
if (showVoiceUI) 0.8f else 1.0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BackHandler(showVoiceUI) {
|
||||||
|
showVoiceUI = false
|
||||||
|
}
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
RevoltTheme(
|
RevoltTheme(
|
||||||
requestedTheme = LoadedSettings.theme,
|
requestedTheme = LoadedSettings.theme,
|
||||||
colourOverrides = SyncedSettings.android.colourOverrides
|
colourOverrides = SyncedSettings.android.colourOverrides
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize()
|
||||||
|
.scale(chatUIScale)
|
||||||
|
.alpha(chatUIOpacity),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
if (isHealthAlertActive) {
|
if (isHealthAlertActive) {
|
||||||
|
|
@ -551,6 +603,7 @@ fun AppEntrypoint(
|
||||||
ChatRouterScreen(
|
ChatRouterScreen(
|
||||||
navController,
|
navController,
|
||||||
windowSizeClass,
|
windowSizeClass,
|
||||||
|
disableBackHandler = showVoiceUI,
|
||||||
onNullifiedUser = {
|
onNullifiedUser = {
|
||||||
onRetryConnection()
|
onRetryConnection()
|
||||||
navController.popBackStack(
|
navController.popBackStack(
|
||||||
|
|
@ -558,7 +611,10 @@ fun AppEntrypoint(
|
||||||
inclusive = true
|
inclusive = true
|
||||||
)
|
)
|
||||||
navController.navigate("default")
|
navController.navigate("default")
|
||||||
}
|
},
|
||||||
|
onEnterVoiceUI = {
|
||||||
|
showVoiceUI = true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -655,5 +711,55 @@ fun AppEntrypoint(
|
||||||
composable("labs") { LabsRootScreen(navController) }
|
composable("labs") { LabsRootScreen(navController) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showVoiceUI) { // if tapped outside the voice UI, close it
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
showVoiceUI = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showVoiceUI,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { it -> it },
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { it -> it },
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = EasingTokens.EmphasizedDecelerate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// We need a box as applying the padding elsewhere leads to either
|
||||||
|
// janky animation or layout
|
||||||
|
Box(Modifier.safeDrawingPadding()) {
|
||||||
|
Card(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.widthIn(max = 600.dp)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
showVoiceUI = false
|
||||||
|
}) {
|
||||||
|
Text("Close voice UI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,9 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
fun ChatRouterScreen(
|
fun ChatRouterScreen(
|
||||||
topNav: NavController,
|
topNav: NavController,
|
||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
|
disableBackHandler: Boolean,
|
||||||
onNullifiedUser: () -> Unit,
|
onNullifiedUser: () -> Unit,
|
||||||
|
onEnterVoiceUI: () -> Unit,
|
||||||
viewModel: ChatRouterViewModel = hiltViewModel()
|
viewModel: ChatRouterViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
|
@ -848,9 +850,11 @@ fun ChatRouterScreen(
|
||||||
dest = viewModel.currentDestination,
|
dest = viewModel.currentDestination,
|
||||||
topNav = topNav,
|
topNav = topNav,
|
||||||
useDrawer = false,
|
useDrawer = false,
|
||||||
|
disableBackHandler = disableBackHandler,
|
||||||
toggleDrawer = {
|
toggleDrawer = {
|
||||||
toggleDrawerLambda()
|
toggleDrawerLambda()
|
||||||
}
|
},
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -891,6 +895,7 @@ fun ChatRouterScreen(
|
||||||
dest = viewModel.currentDestination,
|
dest = viewModel.currentDestination,
|
||||||
topNav = topNav,
|
topNav = topNav,
|
||||||
useDrawer = true,
|
useDrawer = true,
|
||||||
|
disableBackHandler = disableBackHandler,
|
||||||
toggleDrawer = {
|
toggleDrawer = {
|
||||||
toggleDrawerLambda()
|
toggleDrawerLambda()
|
||||||
},
|
},
|
||||||
|
|
@ -898,7 +903,8 @@ fun ChatRouterScreen(
|
||||||
drawerGestureEnabled = useSidebarGesture,
|
drawerGestureEnabled = useSidebarGesture,
|
||||||
setDrawerGestureEnabled = {
|
setDrawerGestureEnabled = {
|
||||||
useSidebarGesture = it
|
useSidebarGesture = it
|
||||||
}
|
},
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -942,11 +948,13 @@ fun ChannelNavigator(
|
||||||
toggleDrawer: () -> Unit,
|
toggleDrawer: () -> Unit,
|
||||||
drawerState: DrawerState? = null,
|
drawerState: DrawerState? = null,
|
||||||
drawerGestureEnabled: Boolean = true,
|
drawerGestureEnabled: Boolean = true,
|
||||||
|
disableBackHandler: Boolean = false,
|
||||||
|
onEnterVoiceUI: () -> Unit = {},
|
||||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
BackHandler(enabled = useDrawer) {
|
BackHandler(useDrawer && !disableBackHandler) {
|
||||||
toggleDrawer()
|
toggleDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,6 +992,7 @@ fun ChannelNavigator(
|
||||||
drawerGestureEnabled = drawerGestureEnabled,
|
drawerGestureEnabled = drawerGestureEnabled,
|
||||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||||
drawerIsOpen = drawerState?.isOpen == true,
|
drawerIsOpen = drawerState?.isOpen == true,
|
||||||
|
onEnterVoiceUI = onEnterVoiceUI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DrawerState
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -95,7 +94,6 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
|
@ -200,6 +198,7 @@ fun ChannelScreen(
|
||||||
drawerIsOpen: Boolean = false,
|
drawerIsOpen: Boolean = false,
|
||||||
backButtonAction: (() -> Unit)? = null,
|
backButtonAction: (() -> Unit)? = null,
|
||||||
useChatUI: Boolean = false,
|
useChatUI: Boolean = false,
|
||||||
|
onEnterVoiceUI: () -> Unit = {},
|
||||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// <editor-fold desc="State and effects">
|
// <editor-fold desc="State and effects">
|
||||||
|
|
@ -848,12 +847,14 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.showPhysicalKeyboardSpark) {
|
Column(
|
||||||
Card(
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
|
if (viewModel.showPhysicalKeyboardSpark) {
|
||||||
|
Card {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
|
|
@ -917,6 +918,19 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.channel?.channelType == ChannelType.VoiceChannel) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onEnterVoiceUI()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Join Voice Channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue