desktop: adapted UI (#2755)

* desktop: adapted UI

* more changes

* divider fix

* do not close screens on non-desktop in terminal view

* background click to close views and small changes

* dark theme detection on supported OSes

* fix text color after theme change

* placement of desktop text field

* marked as @Composable

* padding of text view

* window sizes

* screen layout

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-07-26 11:35:29 +03:00
committed by GitHub
parent c7783a7039
commit 26a233ab1a
59 changed files with 459 additions and 192 deletions

View File

@@ -55,6 +55,7 @@ allprojects {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://jitpack.io")
}
}

View File

@@ -94,6 +94,7 @@ kotlin {
val desktopMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1")
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
}
}
val desktopTest by getting

View File

@@ -48,4 +48,6 @@ actual fun screenOrientation(): ScreenOrientation = when (mainActivity.get()?.re
@Composable
actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
actual fun desktopExpandWindowToWidth(width: Dp) {}
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)

View File

@@ -0,0 +1,6 @@
package chat.simplex.common.ui.theme
import androidx.compose.runtime.Composable
@Composable
actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme()

View File

@@ -35,7 +35,7 @@ actual fun SimpleAndAnimatedImageView(
ImageView(imagePainter) {
hideKeyboard(view)
if (getLoadedFilePath(file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
}

View File

@@ -69,7 +69,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
changeIcon = ::setAppIcon,
showSettingsModal = showSettingsModal,
editColor = { name, initialColor ->
ModalManager.shared.showModalCloseable { close ->
ModalManager.start.showModalCloseable { close ->
ColorEditor(name, initialColor, close)
}
},

View File

@@ -1,35 +1,46 @@
package chat.simplex.common
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.common.views.SplashView
import chat.simplex.common.views.call.ActiveCallView
import chat.simplex.common.views.call.IncomingCallAlertView
import chat.simplex.common.views.chat.ChatView
import chat.simplex.common.views.chatlist.ChatListView
import chat.simplex.common.views.chatlist.ShareListView
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.database.DatabaseErrorView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.localauth.VerticalDivider
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>,
val scaffoldState: ScaffoldState,
val switchingUsers: MutableState<Boolean>
)
@Composable
fun AppScreen() {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
@@ -66,7 +77,7 @@ fun MainScreen() {
}
LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value) {
ModalManager.shared.closeModals()
ModalManager.closeAllModalsEverywhere()
chatModel.clearOverlays.value = false
}
}
@@ -105,61 +116,30 @@ fun MainScreen() {
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
Box {
showAdvertiseLAAlert = true
BoxWithConstraints {
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
Box(
Modifier
.graphicsLayer {
translationX = -offset.value.dp.toPx()
}
) {
if (chatModel.setDeliveryReceipts.value) {
SetDeliveryReceiptsView(chatModel)
} else {
val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null)
ChatListView(chatModel, AppLock::setPerformLA, stopped)
else
ShareListView(chatModel, stopped)
}
}
val scope = rememberCoroutineScope()
val onComposed: () -> Unit = {
scope.launch {
offset.animateTo(
if (chatModel.chatId.value == null) 0f else maxWidth.value,
chatListAnimationSpec()
)
if (offset.value == 0f) {
currentChatId = null
}
}
}
LaunchedEffect(Unit) {
launch {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.collect {
if (it != null) currentChatId = it
else onComposed()
}
}
}
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
currentChatId?.let {
ChatView(it, chatModel, onComposed)
}
}
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val scaffoldState = rememberScaffoldState()
val switchingUsers = rememberSaveable { mutableStateOf(false) }
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsers) }
if (appPlatform.isAndroid) {
AndroidScreen(settingsState)
} else {
DesktopScreen(settingsState)
}
}
}
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
onboarding == OnboardingStage.Step1_SimpleXInfo -> {
SimpleXInfo(chatModel, onboarding = true)
if (appPlatform.isDesktop) {
ModalManager.fullscreen.showInView()
}
}
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
ModalManager.shared.showInView()
if (appPlatform.isAndroid) {
ModalManager.fullscreen.showInView()
}
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
@@ -178,7 +158,7 @@ fun MainScreen() {
} else if (chatModel.showCallView.value) {
ActiveCallView()
}
ModalManager.shared.showPasscodeInView()
ModalManager.fullscreen.showPasscodeInView()
val invitation = chatModel.activeCallInvitation.value
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView()
@@ -200,6 +180,141 @@ fun MainScreen() {
}
}
@Composable
fun AndroidScreen(settingsState: SettingsViewState) {
BoxWithConstraints {
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
Box(
Modifier
.graphicsLayer {
translationX = -offset.value.dp.toPx()
}
) {
StartPartOfScreen(settingsState)
}
val scope = rememberCoroutineScope()
val onComposed: () -> Unit = {
scope.launch {
offset.animateTo(
if (chatModel.chatId.value == null) 0f else maxWidth.value,
chatListAnimationSpec()
)
if (offset.value == 0f) {
currentChatId = null
}
}
}
LaunchedEffect(Unit) {
launch {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.collect {
if (it != null) currentChatId = it
else onComposed()
}
}
}
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
currentChatId?.let {
ChatView(it, chatModel, onComposed)
}
}
}
}
@Composable
fun StartPartOfScreen(settingsState: SettingsViewState) {
if (chatModel.setDeliveryReceipts.value) {
SetDeliveryReceiptsView(chatModel)
} else {
val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null)
ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped)
else
ShareListView(chatModel, settingsState, stopped)
}
}
@Composable
fun CenterPartOfScreen() {
val currentChatId by remember { ChatModel.chatId }
LaunchedEffect(Unit) {
snapshotFlow { currentChatId }
.distinctUntilChanged()
.collect {
if (it != null) {
ModalManager.center.closeModals()
}
}
}
when (val id = currentChatId) {
null -> {
if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
Box(
Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
contentAlignment = Alignment.Center
) {
Text(stringResource(MR.strings.no_selected_chat))
}
} else {
ModalManager.center.showInView()
}
}
else -> ChatView(id, chatModel) {}
}
}
@Composable
fun EndPartOfScreen() {
ModalManager.end.showInView()
}
@Composable
fun DesktopScreen(settingsState: SettingsViewState) {
Box {
// 56.dp is a size of unused space of settings drawer
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) {
StartPartOfScreen(settingsState)
}
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
ModalManager.start.showInView()
}
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
CenterPartOfScreen()
}
if (ModalManager.end.hasModalsOpen()) {
VerticalDivider()
}
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) {
EndPartOfScreen()
}
}
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
val scope = rememberCoroutineScope()
if (scaffoldState.drawerState.isOpen) {
Box(
Modifier
.fillMaxSize()
.padding(start = DEFAULT_START_MODAL_WIDTH)
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
ModalManager.start.closeModals()
scope.launch { settingsState.scaffoldState.drawerState.close() }
})
)
}
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
}
ModalManager.fullscreen.showInView()
ModalManager.fullscreen.showPasscodeInView()
}
}
@Composable
fun InitializationView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

