Refactor: Improve navigation and UI for channel and login screens
This commit introduces several improvements to the navigation and user interface, primarily focused on the channel screen and login/registration flows. **Channel Screen & Navigation:** - Implement swipe-to-go-back gesture on the ChannelScreen. - Persist the ChannelSideDrawer as a background layer when a channel is open. This allows for a smoother transition when returning to the channel list. - Allow swiping from the left edge of the screen (when not in a channel view) to reopen the last viewed channel. - Ensure the correct channel is highlighted in the ChannelSideDrawer when a channel is active. - The back button on the ChannelScreen now navigates to the previous non-channel screen or the server's channel list, providing a more intuitive backstack behavior. **Login/Registration:** - Disable HTTP request retries for login attempts to prevent unexpected behavior with MFA or incorrect credentials. - Add loading indicators to the Login and RegisterDetails screens to provide visual feedback during network operations. - Display error messages more prominently on the Login and RegisterDetails screens. - Adjust the soft input mode on the LoginScreen to `SOFT_INPUT_ADJUST_PAN` to prevent UI elements from being obscured by the keyboard. - Fix a bug where the default notification type was incorrectly mapped. **Other Changes:** - Update the monochrome notification icon. - Modify the primary color in `colors.xml`. - Minor cleanups and toast message improvements in ChannelScreen camera handling.
This commit is contained in:
parent
5596d56525
commit
da28d50b18
|
|
@ -153,8 +153,17 @@ val PeptideHttp by lazy {
|
|||
install(WebSockets)
|
||||
|
||||
install(HttpRequestRetry) {
|
||||
retryOnServerErrors(maxRetries = 5)
|
||||
retryOnException(maxRetries = 5)
|
||||
retryIf(maxRetries = 5) { _, response ->
|
||||
// Do not retry for explicit no-retry requests (like login)
|
||||
val noRetry = response.call.request.headers["x-no-retry"] == "true"
|
||||
if (noRetry) return@retryIf false
|
||||
// Retry only on server errors (5xx)
|
||||
response.status.value in 500..599
|
||||
}
|
||||
retryOnExceptionIf(maxRetries = 5) { request, cause ->
|
||||
// Allow network retries unless explicitly opted-out
|
||||
request.headers["x-no-retry"] != "true"
|
||||
}
|
||||
|
||||
modifyRequest { request ->
|
||||
request.headers.append("x-retry-count", retryCount.toString())
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ suspend fun negotiateAuthentication(email: String, password: String): EmailPassw
|
|||
|
||||
val response: HttpResponse = PeptideHttp.post("/auth/session/login".api()) {
|
||||
contentType(ContentType.Application.Json)
|
||||
headers.append("x-no-retry", "true")
|
||||
setBody(LoginNegotiation(email, password, sessionName, null))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package chat.peptide.api.settings
|
||||
|
||||
enum class NotificationType(val storageValue: String) {
|
||||
DEFAULT("default"),
|
||||
DEFAULT("all"),
|
||||
MUTED("muted"),
|
||||
ALL("all"),
|
||||
MENTIONS_ONLY("mentions"),
|
||||
|
|
@ -11,7 +11,6 @@ enum class NotificationType(val storageValue: String) {
|
|||
fun fromStorage(value: String?): NotificationType {
|
||||
return when (value) {
|
||||
null -> DEFAULT
|
||||
"default" -> DEFAULT
|
||||
"muted" -> MUTED
|
||||
"all" -> ALL
|
||||
"mentions" -> MENTIONS_ONLY
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ import chat.peptide.sheets.UserInfoSheet
|
|||
fun ChannelSideDrawer(
|
||||
currentServer: String?,
|
||||
currentDestination: ChatRouterDestination,
|
||||
selectedChannelId: String?,
|
||||
onDestinationChanged: (ChatRouterDestination) -> Unit,
|
||||
navigateToServer: (String) -> Unit,
|
||||
onShowServerContextSheet: (String) -> Unit,
|
||||
|
|
@ -121,6 +122,11 @@ fun ChannelSideDrawer(
|
|||
}
|
||||
val channelListState = rememberLazyListState()
|
||||
|
||||
val effectiveDestination = when (currentDestination) {
|
||||
is ChatRouterDestination.Channel -> currentDestination
|
||||
else -> selectedChannelId?.let { ChatRouterDestination.Channel(it) } ?: currentDestination
|
||||
}
|
||||
|
||||
LaunchedEffect(currentDestination) {
|
||||
if ((currentDestination is ChatRouterDestination.ServersChannels) && currentServer != null) {
|
||||
val channelIndex = categorisedChannels?.indexOfFirst {
|
||||
|
|
@ -590,7 +596,7 @@ fun ChannelSideDrawer(
|
|||
} else {
|
||||
if (currentServer == null) {
|
||||
DirectMessagesChannelListRenderer(
|
||||
currentDestination,
|
||||
effectiveDestination,
|
||||
onDestinationChanged,
|
||||
channelListState,
|
||||
onOpenUserInfoSheet = { userId -> userContextSheetTarget = userId }
|
||||
|
|
@ -598,7 +604,7 @@ fun ChannelSideDrawer(
|
|||
} else {
|
||||
ServerChannelListRenderer(
|
||||
categorisedChannels,
|
||||
currentDestination,
|
||||
effectiveDestination,
|
||||
onDestinationChanged,
|
||||
channelListState,
|
||||
onOpenChannelContextSheet = { channelContextSheetTarget = it },
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import android.view.inputmethod.InputMethodManager
|
|||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
|
|
@ -23,6 +25,8 @@ import androidx.compose.foundation.layout.systemBars
|
|||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.DismissibleDrawerSheet
|
||||
import androidx.compose.material3.DismissibleNavigationDrawer
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -50,6 +54,8 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -156,6 +162,8 @@ class ChatRouterViewModel @Inject constructor(
|
|||
@ApplicationContext val context: Context
|
||||
) : ViewModel() {
|
||||
var currentDestination by mutableStateOf<ChatRouterDestination>(ChatRouterDestination.default)
|
||||
var previousDestination by mutableStateOf<ChatRouterDestination?>(null)
|
||||
var lastNonChannelDestination by mutableStateOf<ChatRouterDestination?>(null)
|
||||
var latestChangelogRead by mutableStateOf(true)
|
||||
var latestChangelog by mutableStateOf("")
|
||||
var latestChangelogBody by mutableStateOf("")
|
||||
|
|
@ -180,6 +188,8 @@ class ChatRouterViewModel @Inject constructor(
|
|||
setSaveDestination(ChatRouterDestination.fromString(current ?: ""))
|
||||
}
|
||||
|
||||
setRegisterForNotifications()
|
||||
|
||||
latestChangelogRead = changelogs.hasSeenCurrent()
|
||||
latestChangelog = changelogs.getLatestChangelogCode()
|
||||
latestChangelogBody =
|
||||
|
|
@ -211,7 +221,13 @@ class ChatRouterViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun setSaveDestination(destination: ChatRouterDestination) {
|
||||
currentDestination = destination
|
||||
if (destination != currentDestination) {
|
||||
previousDestination = currentDestination
|
||||
currentDestination = destination
|
||||
if (destination !is ChatRouterDestination.Channel) {
|
||||
lastNonChannelDestination = destination
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setRegisterForNotifications() {
|
||||
|
|
@ -913,169 +929,215 @@ fun ChannelNavigator(
|
|||
isTouchExplorationEnabled: Boolean,
|
||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||
) {
|
||||
when (dest) {
|
||||
is ChatRouterDestination.Channel -> {
|
||||
ChannelScreen(
|
||||
channelId = dest.channelId,
|
||||
messageId = dest.messageId,
|
||||
backToChannelsScreen = {
|
||||
currentServer?.let {
|
||||
viewModel.setSaveDestination(
|
||||
ChatRouterDestination.ServersChannels(
|
||||
serverID = currentServer
|
||||
)
|
||||
)
|
||||
} ?: viewModel.setSaveDestination(
|
||||
ChatRouterDestination.Home
|
||||
)
|
||||
},
|
||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||
)
|
||||
}
|
||||
// Keep track of the last opened channel to allow edge-swipe reopen
|
||||
var lastChannel by remember { mutableStateOf<ChatRouterDestination.Channel?>(null) }
|
||||
if (dest is ChatRouterDestination.Channel) {
|
||||
lastChannel = dest
|
||||
}
|
||||
|
||||
else -> {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_home),
|
||||
contentDescription = "Home",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "you")
|
||||
},
|
||||
selected = when (dest) {
|
||||
is ChatRouterDestination.ServersChannels,
|
||||
is ChatRouterDestination.NoCurrentChannel,
|
||||
ChatRouterDestination.Home,
|
||||
ChatRouterDestination.Discover -> true
|
||||
var containerWidth by remember { mutableStateOf(0) }
|
||||
var isReopenDragging by remember { mutableStateOf(false) }
|
||||
var reopenDragDelta by remember { mutableStateOf(0f) }
|
||||
|
||||
else -> false
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
enabled = true,
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Home)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(dest, lastChannel) {
|
||||
if (dest !is ChatRouterDestination.Channel && lastChannel != null) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { _ ->
|
||||
// Allow reopen swipe to start anywhere on the screen
|
||||
isReopenDragging = true
|
||||
reopenDragDelta = 0f
|
||||
},
|
||||
onDragEnd = {
|
||||
if (isReopenDragging) {
|
||||
// If dragged sufficiently left, reopen the last channel
|
||||
if (reopenDragDelta < -containerWidth / 6f) {
|
||||
lastChannel?.let { viewModel.setSaveDestination(it) }
|
||||
}
|
||||
}
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_friends),
|
||||
contentDescription = "Friends",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "Friends")
|
||||
},
|
||||
selected = dest is ChatRouterDestination.Friends,
|
||||
enabled = true,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Friends)
|
||||
}
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_you),
|
||||
contentDescription = "You",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "you")
|
||||
},
|
||||
selected = dest is ChatRouterDestination.Settings,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
enabled = true,
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Settings)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
when (dest) {
|
||||
is ChatRouterDestination.Settings -> {
|
||||
SettingsScreen(
|
||||
navController = topNav,
|
||||
)
|
||||
isReopenDragging = false
|
||||
reopenDragDelta = 0f
|
||||
},
|
||||
onDragCancel = {
|
||||
isReopenDragging = false
|
||||
reopenDragDelta = 0f
|
||||
}
|
||||
|
||||
is ChatRouterDestination.Friends -> {
|
||||
FriendsScreen(
|
||||
topNav = topNav,
|
||||
onDrawerClicked = toggleDrawer,
|
||||
)
|
||||
}
|
||||
|
||||
is ChatRouterDestination.Home,
|
||||
is ChatRouterDestination.ServersChannels,
|
||||
is ChatRouterDestination.Discover -> {
|
||||
ChannelSideDrawer(
|
||||
onDestinationChanged = viewModel::setSaveDestination,
|
||||
currentDestination = viewModel.currentDestination,
|
||||
currentServer = when (dest) {
|
||||
is ChatRouterDestination.Home -> currentServer
|
||||
is ChatRouterDestination.ServersChannels -> dest.serverID
|
||||
else -> null
|
||||
},
|
||||
navigateToServer = viewModel::navigateToServer,
|
||||
onShowServerContextSheet = onShowServerContextSheet,
|
||||
showSettingsIcon = isTouchExplorationEnabled,
|
||||
onOpenSettings = {
|
||||
topNav.navigate("settings")
|
||||
},
|
||||
onShowAddServerSheet = onShowAddServerSheet
|
||||
)
|
||||
}
|
||||
|
||||
is ChatRouterDestination.Channel -> {}
|
||||
|
||||
is ChatRouterDestination.NoCurrentChannel -> {
|
||||
NoCurrentChannelScreen(onDrawerClicked = toggleDrawer)
|
||||
) { change, dragAmount ->
|
||||
if (isReopenDragging) {
|
||||
reopenDragDelta += dragAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
containerWidth = placeable.width
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
}
|
||||
) {
|
||||
// Background scaffold is always rendered to act as the underlying layer
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_home),
|
||||
contentDescription = "Home",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "you")
|
||||
},
|
||||
selected = when (dest) {
|
||||
is ChatRouterDestination.ServersChannels,
|
||||
is ChatRouterDestination.NoCurrentChannel,
|
||||
ChatRouterDestination.Home,
|
||||
ChatRouterDestination.Discover -> true
|
||||
else -> false
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
enabled = true,
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Home)
|
||||
}
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_friends),
|
||||
contentDescription = "Friends",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "Friends")
|
||||
},
|
||||
selected = dest is ChatRouterDestination.Friends,
|
||||
enabled = true,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Friends)
|
||||
}
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_bottom_navbar_you),
|
||||
contentDescription = "You",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = "you")
|
||||
},
|
||||
selected = dest is ChatRouterDestination.Settings,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = Color.Transparent,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onBackground,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
),
|
||||
enabled = true,
|
||||
onClick = {
|
||||
viewModel.setSaveDestination(ChatRouterDestination.Settings)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
when (dest) {
|
||||
is ChatRouterDestination.Settings -> {
|
||||
SettingsScreen(
|
||||
navController = topNav,
|
||||
)
|
||||
}
|
||||
|
||||
is ChatRouterDestination.Friends -> {
|
||||
FriendsScreen(
|
||||
topNav = topNav,
|
||||
onDrawerClicked = toggleDrawer,
|
||||
)
|
||||
}
|
||||
|
||||
is ChatRouterDestination.Home,
|
||||
is ChatRouterDestination.ServersChannels,
|
||||
is ChatRouterDestination.Discover,
|
||||
is ChatRouterDestination.Channel -> { // When Channel is active, show drawer as background
|
||||
ChannelSideDrawer(
|
||||
onDestinationChanged = viewModel::setSaveDestination,
|
||||
currentDestination = viewModel.currentDestination,
|
||||
currentServer = when (dest) {
|
||||
is ChatRouterDestination.Home -> currentServer
|
||||
is ChatRouterDestination.ServersChannels -> dest.serverID
|
||||
is ChatRouterDestination.Channel -> currentServer
|
||||
else -> null
|
||||
},
|
||||
selectedChannelId = (if (dest is ChatRouterDestination.Channel) dest.channelId else null),
|
||||
navigateToServer = viewModel::navigateToServer,
|
||||
onShowServerContextSheet = onShowServerContextSheet,
|
||||
showSettingsIcon = isTouchExplorationEnabled,
|
||||
onOpenSettings = {
|
||||
topNav.navigate("settings")
|
||||
},
|
||||
onShowAddServerSheet = onShowAddServerSheet
|
||||
)
|
||||
}
|
||||
|
||||
is ChatRouterDestination.NoCurrentChannel -> {
|
||||
NoCurrentChannelScreen(onDrawerClicked = toggleDrawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground overlay: Channel screen when destination is a Channel
|
||||
if (dest is ChatRouterDestination.Channel) {
|
||||
ChannelScreen(
|
||||
channelId = dest.channelId,
|
||||
messageId = dest.messageId,
|
||||
backToChannelsScreen = {
|
||||
// Prefer going back to the current server's channels screen explicitly
|
||||
val target = currentServer?.let { ChatRouterDestination.ServersChannels(serverID = it) }
|
||||
?: viewModel.lastNonChannelDestination
|
||||
?: viewModel.previousDestination
|
||||
?: ChatRouterDestination.Home
|
||||
viewModel.setSaveDestination(target)
|
||||
},
|
||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.camera.view.PreviewView
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
|
|
@ -36,6 +37,7 @@ import androidx.compose.animation.slideOutVertically
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -54,10 +56,12 @@ import androidx.compose.foundation.layout.imeAnimationTarget
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -80,7 +84,6 @@ import androidx.compose.material3.ModalBottomSheet
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -89,6 +92,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -99,6 +103,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -114,8 +120,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
|
@ -170,6 +178,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.datetime.Instant
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.camera.core.Preview as CameraPreview
|
||||
|
||||
sealed class ChannelScreenItem {
|
||||
|
|
@ -383,13 +392,18 @@ fun ChannelScreen(
|
|||
}
|
||||
|
||||
override fun onError(exc: ImageCaptureException) {
|
||||
Toast.makeText(context, "Photo capture failed: ${exc.message}", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Photo capture failed: ${exc.message}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e("ChannelScreen", "Photo capture failed: ", exc)
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChannelScreen", "Error opening camera: ", e)
|
||||
Toast.makeText(context, "Error opening camera: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, "Error opening camera: ${e.message}", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -403,7 +417,10 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
val openCameraCallback = {
|
||||
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
showCameraSheet = true
|
||||
} else {
|
||||
|
|
@ -652,7 +669,53 @@ fun ChannelScreen(
|
|||
}
|
||||
// </editor-fold>
|
||||
// <editor-fold desc="Begin UI composition">
|
||||
// Swipe-back state
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
var componentWidth by remember { mutableIntStateOf(0) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
||||
.pointerInput(Unit) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { _ ->
|
||||
// Allow swipe-back to start anywhere on the screen
|
||||
isDragging = true
|
||||
},
|
||||
onDragEnd = {
|
||||
if (isDragging) {
|
||||
scope.launch {
|
||||
if (offsetX.value > componentWidth * 0.25f) {
|
||||
backToChannelsScreen?.invoke()
|
||||
} else {
|
||||
offsetX.animateTo(0f, animationSpec = tween(durationMillis = 300))
|
||||
}
|
||||
}
|
||||
}
|
||||
isDragging = false
|
||||
},
|
||||
onDragCancel = { isDragging = false }
|
||||
) { change, dragAmount ->
|
||||
if (isDragging) {
|
||||
change.consume()
|
||||
scope.launch {
|
||||
offsetX.snapTo(
|
||||
(offsetX.value + dragAmount).coerceIn(0f, componentWidth.toFloat())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
componentWidth = placeable.width
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
},
|
||||
contentWindowInsets = WindowInsets.zero,
|
||||
topBar = {
|
||||
Column {
|
||||
|
|
@ -674,6 +737,16 @@ fun ChannelScreen(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
backToChannelsScreen?.invoke()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.icn_arrow_back_24dp),
|
||||
contentDescription = "back",
|
||||
)
|
||||
}
|
||||
viewModel.channel?.let {
|
||||
when (it.channelType) {
|
||||
ChannelType.DirectMessage -> {
|
||||
|
|
@ -1139,7 +1212,8 @@ fun ChannelScreen(
|
|||
|
||||
// Full-screen camera preview sheet
|
||||
if (showCameraSheet) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val sheetState =
|
||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = { showCameraSheet = false }
|
||||
|
|
@ -1153,9 +1227,10 @@ fun ChannelScreen(
|
|||
// Bind use cases when view is ready
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
cameraProvider.unbindAll()
|
||||
val preview = CameraPreview.Builder().build().also { pr ->
|
||||
pr.surfaceProvider = pv.surfaceProvider
|
||||
}
|
||||
val preview =
|
||||
CameraPreview.Builder().build().also { pr ->
|
||||
pr.surfaceProvider = pv.surfaceProvider
|
||||
}
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
|
|
@ -1454,7 +1529,7 @@ fun ChannelScreen(
|
|||
ChannelScreenActivePane.None
|
||||
}
|
||||
|
||||
MediaPickerGateway(
|
||||
MediaPickerGateway(
|
||||
onOpenPhotoPicker = {
|
||||
openPhotoPickerCallback()
|
||||
viewModel.activePane =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package chat.peptide.screens.login
|
||||
|
||||
import android.util.Log
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -13,6 +15,8 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.text.input.TextObfuscationMode
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -27,6 +31,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -34,6 +39,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -68,6 +74,8 @@ import javax.inject.Inject
|
|||
class LoginViewModel @Inject constructor(
|
||||
private val kvStorage: KVStorage
|
||||
) : ViewModel() {
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
private var _email by mutableStateOf("")
|
||||
val email: String
|
||||
get() = _email
|
||||
|
|
@ -90,51 +98,59 @@ class LoginViewModel @Inject constructor(
|
|||
|
||||
fun doLogin() {
|
||||
_error = null
|
||||
isLoading = true
|
||||
|
||||
viewModelScope.launch {
|
||||
val response = try {
|
||||
negotiateAuthentication(_email, _password)
|
||||
} catch (e: Exception) {
|
||||
_error = if (e.message?.startsWith("Unexpected JSON token") == true) {
|
||||
PeptideApplication.instance.getString(R.string.service_health_alert_body_default)
|
||||
} else e.message ?: "Unknown error"
|
||||
return@launch
|
||||
}
|
||||
if (response.error != null) {
|
||||
_error = response.error.type
|
||||
} else {
|
||||
try {
|
||||
val response = negotiateAuthentication(_email, _password)
|
||||
|
||||
if (response.error != null) {
|
||||
_error = response.error.type
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.d("Login", "Checking for MFA")
|
||||
if (response.proceedMfa) {
|
||||
Log.d("Login", "MFA required. Navigating to MFA screen")
|
||||
_mfaResponse = response
|
||||
_navigateTo = "mfa"
|
||||
} else {
|
||||
Log.d(
|
||||
"Login",
|
||||
"No MFA required. Login is complete! We should have a session token"
|
||||
)
|
||||
|
||||
try {
|
||||
val token = response.firstUserHints!!.token
|
||||
val id = response.firstUserHints.id
|
||||
|
||||
kvStorage.set("sessionToken", token)
|
||||
kvStorage.set("sessionId", id)
|
||||
|
||||
val onboard = needsOnboarding(token)
|
||||
if (onboard) {
|
||||
_navigateTo = "onboarding"
|
||||
return@launch
|
||||
}
|
||||
|
||||
PeptideAPI.loginAs(token)
|
||||
PeptideAPI.setSessionId(response.firstUserHints.token)
|
||||
|
||||
_navigateTo = "home"
|
||||
} catch (e: Error) {
|
||||
_error = e.message ?: "Unknown error"
|
||||
}
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.d(
|
||||
"Login",
|
||||
"No MFA required. Login is complete! We should have a session token"
|
||||
)
|
||||
|
||||
try {
|
||||
val token = response.firstUserHints!!.token
|
||||
val id = response.firstUserHints.id
|
||||
|
||||
kvStorage.set("sessionToken", token)
|
||||
kvStorage.set("sessionId", id)
|
||||
|
||||
val onboard = needsOnboarding(token)
|
||||
if (onboard) {
|
||||
_navigateTo = "onboarding"
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
PeptideAPI.loginAs(token)
|
||||
PeptideAPI.setSessionId(response.firstUserHints.token)
|
||||
|
||||
_navigateTo = "home"
|
||||
} catch (e: Exception) {
|
||||
_error = e.message ?: "Unknown error"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_error = if (e.message?.startsWith("Unexpected JSON token") == true) {
|
||||
PeptideApplication.instance.getString(R.string.service_health_alert_body_default)
|
||||
} else e.message ?: "Unknown error"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +171,13 @@ class LoginViewModel @Inject constructor(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltViewModel()) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val window = (context as Activity).window
|
||||
val previousMode = window.attributes.softInputMode
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
||||
onDispose { window.setSoftInputMode(previousMode) }
|
||||
}
|
||||
val passwordTextFieldState = rememberTextFieldState()
|
||||
LaunchedEffect(passwordTextFieldState.text) {
|
||||
viewModel.setPassword(passwordTextFieldState.text.toString())
|
||||
|
|
@ -230,6 +253,7 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
|
|||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||
.imePadding(),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
|
|
@ -334,6 +358,16 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
|
|||
url = "${PeptideAPI.getCurrentAppUrl()}/login/reset",
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
if (viewModel.error != null) {
|
||||
Text(
|
||||
text = viewModel.error ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
SquareButton(
|
||||
|
|
@ -343,8 +377,17 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("confirm_platform_button"),
|
||||
enabled = !viewModel.isLoading && viewModel.email.isNotBlank() && viewModel.password.isNotBlank()
|
||||
) {
|
||||
Text(text = stringResource(R.string.login))
|
||||
if (viewModel.isLoading) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text(text = stringResource(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,20 +74,24 @@ class RegisterDetailsScreenViewModel : ViewModel() {
|
|||
var password by mutableStateOf("")
|
||||
var error by mutableStateOf<String?>(null)
|
||||
private var captchaToken by mutableStateOf<String?>(null)
|
||||
var isLoading by mutableStateOf(false)
|
||||
|
||||
fun initCaptcha(context: Context, onSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
val root = try {
|
||||
getRootRoute()
|
||||
} catch (e: Exception) {
|
||||
error = if (e.message?.startsWith("Expected response body of the type") == true) {
|
||||
PeptideApplication.instance.getString(R.string.service_health_alert_body_default)
|
||||
} else e.message
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!root.features.captcha.enabled) {
|
||||
onSuccess()
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
@ -97,18 +101,25 @@ class RegisterDetailsScreenViewModel : ViewModel() {
|
|||
size(HCaptchaSize.INVISIBLE)
|
||||
}.build()
|
||||
|
||||
HCaptcha.getClient(context).apply {
|
||||
addOnSuccessListener {
|
||||
captchaToken = it.tokenResult
|
||||
onSuccess()
|
||||
}
|
||||
try {
|
||||
HCaptcha.getClient(context).apply {
|
||||
addOnSuccessListener {
|
||||
captchaToken = it.tokenResult
|
||||
onSuccess()
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
addOnFailureListener {
|
||||
error = it.message
|
||||
}
|
||||
addOnFailureListener {
|
||||
error = it.message
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
setup(config)
|
||||
verifyWithHCaptcha()
|
||||
setup(config)
|
||||
verifyWithHCaptcha()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,12 +132,18 @@ class RegisterDetailsScreenViewModel : ViewModel() {
|
|||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = register(body)
|
||||
|
||||
if (result.ok) {
|
||||
navController.navigate("register/verify/$email")
|
||||
} else {
|
||||
error = result.unwrapError().type
|
||||
isLoading = true
|
||||
try {
|
||||
val result = register(body)
|
||||
if (result.ok) {
|
||||
navController.navigate("register/verify/$email")
|
||||
} else {
|
||||
error = result.unwrapError().type
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,6 +310,17 @@ fun RegisterDetailsScreen(
|
|||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (viewModel.error != null) {
|
||||
Text(
|
||||
text = viewModel.error ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
SquareButton(
|
||||
onClick = {
|
||||
viewModel.initCaptcha(context) {
|
||||
|
|
@ -302,8 +330,17 @@ fun RegisterDetailsScreen(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("setup_continue_button"),
|
||||
enabled = !viewModel.isLoading && viewModel.email.isNotBlank() && viewModel.password.isNotBlank(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.continue_))
|
||||
if (viewModel.isLoading) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text(text = stringResource(R.string.continue_))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="500">
|
||||
android:width="73dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="73"
|
||||
android:viewportHeight="72">
|
||||
<path
|
||||
android:pathData="M122.59,447C122.59,326.35 122.59,247.26 122.59,144.56L46,53H293.71C323.14,53 348.89,58.32 370.95,68.97C393.02,79.61 410.19,94.94 422.45,114.95C434.71,134.96 440.84,158.94 440.84,186.9C440.84,215.12 434.51,238.91 421.87,258.27C409.35,277.64 391.73,292.26 369.02,302.14C346.43,312.01 320.04,316.95 289.84,316.95H187.63V233.84H268.16C280.81,233.84 291.59,232.3 300.49,229.22C309.52,226.02 316.43,220.95 321.2,214.02C326.11,207.1 328.56,198.06 328.56,186.9C328.56,175.61 326.11,166.44 321.2,159.39C316.43,152.21 309.52,146.95 300.49,143.61C291.59,140.15 280.81,138.42 268.16,138.42H230.22V447H122.59ZM354.89,266.16L454,447H337.08L240.29,266.16H354.89Z"
|
||||
android:fillColor="@color/primary"/>
|
||||
android:pathData="M32.783,19.341V30.328H25.235V19.341H32.783ZM55.191,19.341V30.328H47.644V19.341H55.191Z"
|
||||
android:fillColor="#662D91"/>
|
||||
<path
|
||||
android:pathData="M55.191,44.908H47.69V52.278H25.235V44.908H47.644V37.538H55.191V44.908ZM25.235,37.538V44.908H17.688V37.538H25.235ZM25.235,30.328H17.688V19.341H25.235V30.328ZM47.667,30.328H40.12V19.341H47.667V30.328Z"
|
||||
android:fillColor="#FFDE17"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#FFFF005C</color>
|
||||
<color name="primary">#FF100518</color>
|
||||
<color name="background">#FF131722</color>
|
||||
<color name="splashscreen_background">#FFFFFFFF</color>
|
||||
<color name="foreground">#FF000000</color>
|
||||
|
|
|
|||
Loading…
Reference in New Issue