multiplatform: edge to edge design (#5051)

* multiplatform: insets

* more features and better performance

* calls and removed unused code

* changes

* removed logs

* status and nav bar colors

* chatList and newChatSheet search fields

* overhaul

* search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

* changes

* ideal adapting height layout

* dropdownmenu, userpicker, onehandui, call layout, columns

* rename bars properties and strings

* faster update and better layout

* gallery in landscape with cutout

* better cutout

* 1% step on slider

* app bar moves to bottom in one hand ui

* default alpha

* changes

* userpicker colors

* changes

* blur

* fix wrong drawing area in chatview

* fix

* fixed differently

* changes

* changes

* android fix

* Revert "android fix"

This reverts commit 7d417afd9b.

* changes

* changes

* blur

* swap

* no logs

* fix build

* old Android support

* fix position of menu

* disable blur on Android 12

* call button padding

* useless code

* fix padding in group info view

* rename

* rename

* newline

* one more fix

* changes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-10-31 17:26:17 +00:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent 24090fe350
commit 4162bccc46
108 changed files with 2651 additions and 1935 deletions
@@ -3,20 +3,8 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import com.google.accompanist.insets.navigationBarsWithImePadding
import java.io.File
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
@Composable
actual fun ProvideWindowInsets(
consumeWindowInsets: Boolean,
windowInsetsAnimationsEnabled: Boolean,
content: @Composable () -> Unit
) {
com.google.accompanist.insets.ProvideWindowInsets(content = content)
}
@Composable
actual fun Modifier.desktopOnExternalDrag(
enabled: Boolean,
@@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.text.InputType
import android.util.Log
import android.view.OnReceiveContentListener
import android.view.ViewGroup
import android.view.*
import android.view.inputmethod.*
import android.widget.EditText
import android.widget.TextView
@@ -141,6 +140,13 @@ actual fun PlatformTextField(
Log.e(TAG, e.stackTraceToString())
}
}
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
// shows keyboard when user had search field on ChatView focused before clicking on this text field
// it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField
if (hasFocus) {
showKeyboard = true
}
}
editText.doOnTextChanged { text, _, _, _ ->
if (!composeState.value.inProgress) {
onMessageChange(text.toString())
@@ -6,11 +6,11 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.text.BidiFormatter
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
}
@Composable
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
actual fun windowWidth(): Dp {
val direction = LocalLayoutDirection.current
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction)
}
@Composable
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
@@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.chatlist.NavigationBarBackground
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.filter
import kotlin.math.absoluteValue
@@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
fillMaxSize: Boolean,
content: LazyListScope.() -> Unit
) {
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
val connection = LocalAppBarHandler.current?.connection
val handler = LocalAppBarHandler.current
require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" }
val state = state ?: handler.listState
val connection = handler.connection
LaunchedEffect(Unit) {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
if (reverseLayout) {
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
.collect { scrollPosition ->
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
} else {
// show always when last item is not visible
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
} else {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection.appBarOffset
if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
}
if (connection != null) {
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
} else {
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
LazyColumn(
if (fillMaxSize) {
Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
} else {
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
},
state,
contentPadding,
reverseLayout,
verticalArrangement,
horizontalAlignment,
flingBehavior,
userScrollEnabled
) {
content()
}
}
@Composable
actual fun LazyColumnWithScrollBarNoAppBar(
modifier: Modifier,
state: LazyListState?,
contentPadding: PaddingValues,
reverseLayout: Boolean,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
content: LazyListScope.() -> Unit
) {
val state = state ?: rememberLazyListState()
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) {
content()
}
}
@@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal,
state: ScrollState?,
maxIntrinsicSize: Boolean,
fillMaxSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit)
) {
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState()
val connection = LocalAppBarHandler.current?.connection
val handler = LocalAppBarHandler.current
require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" }
val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding()
val state = state ?: handler.scrollState
val connection = handler.connection
LaunchedEffect(Unit) {
snapshotFlow { state.value }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
val offset = connection.appBarOffset
if ((offset + scrollPosition).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
if (connection != null) {
Column(
if (maxIntrinsicSize) {
modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
} else {
modifier.nestedScroll(connection).verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content)
} else {
Column(if (maxIntrinsicSize) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column(
if (maxIntrinsicSize) {
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
} else {
Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state)
}, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars))
content()
Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier))
} else {
Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
}
}
@Composable
actual fun ColumnWithScrollBarNoAppBar(
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
state: ScrollState?,
maxIntrinsicSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit)
) {
val modifier = modifier.imePadding()
val state = state ?: rememberScrollState()
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column(
if (maxIntrinsicSize) {
modifier.verticalScroll(state).height(IntrinsicSize.Max)
} else {
modifier.verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content)
}, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
content()
} else {
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
}
}
@@ -3,17 +3,18 @@ package chat.simplex.common.platform
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.os.*
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR
@@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable
actual fun getKeyboardState(): State<KeyboardState> {
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
KeyboardState.Opened
} else {
KeyboardState.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
val density = LocalDensity.current
val ime = WindowInsets.ime
return remember {
derivedStateOf {
if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened
}
}
return keyboardState
}
actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {
@@ -47,6 +47,7 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource
@@ -328,11 +329,14 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit
) {
Column {
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
if (call.hasVideo) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
}
CallAppBar(
title = {
if (call.hasVideo) {
Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
},
onBack = { chatModel.activeCallViewIsCollapsed.value = true }
)
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@Composable
fun SelectSoundDevice(size: Dp) {
@@ -590,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
}
}
} else {
ModalView(background = Color.Black, showClose = false, close = {}) {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
ModalView(background = Color.Black, showAppBar = false, close = {}) {
Column {
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
val onClick = {
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.model.CIFile
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.helpers.ModalManager
@@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView(
if (getLoadedFilePath(file) != null) {
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
if (smallView) {
DisposableEffect(Unit) {
onDispose {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
}
}
}
@@ -1,6 +1,5 @@
package chat.simplex.common.views.chatlist
import android.app.Activity
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@@ -11,21 +10,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
import chat.simplex.common.model.durationText
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
@@ -37,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val onClick = { platform.androidStartCallActivity(false) }
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) {
val source = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = true, 3000.dp) }
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = ripple, interactionSource = source)) {
GreenLine(call)
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) {
GreenLine(statusBar, call)
}
Box(
Modifier
@@ -62,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) {
}
}
// Temporary solution for storing a color that needs to be applied after call ends
var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb())
@Composable
private fun GreenLine(call: Call) {
private fun GreenLine(statusBarHeight: Dp, call: Call) {
Row(
Modifier
.fillMaxSize()
.background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET)
.padding(top = -CALL_TOP_OFFSET + statusBarHeight)
.padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
@@ -80,12 +73,10 @@ private fun GreenLine(call: Call) {
Spacer(Modifier.weight(1f))
CallDuration(call)
}
val window = (LocalContext.current as Activity).window
DisposableEffect(Unit) {
statusBarColorAfterCall.intValue = window.statusBarColor
window.statusBarColor = SimplexGreen.toArgb()
platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight)
onDispose {
window.statusBarColor = statusBarColorAfterCall.intValue
platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight)
}
}
}
@@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp
@Composable
actual fun UserPickerUsersSection(
users: List<UserInfo>,
iconColor: Color,
stopped: Boolean,
onUserClicked: (user: User) -> Unit,
) {
@@ -140,87 +139,73 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
} else {
Modifier
}
Box(
Modifier
.fillMaxSize()
.then(clickableModifier)
.drawBehind {
val pos = when {
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
}
val colors = CurrentColors.value.colors
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos)
val shadingColor = resultingColor.copy(alpha = adjustedAlpha)
if (pickerState.value.isVisible()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha())
)
} else if (ModalManager.start.modalCount.value == 0) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
colors.background
})
)
}
drawRect(
if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos)
)
}
.graphicsLayer {
if (heightValue == 0) {
alpha = 0f
}
translationY = dismissState.offset.value
},
contentAlignment = Alignment.BottomCenter
) {
Box {
Box(
Modifier.onSizeChanged { height.intValue = it.height }
) {
KeyChangeEffect(pickerIsVisible) {
if (pickerState.value.isVisible()) {
try {
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
} catch (e: CancellationException) {
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
pickerState.value = AnimatedViewState.GONE
Modifier
.fillMaxSize()
.then(clickableModifier)
.drawBehind {
val pos = calculatePosition(dismissState)
val colors = CurrentColors.value.colors
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
drawRect(
if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos)
)
}
.graphicsLayer {
if (heightValue == 0) {
alpha = 0f
}
} else {
try {
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
} catch (e: CancellationException) {
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
pickerState.value = AnimatedViewState.VISIBLE
translationY = dismissState.offset.value
},
contentAlignment = Alignment.BottomCenter
) {
Box(
Modifier.onSizeChanged { height.intValue = it.height }
) {
KeyChangeEffect(pickerIsVisible) {
if (pickerState.value.isVisible()) {
try {
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
} catch (e: CancellationException) {
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
pickerState.value = AnimatedViewState.GONE
}
} else {
try {
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
} catch (e: CancellationException) {
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
pickerState.value = AnimatedViewState.VISIBLE
}
}
}
}
val draggableModifier = if (height.intValue != 0)
Modifier.draggableBottomDrawerModifier(
state = dismissState,
swipeDistance = height.intValue.toFloat(),
)
else Modifier
Box(draggableModifier.then(modifier)) {
content()
val draggableModifier = if (height.intValue != 0)
Modifier.draggableBottomDrawerModifier(
state = dismissState,
swipeDistance = height.intValue.toFloat(),
)
else Modifier
Box(draggableModifier.then(modifier).navigationBarsPadding()) {
content()
}
}
}
NavigationBarBackground(
modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f },
color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha())
)
}
}
private fun calculatePosition(dismissState: DismissState): Float = when {
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
}
private fun Modifier.draggableBottomDrawerModifier(
state: DismissState,
swipeDistance: Float,
@@ -171,6 +171,8 @@ actual fun GetImageBottomSheet(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.imePadding()
.navigationBarsPadding()
.onFocusChanged { focusState ->
if (!focusState.hasFocus) hideBottomSheet()
}
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionSpacer
import SectionView
import android.app.Activity
import android.content.ComponentName
@@ -31,6 +32,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
@@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout(
systemDarkTheme: SharedPreference<String?>,
changeIcon: (AppIcon) -> Unit,
) {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
val context = LocalContext.current
@@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout(
}
// }
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
}
SectionDividerSpaced()
ThemesSection(systemDarkTheme)
SectionDividerSpaced()
AppToolbarsSection()
SectionDividerSpaced()
MessageShapeSection()
@@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) {
SectionView(stringResource(MR.strings.settings_section_title_app)) {
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
AppVersionItem(showVersion)
}
}