View File

@@ -71,7 +71,7 @@ object AppLock {
private fun initialEnableLA() {
val m = ChatModel
val appPrefs = ChatController.appPrefs
appPrefs.laMode.set(LAMode.SYSTEM)
appPrefs.laMode.set(LAMode.default)
authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential),
@@ -100,7 +100,7 @@ object AppLock {
private fun setPasscode() {
val appPrefs = ChatController.appPrefs
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {

View File

@@ -437,7 +437,7 @@ object ChatModel {
val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return
if (info.contactConnection.connReqInv == connReqInv.value) {
connReqInv.value = null
ModalManager.shared.closeModals()
ModalManager.center.closeModals()
}
}

View File

@@ -72,7 +72,7 @@ class AppPreferences {
set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) }
)
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } }
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.default) { LAMode.values().firstOrNull { it.name == this } }
val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30)
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)

View File

@@ -2,7 +2,7 @@ package com.sd.lib.compose.wheel_picker
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import chat.simplex.common.ui.theme.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue

View File

@@ -10,6 +10,9 @@ enum class AppPlatform {
val isAndroid: Boolean
get() = this == ANDROID
val isDesktop: Boolean
get() = this == DESKTOP
}
expect val appPlatform: AppPlatform

View File

@@ -28,4 +28,6 @@ expect fun screenOrientation(): ScreenOrientation
@Composable
expect fun screenWidth(): Dp
expect fun desktopExpandWindowToWidth(width: Dp)
expect fun isRtl(text: CharSequence): Boolean

View File

@@ -1,7 +1,6 @@
package chat.simplex.common.ui.theme
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -191,6 +190,11 @@ val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
val DEFAULT_BOTTOM_PADDING = 48.dp
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
val DEFAULT_START_MODAL_WIDTH = 388.dp
val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp
val DEFAULT_END_MODAL_WIDTH = 388.dp
val DEFAULT_MAX_IMAGE_WIDTH = 500.dp
val DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
primaryVariant = SimplexBlue,
@@ -255,6 +259,15 @@ val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow
@Composable
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
expect fun isSystemInDarkTheme(): Boolean
fun reactOnDarkThemeChanges(isDark: Boolean) {
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) {
// Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark)
}
}
@Composable
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
LaunchedEffect(darkTheme) {
@@ -264,10 +277,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
}
val systemDark = isSystemInDarkTheme()
LaunchedEffect(systemDark) {
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) {
// Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
}
reactOnDarkThemeChanges(systemDark)
}
val theme by CurrentColors.collectAsState()
MaterialTheme(

View File

@@ -24,6 +24,12 @@ import chat.simplex.common.platform.*
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
val close = {
close()
if (appPlatform.isDesktop) {
ModalManager.center.closeModals()
}
}
BackHandler(onBack = {
close()
})
@@ -126,7 +132,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}

View File

@@ -81,7 +81,7 @@ fun ChatInfoView(
setContactAlias(chat.chatInfo.apiId, it, chatModel)
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
ModalManager.end.showCustomModal { close ->
val user = chatModel.currentUser.value
if (user != null) {
ContactPreferencesView(chatModel, user, contact.contactId, close)
@@ -136,7 +136,7 @@ fun ChatInfoView(
})
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
ModalManager.end.showModalCloseable { close ->
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
VerifyCodeView(
ct.displayName,

View File

@@ -139,7 +139,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
if (chat.chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
}
@@ -149,7 +150,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
var groupLink = link?.first
var groupLinkMemberRole = link?.second
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
groupLink = it.first;
groupLinkMemberRole = it.second
@@ -174,7 +176,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
member to null
}
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
}
@@ -314,7 +317,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
withApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
ModalManager.shared.showModal(endButtons = { ShareButton {
ModalManager.end.closeModals()
ModalManager.end.showModal(endButtons = { ShareButton {
clipboard.shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
@@ -326,8 +330,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
hideKeyboard(view)
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, false, chatModel, close)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, false, chatModel, close)
}
}
},
@@ -462,7 +467,9 @@ fun ChatInfoToolbar(
showSearch = false
}
}
BackHandler(onBack = onBackClicked)
if (appPlatform.isAndroid) {
BackHandler(onBack = onBackClicked)
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add {
@@ -520,7 +527,7 @@ fun ChatInfoToolbar(
}
DefaultTopAppBar(
navigationButton = { NavigationButtonBack(onBackClicked) },
navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } },
title = { ChatInfoToolbarTitle(chat.chatInfo) },
onTitleClick = info,
showSearch = showSearch,

View File

@@ -170,7 +170,7 @@ fun ComposeView(
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
val processPickedMedia = { uris: List<URI>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
@@ -655,7 +655,11 @@ fun ComposeView(
} else {
showChooseAttachment
}
IconButton(attachmentClicked, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value
) {
Icon(
painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach),

View File

@@ -83,7 +83,7 @@ fun SendMsgView(
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd)) {
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val scope = rememberCoroutineScope()

View File

@@ -113,7 +113,7 @@ private fun VerifyCodeLayout(
} else {
if (appPlatform.isAndroid) {
SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) {
ModalManager.shared.showModal {
ModalManager.end.showModal {
ScanCodeView(verifyCode) { }
}
}

View File

@@ -50,7 +50,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
allowModifyMembers = allowModifyMembers,
searchText,
openPreferences = {
ModalManager.shared.showCustomModal { close ->
ModalManager.end.showCustomModal { close ->
GroupPreferencesView(chatModel, groupInfo.id, close)
}
},

View File

@@ -54,7 +54,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
addMembers = {
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, false, chatModel, close)
}
}
@@ -73,7 +73,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
} else {
member to null
}
ModalManager.shared.showModalCloseable(true) { closeCurrent ->
ModalManager.end.showModalCloseable(true) { closeCurrent ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
closeCurrent()
@@ -84,13 +84,13 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
}
},
editGroupProfile = {
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
ModalManager.end.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
},
addOrEditWelcomeMessage = {
ModalManager.shared.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
ModalManager.end.showCustomModal { close ->
GroupPreferencesView(
chatModel,
chat.id,
@@ -102,7 +102,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = {
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
ModalManager.end.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
}
)
}

View File

@@ -149,7 +149,7 @@ fun GroupMemberInfoView(
})
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
ModalManager.end.showModalCloseable { close ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
VerifyCodeView(
mem.displayName,

View File

@@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.File
@@ -85,7 +86,7 @@ fun CIImageView(
@Composable
fun imageViewFullWidth(): Dp {
val approximatePadding = 100.dp
return with(LocalDensity.current) { minOf(1000.dp, LocalWindowWidth() - approximatePadding) }
return with(LocalDensity.current) { minOf(DEFAULT_MAX_IMAGE_WIDTH, LocalWindowWidth() - approximatePadding) }
}
@Composable
@@ -93,10 +94,10 @@ fun CIImageView(
Image(
imageBitmap,
contentDescription = stringResource(MR.strings.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp)
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
@@ -110,10 +111,10 @@ fun CIImageView(
Image(
painter,
contentDescription = stringResource(MR.strings.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp)
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick

View File

@@ -21,7 +21,11 @@ import chat.simplex.res.MR
@Composable
fun CIInvalidJSONView(json: String) {
Row(Modifier
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
.clickable {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
ModalManager.center.showModal(true) { InvalidJSONView(json) }
}
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(stringResource(MR.strings.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)

View File

@@ -44,7 +44,7 @@ fun CIVideoView(
val view = LocalMultiplatformView()
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
hideKeyboard(view)
ModalManager.shared.showCustomModal(animated = false) { close ->
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
})
@@ -113,7 +113,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
}
Box {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
PlayerView(
player,
width,
@@ -202,7 +202,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
@Composable
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
Image(
preview,
contentDescription = stringResource(MR.strings.video_descr),
@@ -311,5 +311,5 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
private fun videoViewFullWidth(windowWidth: Dp): Dp {
val approximatePadding = 100.dp
return minOf(1000.dp, windowWidth - approximatePadding)
return minOf(DEFAULT_MAX_IMAGE_WIDTH, windowWidth - approximatePadding)
}

View File

@@ -25,6 +25,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.ContactConnectionInfoView
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.platform.ntfManager
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@@ -73,7 +74,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
ChatListNavLinkLayout(
chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) },
click = {
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.center.closeModals()
ModalManager.end.closeModals()
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
}
},
@@ -87,7 +90,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
InvalidDataView()
},
click = {
ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
ModalManager.end.closeModals()
ModalManager.center.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
},
dropdownMenuItems = null,
showMenu,
@@ -342,7 +346,9 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
stringResource(MR.strings.set_contact_name),
painterResource(MR.images.ic_edit),
onClick = {
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.center.closeModals()
ModalManager.end.closeModals()
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
}
showMenu.value = false

View File

@@ -14,10 +14,9 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.*
import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@@ -34,9 +33,8 @@ import kotlinx.coroutines.launch
import java.net.URI
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = {
newChatSheetState.value = AnimatedViewState.VISIBLE
}
@@ -47,7 +45,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) {
delay(1000L)
ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) }
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
}
}
LaunchedEffect(chatModel.clearOverlays.value) {
@@ -60,13 +58,13 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
connectIfOpenedViaUri(url, chatModel)
}
}
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
var searchInList by rememberSaveable { mutableStateOf("") }
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold(topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA) },
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
floatingActionButton = {
if (searchInList.isEmpty()) {
@@ -76,7 +74,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
}
},
Modifier.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp),
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
@@ -91,7 +89,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
}
}
) {
Box(Modifier.padding(it)) {
Box(Modifier.padding(it).padding(end = endPadding)) {
Column(
modifier = Modifier
.fillMaxSize()
@@ -112,8 +110,10 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (searchInList.isEmpty()) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
}
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
if (appPlatform.isAndroid) {
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
}
}
if (switchingUsers.value) {
Box(

View File

@@ -13,20 +13,24 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.SettingsViewState
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.Chat
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow
@Composable
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val switchingUsers = rememberSaveable { mutableStateOf(false) }
val (userPickerState, scaffoldState, switchingUsers) = settingsState
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
Scaffold(
Modifier.padding(end = endPadding),
scaffoldState = scaffoldState,
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
) {
Box(Modifier.padding(it)) {
@@ -42,9 +46,11 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
}
}
}
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
})
if (appPlatform.isAndroid) {
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
})
}
}
@Composable

View File

@@ -41,6 +41,11 @@ fun UserPicker(
) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
if (newChat.isVisible()) {
BackHandler {
userPickerState.value = AnimatedViewState.HIDING
}
}
val users by remember {
derivedStateOf {
chatModel.users
@@ -121,6 +126,7 @@ fun UserPicker(
delay(500)
switchingUsers.value = true
}
ModalManager.closeAllModalsEverywhere()
chatModel.controller.changeActiveUser(u.user.userId, null)
job.cancel()
switchingUsers.value = false

View File

@@ -83,7 +83,7 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
if (fileDeleted) {
m.controller.appPrefs.chatArchiveName.set(null)
m.controller.appPrefs.chatArchiveTime.set(null)
ModalManager.shared.closeModal()
ModalManager.start.closeModal()
} else {
Log.e(TAG, "deleteArchiveAlert delete() error")
}

View File

@@ -357,11 +357,11 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
}
if (m.chatDbStatus.value !is DBMigrationResult.OK) {
/** Hide current view and show [DatabaseErrorView] */
ModalManager.shared.closeModals()
ModalManager.closeAllModalsEverywhere()
return@withApi
}
if (m.currentUser.value == null) {
ModalManager.shared.closeModals()
ModalManager.closeAllModalsEverywhere()
return@withApi
} else {
m.controller.apiStartChat()

View File

@@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
@Composable
fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
Column(
Modifier
.fillMaxWidth()
@@ -32,7 +32,11 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
NavigationButtonBack(onButtonClicked = close)
if (showClose) {
NavigationButtonBack(onButtonClicked = close)
} else {
Spacer(Modifier)
}
Row {
endButtons()
}

View File

@@ -45,7 +45,7 @@ fun authenticateWithPasscode(
completed: (LAResult) -> Unit
) {
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
ModalManager.shared.showPasscodeCustomModal { close ->
ModalManager.fullscreen.showPasscodeCustomModal { close ->
BackHandler {
close()
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))

View File

@@ -8,43 +8,51 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.themedBackground
import chat.simplex.common.ui.theme.*
import java.util.concurrent.atomic.AtomicBoolean
@Composable
fun ModalView(
close: () -> Unit,
showClose: Boolean = true,
background: Color = MaterialTheme.colors.background,
modifier: Modifier = Modifier,
endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable () -> Unit,
) {
BackHandler(onBack = close)
if (showClose) {
BackHandler(onBack = close)
}
Surface(Modifier.fillMaxSize()) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close, endButtons)
CloseSheetBar(close, showClose, endButtons)
Box(modifier) { content() }
}
}
}
class ModalManager {
enum class ModalPlacement {
START, CENTER, END, FULLSCREEN
}
class ModalManager(private val placement: ModalPlacement? = null) {
private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>()
private val modalCount = mutableStateOf(0)
private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false)
private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null)
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
showCustomModal { close ->
ModalView(close, endButtons = endButtons, content = content)
ModalView(close, showClose = showClose, endButtons = endButtons, content = content)
}
}
fun showModalCloseable(settings: Boolean = false, content: @Composable (close: () -> Unit) -> Unit) {
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable (close: () -> Unit) -> Unit) {
showCustomModal { close ->
ModalView(close, content = { content(close) })
ModalView(close, showClose = showClose, content = { content(close) })
}
}
@@ -55,8 +63,17 @@ class ModalManager {
if (toRemove.isNotEmpty()) {
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
}
modalViews.add(animated to modal)
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
// to prevent unneeded animation on different situations
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
modalViews.add(anim to modal)
modalCount.value = modalViews.size - toRemove.size
if (placement == ModalPlacement.CENTER) {
ChatModel.chatId.value = null
} else if (placement == ModalPlacement.END) {
desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH)
}
}
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
@@ -146,6 +163,17 @@ private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOu
// private fun <T> animationSpecFromEnd() = tween<T>(durationMillis = 100, easing = FastOutSlowInEasing)
companion object {
val shared = ModalManager()
private val shared = ModalManager()
val start = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START)
val center = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.CENTER)
val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END)
val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN)
fun closeAllModalsEverywhere() {
start.closeModals()
center.closeModals()
end.closeModals()
fullscreen.closeModals()
}
}
}

View File

@@ -70,7 +70,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
if (createdUser != null) {
m.controller.startChat(createdUser)
}
ModalManager.shared.closeModals()
ModalManager.fullscreen.closeModals()
AlertManager.shared.hideAlert()
completed(LAResult.Success)
} catch (e: Exception) {

View File

@@ -30,7 +30,7 @@ fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
connIncognito = connIncognito,
share = { clipboard.shareText(connReqInvitation) },
learnMore = {
ModalManager.shared.showModal {
ModalManager.center.showModal {
Column(
Modifier
.fillMaxHeight()

View File

@@ -45,7 +45,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
chatModel.chatId.value = groupInfo.id
setGroupMembers(groupInfo, chatModel)
close.invoke()
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, true, chatModel, close)
}
}

View File

@@ -42,7 +42,7 @@ fun ContactConnectionInfoView(
**/
DisposableEffect(Unit) {
onDispose {
if (!ModalManager.shared.hasModalsOpen()) {
if (!ModalManager.center.hasModalsOpen()) {
chatModel.connReqInv.value = null
}
}
@@ -57,7 +57,7 @@ fun ContactConnectionInfoView(
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
learnMore = {
ModalManager.shared.showModal {
ModalManager.center.showModal {
Column(
Modifier
.fillMaxHeight()

View File

@@ -35,7 +35,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
**/
DisposableEffect(Unit) {
onDispose {
if (!ModalManager.shared.hasModalsOpen()) {
if (!ModalManager.center.hasModalsOpen()) {
m.connReqInv.value = null
}
}

View File

@@ -40,15 +40,18 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
stopped,
addContact = {
closeNewChatSheet(false)
ModalManager.shared.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
ModalManager.center.closeModals()
ModalManager.center.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
},
connectViaLink = {
closeNewChatSheet(false)
ModalManager.shared.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
ModalManager.center.closeModals()
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
},
createGroup = {
closeNewChatSheet(false)
ModalManager.shared.showCustomModal { close -> AddGroupView(chatModel, close) }
ModalManager.center.closeModals()
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, close) }
},
closeNewChatSheet,
)
@@ -93,10 +96,12 @@ private fun NewChatSheetLayout(
}
}
}
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
val maxWidth = with(LocalDensity.current) { screenWidth() * density }
Column(
Modifier
.fillMaxSize()
.padding(end = endPadding)
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) }
.drawBehind { drawRect(animatedColor.value) },

View File

@@ -4,9 +4,11 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.compose.stringResource
import boofcv.alg.drawing.FiducialImageEngine
import boofcv.alg.fiducial.qrcode.*
@@ -25,7 +27,7 @@ fun QRCode(
) {
val scope = rememberCoroutineScope()
BoxWithConstraints {
BoxWithConstraints(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() }
val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) {
qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
@@ -34,7 +36,9 @@ fun QRCode(
Image(
bitmap = qr,
contentDescription = stringResource(MR.strings.image_descr_qr_code),
modifier
Modifier
.widthIn(max = 500.dp)
.then(modifier)
.clickable {
scope.launch {
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())

View File

@@ -42,7 +42,7 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
if (onboardingStage != null) {
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.shared.closeModal() })
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() })
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}

View File

@@ -26,7 +26,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } },
)
}

View File

@@ -41,7 +41,7 @@ object AppearanceScope {
) {
val currentTheme by CurrentColors.collectAsState()
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
val darkTheme = isSystemInDarkTheme()
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
val state = remember { derivedStateOf { currentTheme.name } }
ThemeSelector(state) {
ThemeManager.applyTheme(it, darkTheme)
@@ -226,7 +226,7 @@ object AppearanceScope {
@Composable
private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) {
val darkTheme = isSystemInDarkTheme()
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) }
ExposedDropDownSettingRow(
generalGetString(MR.strings.theme),

View File

@@ -32,7 +32,7 @@ fun NotificationsSettingsView(
notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state },
notificationPreviewMode = chatModel.notificationPreviewMode,
showPage = { page ->
ModalManager.shared.showModalCloseable(true) {
ModalManager.start.showModalCloseable(true) {
when (page) {
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)

View File

@@ -278,7 +278,7 @@ fun SimplexLockView(
}
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
@@ -306,7 +306,7 @@ fun SimplexLockView(
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
LAResult.Success -> {
if (!selfDestruct.get()) {
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
EnableSelfDestruct(selfDestruct, close)
}
} else {
@@ -322,7 +322,7 @@ fun SimplexLockView(
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
@@ -345,7 +345,7 @@ fun SimplexLockView(
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword,
@@ -380,7 +380,7 @@ fun SimplexLockView(
setPerformLA(true)
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
@@ -427,7 +427,7 @@ fun SimplexLockView(
SectionDividerSpaced()
SectionView(stringResource(MR.strings.self_destruct_passcode).uppercase()) {
val openInfo = {
ModalManager.shared.showModal {
ModalManager.start.showModal {
SelfDestructInfoView()
}
}

View File

@@ -62,7 +62,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
fun showServer(server: ServerCfg) {
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.start.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
val index = servers.indexOf(old)
ProtocolServerView(
@@ -117,7 +117,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
if (appPlatform.isAndroid) {
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ModalManager.start.showModalCloseable { close ->
ScanProtocolServer {
close()
servers = servers + it

View File

@@ -11,10 +11,10 @@ import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import chat.simplex.common.platform.TAG
import chat.simplex.common.platform.Log
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
@@ -73,8 +73,9 @@ private fun SetDeliveryReceiptsLayout(
skip: () -> Unit,
userCount: Int,
) {
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING, end = endPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(MR.strings.delivery_receipts_title))

View File

@@ -23,16 +23,17 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.appVersionInfo
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.database.DatabaseView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.SimpleXInfo
import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.res.MR
import kotlinx.coroutines.launch
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false
@@ -48,10 +49,10 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
chatModel.controller.appPrefs.incognito,
user.displayName,
setPerformLA = setPerformLA,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
showSettingsModalWithSearch = { modalView ->
ModalManager.shared.showCustomModal { close ->
ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
@@ -61,12 +62,12 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
content = { modalView(chatModel, search) })
}
},
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
showVersion = {
withApi {
val info = chatModel.controller.apiGetVersion()
if (info != null) {
ModalManager.shared.showModal { VersionInfoView(info) }
ModalManager.start.showModal { VersionInfoView(info) }
}
}
},
@@ -75,7 +76,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
block()
} else {
var autoShow = true
ModalManager.shared.showModalCloseable { close ->
ModalManager.fullscreen.showModalCloseable { close ->
val onFinishAuth = { success: Boolean ->
if (success) {
close()
@@ -104,6 +105,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
}
}
},
drawerState = drawerState,
)
}
}
@@ -125,15 +127,25 @@ fun SettingsLayout(
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
drawerState: DrawerState,
) {
val scope = rememberCoroutineScope()
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
if (drawerState.isOpen) {
BackHandler {
closeSettings()
}
}
val theme = CurrentColors.collectAsState()
val uriHandler = LocalUriHandler.current
Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).themedBackground(theme.value.base)) {
Box(Modifier.fillMaxSize()) {
Column(
Modifier
.fillMaxSize()
.padding(top = DEFAULT_PADDING)
.verticalScroll(rememberScrollState())
.themedBackground(theme.value.base)
.padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3)
) {
AppBarTitle(stringResource(MR.strings.your_settings))
@@ -178,6 +190,17 @@ fun SettingsLayout(
SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth)
SectionBottomSpacer()
}
if (appPlatform.isDesktop) {
Box(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
.background(if (isInDarkTheme()) ToolbarDark else ToolbarLight)
.padding(start = 4.dp, top = 8.dp)
) {
NavigationButtonBack(closeSettings)
}
}
}
}
@@ -510,6 +533,7 @@ fun PreviewSettingsLayout() {
showCustomModal = { {} },
showVersion = {},
withAuth = { _, _, _ -> },
drawerState = DrawerState(DrawerValue.Closed),
)
}
}

View File

@@ -85,7 +85,7 @@ fun UserAddressView(
}
},
learnMore = {
ModalManager.shared.showModal {
ModalManager.start.showModal {
Column(
Modifier
.fillMaxHeight()

View File

@@ -30,6 +30,7 @@ import chat.simplex.common.views.database.PassphraseField
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.CreateProfile
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay
@@ -47,11 +48,15 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
visibleUsersCount = visibleUsersCount(m),
addUser = {
ModalManager.shared.showModalCloseable { close ->
ModalManager.center.showModalCloseable { close ->
CreateProfile(m, close)
}
},
activateUser = { user ->
if (appPlatform.isDesktop) {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
}
withBGApi {
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
}
@@ -99,7 +104,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
},
unhideUser = { user ->
if (passwordEntryRequired(user, searchTextOrPassword.value)) {
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.start.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
withBGApi {
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
@@ -122,7 +127,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
},
showHiddenProfile = { user ->
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.start.showModalCloseable(true) { close ->
HiddenProfileView(m, user) {
profileHidden.value = true
withBGApi {
@@ -328,7 +333,7 @@ private fun passwordEntryRequired(user: User, searchTextOrPassword: String): Boo
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) {
if (passwordEntryRequired(user, searchTextOrPassword)) {
ModalManager.shared.showModalCloseable(true) { close ->
ModalManager.start.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.DELETE, user) { pwd ->
withBGApi {
doRemoveUser(m, user, users, delSMPQueues, pwd)

View File

@@ -267,6 +267,9 @@
<string name="you_have_no_chats">You have no chats</string>
<string name="no_filtered_chats">No filtered chats</string>
<!-- ChatView.kt -->
<string name="no_selected_chat">No selected chat</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
<string name="share_image">Share media…</string>

View File

@@ -24,7 +24,8 @@ import java.io.File
val simplexWindowState = SimplexWindowState()
fun showApp() = application {
val windowState = rememberWindowState(placement = WindowPlacement.Floating)
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = 1366.dp, height = 768.dp)
simplexWindowState.windowState = windowState
Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = {
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
simplexWindowState.backstack.lastOrNull()?.invoke() != null
@@ -108,6 +109,7 @@ fun showApp() = application {
}
class SimplexWindowState {
lateinit var windowState: WindowState
val backstack = mutableStateListOf<() -> Unit>()
val openDialog = DialogState<File?>()
val openMultipleDialog = DialogState<List<File>>()

View File

@@ -38,7 +38,7 @@ actual fun PlatformTextField(
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
@@ -74,14 +74,10 @@ actual fun PlatformTextField(
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
Box(
Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.padding(top = 4.dp)
.padding(bottom = 6.dp)
) {
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
Spacer(Modifier.height(8.dp))
innerTextField()
Spacer(Modifier.height(10.dp))
}
}
}

View File

@@ -4,8 +4,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import chat.simplex.common.simplexWindowState
import com.russhwolf.settings.*
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
@@ -49,6 +49,11 @@ actual fun screenWidth(): Dp {
return width*/
}// LALAL java.awt.Desktop.getDesktop()
actual fun desktopExpandWindowToWidth(width: Dp) {
if (simplexWindowState.windowState.size.width >= width) return
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width)
}
actual fun isRtl(text: CharSequence): Boolean {
if (text.isEmpty()) return false
return text.any { char ->

View File

@@ -0,0 +1,10 @@
package chat.simplex.common.ui.theme
import com.jthemedetecor.OsThemeDetector
private val detector: OsThemeDetector = OsThemeDetector.getDetector()
.apply {
registerListener(::reactOnDarkThemeChanges)
}
actual fun isSystemInDarkTheme(): Boolean = detector.isDark

View File

@@ -20,7 +20,7 @@ actual fun SimpleAndAnimatedImageView(
// LALAL make it animated too
ImageView(imageBitmap.toAwtImage().toPainter()) {
if (getLoadedFilePath(file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
}

View File

@@ -29,7 +29,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
m.controller.appPrefs.systemDarkTheme,
showSettingsModal = showSettingsModal,
editColor = { name, initialColor ->
ModalManager.shared.showModalCloseable { close ->
ModalManager.start.showModalCloseable { close ->
ColorEditor(name, initialColor, close)
}
},