diff --git a/packages/frontend/apps/android/App/app/build.gradle b/packages/frontend/apps/android/App/app/build.gradle index de49bc829..c86516448 100644 --- a/packages/frontend/apps/android/App/app/build.gradle +++ b/packages/frontend/apps/android/App/app/build.gradle @@ -128,6 +128,9 @@ dependencies { implementation libs.okhttp.logging implementation libs.okhttp.sse + implementation libs.richtext.commonmark + implementation libs.richtext.ui + testImplementation libs.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espresso.core diff --git a/packages/frontend/apps/android/App/app/src/main/AndroidManifest.xml b/packages/frontend/apps/android/App/app/src/main/AndroidManifest.xml index 230091596..ef052d7e1 100644 --- a/packages/frontend/apps/android/App/app/src/main/AndroidManifest.xml +++ b/packages/frontend/apps/android/App/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val messageUiState by viewModel.messagesUiState.collectAsStateWithLifecycle() + val sendBtnEnable by viewModel.sendBtnUiState.collectAsStateWithLifecycle() + + var previousMessageCount by remember { mutableIntStateOf(0) } + var isAtBottom by remember { mutableStateOf(true) } + + LaunchedEffect(messageUiState.messages) { + val currentMessageCount = messageUiState.messages.size + if (isAtBottom && currentMessageCount >= previousMessageCount) { + scrollState.animateScrollToItem(0) + } + previousMessageCount = currentMessageCount + } + + SideEffect { + isAtBottom = messageUiState.messages.isEmpty() + || scrollState.firstVisibleItemIndex == 0 + && scrollState.firstVisibleItemScrollOffset == 0 + } + Column( Modifier .fillMaxSize() .padding(paddingValues) ) { Box(Modifier.weight(1f)) { - with(uiState) { - when { - this is MessageUiState -> LazyColumn( - reverseLayout = true, - state = scrollState, - modifier = Modifier.fillMaxSize(), - ) { - items( - items = messages, - key = { it.id ?: "" }, - contentType = { it.role } - ) { message -> - Message(message) - } + with(messageUiState) { + LazyColumn( + reverseLayout = true, + state = scrollState, + modifier = Modifier.fillMaxSize(), + ) { + items( + items = messages, + key = { it.id ?: "" }, + contentType = { it.role } + ) { message -> + Message(message) } } } } - val context = LocalContext.current UserInput( + modifier = Modifier + .navigationBarsPadding() + .imePadding(), onMessageSent = { content -> - Toast.makeText(context, "Not implemented.", Toast.LENGTH_SHORT) - .show() -// viewModel.sendMessage(content) + viewModel.sendMessage(content) }, + sendMessageEnabled = sendBtnEnable, resetScroll = { scope.launch { scrollState.scrollToItem(0) } }, - modifier = Modifier - .navigationBarsPadding() - .imePadding() ) } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatUiState.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatUiState.kt index 1e4e3c53a..447381448 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatUiState.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatUiState.kt @@ -1,13 +1,19 @@ package app.affine.pro.ai.chat import com.affine.pro.graphql.GetCopilotHistoriesQuery +import kotlinx.datetime.Clock import kotlinx.datetime.Instant -sealed class ChatUiState +data class MessageUiState(val messages: List) { -data class MessageUiState( - val messages: List -) : ChatUiState() + fun addMessage(message: ChatMessage) = MessageUiState(messages = listOf(message) + messages) + + fun updateMessageAt(index: Int, transformer: (ChatMessage) -> ChatMessage) = MessageUiState( + messages = messages.toMutableList().apply { + add(index, transformer(removeAt(index))) + } + ) +} data class ChatMessage( val id: String?, @@ -27,11 +33,29 @@ data class ChatMessage( } companion object { + + const val AI_LOCAL_ID = "ai_local_id" + const val USER_LOCAL_ID = "user_local_id" + + fun newUserMessage(id: String, content: String) = ChatMessage( + id = id, + role = Role.User, + content = content, + createAt = Clock.System.now(), + ) + + fun newAIMessage() = ChatMessage( + id = AI_LOCAL_ID, + role = Role.AI, + content = "", + createAt = Clock.System.now(), + ) + fun from(message: GetCopilotHistoriesQuery.Message) = ChatMessage( id = message.id, role = Role.fromValue(message.role), content = message.content, - createAt = message.createdAt + createAt = message.createdAt, ) } } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt index f790e200e..28f2fbbcb 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt @@ -1,7 +1,9 @@ package app.affine.pro.ai.chat +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.affine.pro.AFFiNEApp import app.affine.pro.service.GraphQLService import app.affine.pro.service.SSEService import app.affine.pro.service.WebService @@ -9,9 +11,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -25,20 +30,28 @@ class ChatViewModel @Inject constructor( private lateinit var sessionId: String - private val _uiState: MutableStateFlow = + private val _messagesUiState: MutableStateFlow = MutableStateFlow(MessageUiState(emptyList())) - val uiState: StateFlow = _uiState + private val _sendBtnUiState: MutableStateFlow = MutableStateFlow(false) + + val messagesUiState: StateFlow = _messagesUiState + + val sendBtnUiState: StateFlow = _sendBtnUiState init { viewModelScope.launch { - sessionId = graphQLService.createCopilotSession( + sessionId = graphQLService.getCopilotSession( + webService.workspaceId(), + webService.docId() + ).getOrNull() ?: graphQLService.createCopilotSession( workspaceId = webService.workspaceId(), docId = webService.docId(), ).getOrElse { Timber.w(it, "Create session failed") return@launch } + _sendBtnUiState.value = true Timber.i("Create session success:[ sessionId = $sessionId].") val historyMessages = graphQLService.getCopilotHistories( workspaceId = webService.workspaceId(), @@ -49,40 +62,61 @@ class ChatViewModel @Inject constructor( }.sortedByDescending { it.createAt } - _uiState.value = MessageUiState(historyMessages) + _messagesUiState.value = MessageUiState(historyMessages) } } fun sendMessage(message: String) { - val sendMessage = suspend { - graphQLService.createCopilotMessage( + viewModelScope.launch { + _sendBtnUiState.value = false + val messageId = graphQLService.createCopilotMessage( sessionId = sessionId, message = message, - ).onSuccess { messageId -> - Timber.i("send message: $messageId") - sseService.messageStream(sessionId, messageId) - .onEach { - Timber.d("On sse message: ${it.getOrNull()}") + ).getOrElse { + Timber.e(it, "Send message fail.") + Toast.makeText(AFFiNEApp.context(), "Loading.", Toast.LENGTH_SHORT).show() + return@launch + } + Timber.i("Send message: $messageId") + _messagesUiState.update { + it.addMessage(ChatMessage.newUserMessage(messageId, message)) + } + sseService.messageStream(sessionId, messageId) + .onStart { + _messagesUiState.update { + it.addMessage(ChatMessage.newAIMessage()) + } + } + .onEach { + val event = it.getOrThrow() + Timber.d("On sse event: $event") + when (event.type) { + "message" -> _messagesUiState.update { state -> + state.updateMessageAt(0) { message -> + message.copy(content = message.content + event.data) + } + } + "error" -> Timber.e(event.data) + } + } + .catch { + Timber.e(it, "Receive message fail.") + } + .flowOn(Dispatchers.IO) + .collect() + val ids = graphQLService.getCopilotHistoryIds( + webService.workspaceId(), + webService.docId(), + sessionId, + ).getOrElse { emptyList() } + if (ids.isNotEmpty() && ids.size == _messagesUiState.value.messages.size) { + _messagesUiState.update { state -> + state.updateMessageAt(0) { message -> + message.copy(id = ids.last().id) } - .flowOn(Dispatchers.IO) - .collect() - } - } - viewModelScope.launch { - if (!this@ChatViewModel::sessionId.isInitialized) { - graphQLService.getCopilotSession( - workspaceId = webService.workspaceId(), - docId = webService.docId(), - ).onSuccess { id -> - sessionId = id - Timber.i("Create session: $id") - sendMessage() - }.onFailure { - Timber.e(it, "Create session failed.") } - } else { - sendMessage() } + _sendBtnUiState.value = true } } } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/ChatAppBar.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/ChatAppBar.kt index 50e22d6e5..6f19ca546 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/ChatAppBar.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/ChatAppBar.kt @@ -1,25 +1,21 @@ package app.affine.pro.ai.chat.ui -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChatBubble -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.MoreHoriz +import androidx.compose.foundation.layout.size import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import app.affine.pro.components.AffineAppBar -import app.affine.pro.components.AffineDropMenu +import androidx.compose.ui.unit.sp +import app.affine.pro.R +import app.affine.pro.components.AFFiNEAppBar +import app.affine.pro.components.AFFiNEDropMenu +import app.affine.pro.components.AFFiNEIcon @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -30,36 +26,33 @@ fun ChatAppBar( onClearHistory: () -> Unit = { }, onSaveAsChatBlock: () -> Unit = { }, ) { - AffineAppBar( + AFFiNEAppBar( modifier = modifier, scrollBehavior = scrollBehavior, onNavIconPressed = onBackClick, title = { Row( - modifier = Modifier.clickable { }, verticalAlignment = Alignment.CenterVertically ) { - Text("Chat with AI") - Spacer(Modifier.width(10.dp)) - Icon(imageVector = Icons.Default.KeyboardArrowDown, contentDescription = null) + Text("Chat with AI", fontSize = 17.sp, fontWeight = FontWeight.Bold) } }, actions = { - AffineDropMenu( - icon = { Icon(Icons.Default.MoreHoriz, contentDescription = "More actions") }, + AFFiNEDropMenu( + R.drawable.ic_more_horizontal, + modifier = Modifier.size(44.dp), menuItems = { DropdownMenuItem( text = { Text("Clear history") }, - trailingIcon = { Icon(Icons.Default.Delete, contentDescription = null) }, + trailingIcon = { + AFFiNEIcon(R.drawable.ic_broom) + }, onClick = onClearHistory, ) DropdownMenuItem( text = { Text("Save as chat block") }, trailingIcon = { - Icon( - Icons.Default.ChatBubble, - contentDescription = null - ) + AFFiNEIcon(R.drawable.ic_bubble) }, onClick = onSaveAsChatBlock, ) diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/Message.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/Message.kt index 57631c4ce..0c93c4bb0 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/Message.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/Message.kt @@ -7,23 +7,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.affine.pro.R import app.affine.pro.ai.chat.ChatMessage +import app.affine.pro.components.AFFiNEIcon +import app.affine.pro.components.Markdown +import app.affine.pro.theme.AFFiNETheme import kotlinx.datetime.Clock @Composable @@ -44,11 +43,9 @@ fun Message(message: ChatMessage) { .padding(8.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_ai), - contentDescription = null, - tint = Color(0XFF1E96EB), - modifier = Modifier.size(24.dp) + AFFiNEIcon( + R.drawable.ic_ai, + tint = AFFiNETheme.colors.iconActivated ) Spacer(Modifier.width(6.dp)) Text( @@ -56,17 +53,21 @@ fun Message(message: ChatMessage) { ChatMessage.Role.User -> "You" ChatMessage.Role.AI -> "Affine AI" }, - color = Color.White, + color = AFFiNETheme.colors.textPrimary, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, ) } Spacer(Modifier.height(8.dp)) - Text( - text = message.content, - color = Color.White, - fontSize = 16.sp, - ) + when (message.role) { + ChatMessage.Role.User -> Text( + text = message.content, + color = Color.White, + fontSize = 16.sp, + ) + + ChatMessage.Role.AI -> Markdown(markdown = message.content) + } } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/UserInput.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/UserInput.kt index e2993ccdc..22f7ed106 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/UserInput.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ui/UserInput.kt @@ -12,24 +12,15 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.outlined.CameraAlt -import androidx.compose.material.icons.outlined.InsertPhoto -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,7 +29,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager @@ -52,7 +42,10 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import app.affine.pro.theme.AffineTheme +import app.affine.pro.R +import app.affine.pro.components.AFFiNEIconButton +import app.affine.pro.theme.AFFiNETheme +import app.affine.pro.theme.ThemeMode enum class InputSelector { NONE, @@ -63,7 +56,7 @@ enum class InputSelector { @Preview @Composable fun UserInputPreview() { - AffineTheme(isDarkTheme = true) { + AFFiNETheme(ThemeMode.Dark) { UserInput(onMessageSent = {}) } } @@ -71,8 +64,9 @@ fun UserInputPreview() { @OptIn(ExperimentalFoundationApi::class) @Composable fun UserInput( - onMessageSent: (String) -> Unit, modifier: Modifier = Modifier, + onMessageSent: (String) -> Unit, + sendMessageEnabled: Boolean = true, resetScroll: () -> Unit = {}, ) { var currentInputSelector by rememberSaveable { mutableStateOf(InputSelector.NONE) } @@ -98,8 +92,7 @@ fun UserInput( tonalElevation = 2.dp, shadowElevation = 2.dp, shape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp), - color = MaterialTheme.colorScheme.surfaceContainer, - contentColor = MaterialTheme.colorScheme.onSurface, + color = AFFiNETheme.colors.backgroundOverlayPanel, ) { Column(modifier = modifier) { UserInputText( @@ -116,17 +109,15 @@ fun UserInput( onMessageSent = { onMessageSent(textState.text) textState = TextFieldValue() - resetScroll() }, focusState = textFieldFocusState ) UserInputSelector( onSelectorChange = { currentInputSelector = it }, - sendMessageEnabled = textState.text.isNotBlank(), + sendMessageEnabled = textState.text.isNotBlank() && sendMessageEnabled, onMessageSent = { onMessageSent(textState.text) textState = TextFieldValue() - resetScroll() dismissKeyboard() }, ) @@ -143,59 +134,30 @@ private fun UserInputSelector( ) { Row( modifier = modifier - .height(44.dp) .wrapContentHeight() .padding(10.dp), verticalAlignment = Alignment.CenterVertically ) { - IconButton( - modifier = Modifier.size(24.dp), + AFFiNEIconButton( + R.drawable.ic_camera, onClick = { onSelectorChange(InputSelector.CAMERA) }, - ) { - Icon( - imageVector = Icons.Outlined.CameraAlt, - contentDescription = "Camera", - ) - } - - Spacer(modifier = Modifier.width(14.dp)) - - IconButton( - modifier = Modifier.size(24.dp), - onClick = { onSelectorChange(InputSelector.PICTURE) }, - ) { - Icon( - imageVector = Icons.Outlined.InsertPhoto, - contentDescription = "Picture", - ) - } - - Spacer(modifier = Modifier.width(14.dp)) - - VerticalDivider(modifier = Modifier.height(10.dp)) - - Checkbox( - modifier = Modifier.scale(0.8f), - checked = true, - enabled = false, - onCheckedChange = {}, ) - Text(text = "Embed this doc") + Spacer(modifier = Modifier.width(14.dp)) + + AFFiNEIconButton( + R.drawable.ic_image, + onClick = { onSelectorChange(InputSelector.PICTURE) }, + ) Spacer(modifier = Modifier.weight(1f)) // Send button - IconButton( - modifier = Modifier.size(24.dp), + AFFiNEIconButton( + R.drawable.ic_send, enabled = sendMessageEnabled, onClick = onMessageSent, - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.Send, - contentDescription = "Send message", - ) - } + ) } } @@ -221,7 +183,6 @@ private fun UserInputText( ) { Box(Modifier.fillMaxSize()) { - UserInputTextField( textFieldValue, onTextChanged, @@ -251,7 +212,7 @@ private fun BoxScope.UserInputTextField( modifier: Modifier = Modifier ) { var lastFocusState by remember { mutableStateOf(false) } - val color = MaterialTheme.colorScheme.onSurfaceVariant + val color = AFFiNETheme.colors.textPrimary BasicTextField( value = textFieldValue, onValueChange = { onTextChanged(it) }, @@ -281,6 +242,7 @@ private fun BoxScope.UserInputTextField( .align(Alignment.CenterStart) .padding(start = 10.dp, end = 10.dp, top = 10.dp), text = "Feel free to input any text and see how AI responds. Give it a go!", + color = color ) } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AffineAppBar.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEAppBar.kt similarity index 59% rename from packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AffineAppBar.kt rename to packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEAppBar.kt index cd4c58aea..a7afe54b3 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AffineAppBar.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEAppBar.kt @@ -1,18 +1,15 @@ package app.affine.pro.components +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.RowScope -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBackIos -import androidx.compose.material.icons.filled.MoreHoriz +import androidx.compose.foundation.layout.size import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -21,11 +18,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import app.affine.pro.theme.AffineTheme +import androidx.compose.ui.unit.dp +import app.affine.pro.R +import app.affine.pro.theme.AFFiNETheme +import app.affine.pro.theme.ThemeMode @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AffineAppBar( +fun AFFiNEAppBar( modifier: Modifier = Modifier, scrollBehavior: TopAppBarScrollBehavior? = null, onNavIconPressed: () -> Unit = { }, @@ -37,30 +37,33 @@ fun AffineAppBar( actions = actions, title = title, scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors().copy( + containerColor = AFFiNETheme.colors.backgroundPrimary, + titleContentColor = AFFiNETheme.colors.textPrimary, + ), navigationIcon = { - IconButton( + AFFiNEIconButton( + R.drawable.ic_close, + modifier = Modifier.size(44.dp), onClick = onNavIconPressed - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBackIos, - contentDescription = "Back", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + ) } ) } @Composable -fun AffineDropMenu( - icon: @Composable () -> Unit, +fun AFFiNEDropMenu( + @DrawableRes resId: Int, + modifier: Modifier, menuItems: @Composable ColumnScope.() -> Unit = {} ) { var expanded by remember { mutableStateOf(false) } Box { - IconButton(onClick = { expanded = !expanded }) { - icon() - } + AFFiNEIconButton( + resId, + modifier = modifier, + onClick = { expanded = !expanded }, + ) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } @@ -73,16 +76,12 @@ fun AffineDropMenu( @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable -fun AffineAppBarPreview() { - AffineTheme { - AffineAppBar( +fun AffineAppBarPreviewLight() { + AFFiNETheme(mode = ThemeMode.Light) { + AFFiNEAppBar( title = { Text("Preview!") }, actions = { - AffineDropMenu( - icon = { - Icon(Icons.Default.MoreHoriz, contentDescription = "Actions") - }, - ) + AFFiNEDropMenu(R.drawable.ic_more_horizontal, Modifier.size(44.dp)) }, ) } @@ -92,15 +91,11 @@ fun AffineAppBarPreview() { @Preview @Composable fun AffineAppBarPreviewDark() { - AffineTheme(isDarkTheme = true) { - AffineAppBar( + AFFiNETheme(mode = ThemeMode.Dark) { + AFFiNEAppBar( title = { Text("Preview!") }, actions = { - AffineDropMenu( - icon = { - Icon(Icons.Default.MoreHoriz, contentDescription = "Actions") - }, - ) + AFFiNEDropMenu(R.drawable.ic_more_horizontal, Modifier.size(44.dp)) }, ) } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEIcon.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEIcon.kt new file mode 100644 index 000000000..90c94b8ec --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/AFFiNEIcon.kt @@ -0,0 +1,47 @@ +package app.affine.pro.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import app.affine.pro.theme.AFFiNETheme + +@Composable +fun AFFiNEIcon( + @DrawableRes resId: Int, + modifier: Modifier = Modifier, + tint: Color = AFFiNETheme.colors.iconPrimary, + contentDescription: String? = null, +) { + Icon( + painter = painterResource(resId), + tint = tint, + contentDescription = contentDescription, + modifier = modifier.size(24.dp) + ) +} + +@Composable +fun AFFiNEIconButton( + @DrawableRes resId: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors().copy( + contentColor = AFFiNETheme.colors.iconPrimary, + disabledContentColor = AFFiNETheme.colors.iconDisable, + ), + interactionSource: MutableInteractionSource? = null, +) { + IconButton(onClick, modifier.size(24.dp), enabled, colors, interactionSource) { + AFFiNEIcon(resId) + } +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/Markdown.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/Markdown.kt new file mode 100644 index 000000000..d0bc8a7e6 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/components/Markdown.kt @@ -0,0 +1,223 @@ +package app.affine.pro.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.affine.pro.R +import app.affine.pro.theme.AFFiNETheme +import app.affine.pro.theme.ThemeMode +import com.halilibo.richtext.commonmark.Markdown +import com.halilibo.richtext.ui.BasicRichText +import com.halilibo.richtext.ui.BlockQuoteGutter +import com.halilibo.richtext.ui.CodeBlockStyle +import com.halilibo.richtext.ui.HeadingStyle +import com.halilibo.richtext.ui.ListStyle +import com.halilibo.richtext.ui.OrderedMarkers +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.RichTextThemeProvider +import com.halilibo.richtext.ui.TableStyle +import com.halilibo.richtext.ui.UnorderedMarkers +import com.halilibo.richtext.ui.string.RichTextStringStyle + +private val LocalMarkdownTextStyle = staticCompositionLocalOf { + TextStyle( + fontSize = 16.sp, + lineHeight = 21.sp, + fontWeight = FontWeight.Normal, + ) +} + +@Composable +fun Markdown( + modifier: Modifier = Modifier, + markdown: String, +) { + RichTextThemeProvider( + contentColorProvider = { AFFiNETheme.colors.textPrimary }, + textStyleProvider = { LocalMarkdownTextStyle.current }, + textStyleBackProvider = { textStyle, content -> + CompositionLocalProvider(LocalMarkdownTextStyle provides textStyle, content) + } + ) { + val dividerColor = AFFiNETheme.colors.divider + BasicRichText( + modifier = modifier, + style = RichTextStyle( + headingStyle = headingStyle, + listStyle = listStyle, + blockQuoteGutter = BlockQuoteGutter.BarGutter( + startMargin = 0.sp, + barWidth = 5.sp, + endMargin = 8.sp, + color = { dividerColor }, + ), + codeBlockStyle = CodeBlockStyle( + textStyle = AFFiNETheme.typography.body.copy( + fontSize = 14.sp, fontFamily = FontFamily.Monospace + ), + modifier = Modifier + .fillMaxWidth(1f) + .background( + color = AFFiNETheme.colors.backgroundCodeBlock, + shape = RoundedCornerShape(8.dp), + ), + padding = 16.sp, + wordWrap = true, + ), + tableStyle = TableStyle(borderColor = dividerColor, borderStrokeWidth = 2.dp.value), + stringStyle = RichTextStringStyle( + linkStyle = TextLinkStyles( + SpanStyle(color = AFFiNETheme.colors.textEmphasis), + SpanStyle(color = AFFiNETheme.colors.textEmphasis), + SpanStyle(color = AFFiNETheme.colors.textEmphasis), + SpanStyle(color = AFFiNETheme.colors.textEmphasis), + ), + codeStyle = SpanStyle( + fontSize = 13.sp, + baselineShift = BaselineShift(0.08f), + background = AFFiNETheme.colors.backgroundCodeBlock, + ) + ) + ), + ) { + Markdown(markdown) + } + } +} + +private val headingStyle: HeadingStyle = { level, textStyle -> + when (level) { + 0 -> TextStyle( + fontSize = 28.sp, lineHeight = 34.sp, fontWeight = FontWeight.SemiBold + ) + + 1 -> TextStyle( + fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.SemiBold + ) + + 2 -> TextStyle( + fontSize = 20.sp, lineHeight = 25.sp, fontWeight = FontWeight.SemiBold + ) + + 3 -> TextStyle( + fontSize = 17.sp, lineHeight = 22.sp, fontWeight = FontWeight.SemiBold + ) + + 4 -> TextStyle( + fontSize = 17.sp, lineHeight = 22.sp, fontWeight = FontWeight.SemiBold + ) + + else -> textStyle.copy(fontWeight = FontWeight.SemiBold) + } +} + +private val listStyle = ListStyle(orderedMarkers = { + OrderedMarkers { _, index -> + Text( + "${index + 1}.", + modifier = Modifier + .width(24.dp) + .padding(start = 4.dp), + style = AFFiNETheme.typography.body, + color = AFFiNETheme.colors.textEmphasis, + ) + } +}, unorderedMarkers = { + val markers = listOf( + R.drawable.ic_bulleted_list_01, + R.drawable.ic_bulleted_list_02, + R.drawable.ic_bulleted_list_03, + R.drawable.ic_bulleted_list_04, + ) + UnorderedMarkers { level -> + AFFiNEIcon(markers[level % markers.size], tint = AFFiNETheme.colors.textEmphasis) + } +}) + +@Preview +@Composable +fun MarkdownPreview() { + AFFiNETheme(mode = ThemeMode.Dark) { + Markdown( + markdown = """ + + + 当然可以,大熊!下面是一个包含**所有常用 Markdown 格式**的示例内容,您可以直接复制到 AFFiNE 或其他支持 Markdown 的编辑器中体验效果: + + --- + + # 这是一级标题 + + ## 这是二级标题 + + ### 这是三级标题 + + --- + + **加粗文本***斜体文本*~~删除线文本~~`行内代码` + + --- + + > 这是一个引用块,可以用来高亮重要信息。 + + --- + + - 无序列表项 1 + - 无序列表项 2 + - 嵌套子项 + - 嵌套子项 + - 嵌套子项 + + 1. 有序列表项 1 + 1. 有序列表项 1-1 + 2. 有序列表项 1-2 + 2. 有序列表项 2 + + --- + + | 姓名 | 年龄 | 爱好 | + | ------ | ---- | -------- | + | 大熊 | 28 | 阅读 | + | 阿芬 | ∞ | 帮助你 | + + --- + + ```kotlin + // 这是一个 Kotlin 代码块 + fun main() { + println(\"Hello, Markdown!\") + } + ``` + + --- + + ![示例图片](https://affine.pro/_next/static/media/logo.1e7b6b7e.svg) + + --- + + [这是一个链接,点我访问 AFFiNE 官网](https://affine.pro) + + --- + + 大熊,如果还想体验更多格式或者有特殊内容需求,随时告诉阿芬! + + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AFFiNEThemePlugin.kt similarity index 95% rename from packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt rename to packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AFFiNEThemePlugin.kt index dd765a9fc..ebaa0a4b6 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AFFiNEThemePlugin.kt @@ -8,7 +8,7 @@ import com.getcapacitor.annotation.CapacitorPlugin import timber.log.Timber @CapacitorPlugin(name = "AffineTheme") -class AffineThemePlugin : Plugin() { +class AFFiNEThemePlugin : Plugin() { interface Callback { fun onThemeChanged(darkMode: Boolean) diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt index 7becc0b55..f9d5c446b 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt @@ -1,7 +1,6 @@ package app.affine.pro.plugin import android.annotation.SuppressLint -import app.affine.pro.CapacitorConfig import app.affine.pro.service.CookieStore import app.affine.pro.service.OkHttp import com.getcapacitor.JSObject @@ -46,7 +45,6 @@ class AuthPlugin : Plugin() { val endpoint = call.getStringEnsure("endpoint") val request = Request.Builder() .url("$endpoint/api/auth/sign-out") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) .get() .build() OkHttp.client.newCall(request).executeAsync().use { response -> @@ -88,7 +86,6 @@ class AuthPlugin : Plugin() { val requestBuilder = Request.Builder() .url("$endpoint/api/auth/sign-in") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) .post(body) if (verifyToken != null) { requestBuilder.addHeader("x-captcha-token", verifyToken) @@ -114,7 +111,6 @@ class AuthPlugin : Plugin() { Request.Builder() .url("$endpoint/api/oauth/callback") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) .post(body) .build() } @@ -134,7 +130,6 @@ class AuthPlugin : Plugin() { Request.Builder() .url("$endpoint/api/auth/magic-link") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) .post(body) .build() } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/GraphQLService.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/GraphQLService.kt index 7cbf7b3cc..eb7af6382 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/GraphQLService.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/GraphQLService.kt @@ -5,9 +5,11 @@ import app.affine.pro.utils.getCurrentServerBaseUrl import com.affine.pro.graphql.CreateCopilotMessageMutation import com.affine.pro.graphql.CreateCopilotSessionMutation import com.affine.pro.graphql.GetCopilotHistoriesQuery +import com.affine.pro.graphql.GetCopilotHistoryIdsQuery import com.affine.pro.graphql.GetCopilotSessionsQuery import com.affine.pro.graphql.type.CreateChatMessageInput import com.affine.pro.graphql.type.CreateChatSessionInput +import com.affine.pro.graphql.type.QueryChatSessionsInput import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Mutation import com.apollographql.apollo.api.Optional @@ -26,10 +28,13 @@ class GraphQLService @Inject constructor() { suspend fun getCopilotSession(workspaceId: String, docId: String) = query( GetCopilotSessionsQuery( workspaceId = workspaceId, - docId = Optional.present(docId) + docId = Optional.present(docId), + options = Optional.present(QueryChatSessionsInput(action = Optional.present(false))) ) ).mapCatching { data -> - data.currentUser?.copilot?.sessions?.firstOrNull()?.id ?: error(ERROR_NULL_SESSION_ID) + data.currentUser?.copilot?.sessions?.find { + it.parentSessionId == null + }?.id ?: error(ERROR_NULL_SESSION_ID) } suspend fun createCopilotSession( @@ -63,6 +68,21 @@ class GraphQLService @Inject constructor() { }?.messages ?: emptyList() } + suspend fun getCopilotHistoryIds( + workspaceId: String, + docId: String, + sessionId: String, + ) = query( + GetCopilotHistoryIdsQuery( + workspaceId = workspaceId, + docId = Optional.present(docId), + ) + ).mapCatching { data -> + data.currentUser?.copilot?.histories?.firstOrNull { history -> + history.sessionId == sessionId + }?.messages ?: emptyList() + } + suspend fun createCopilotMessage( sessionId: String, message: String, diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt index fad34f8b4..cb3bfbc24 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt @@ -1,6 +1,7 @@ package app.affine.pro.service -import app.affine.pro.AffineApp +import app.affine.pro.AFFiNEApp +import app.affine.pro.CapacitorConfig import app.affine.pro.utils.dataStore import app.affine.pro.utils.set import com.google.firebase.crashlytics.ktx.crashlytics @@ -32,6 +33,14 @@ object OkHttp { CookieStore.saveCookies(url.host, cookies) } }) + .addInterceptor { + it.proceed( + it.request() + .newBuilder() + .addHeader("x-affine-version", CapacitorConfig.getAffineVersion()) + .build() + ) + } .addInterceptor(HttpLoggingInterceptor { msg -> Timber.d(msg) }.apply { @@ -52,11 +61,11 @@ object CookieStore { _cookies[host] = cookies MainScope().launch(Dispatchers.IO) { cookies.find { it.name == AFFINE_SESSION }?.let { - AffineApp.context().dataStore.set(host + AFFINE_SESSION, it.toString()) + AFFiNEApp.context().dataStore.set(host + AFFINE_SESSION, it.toString()) } cookies.find { it.name == AFFINE_USER_ID }?.let { Timber.d("Update user id [${it.value}]") - AffineApp.context().dataStore.set(host + AFFINE_USER_ID, it.toString()) + AFFiNEApp.context().dataStore.set(host + AFFINE_USER_ID, it.toString()) Firebase.crashlytics.setUserId(it.value) } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Color.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Color.kt deleted file mode 100644 index 5562e946e..000000000 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Color.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.affine.pro.theme - -import androidx.compose.ui.graphics.Color - -object Dark { - val BackgroundPrimary = Color(0XFF000000) - val InverseBackgroundPrimary = Color(0XFFFFFFFF) - val IconPrimary = Color(0XFFF3F3F3) - val Surface = Color(0XFF2A2A2A) - val TextPrimary = Color(0XFFE6E6E6) -} - -object Light { - val BackgroundPrimary = Color(0XFFFFFFFF) - val InverseBackgroundPrimary = Color(0XFF000000) -} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Colors.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Colors.kt new file mode 100644 index 000000000..299f213d7 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Colors.kt @@ -0,0 +1,344 @@ +package app.affine.pro.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +@Immutable +data class AFFiNEColorScheme( + val textPrimary: Color, + val textSecondary: Color, + val textDisable: Color, + val textEmphasis: Color, + val backgroundPrimary: Color, + val backgroundSecondary: Color, + val backgroundOverlayPanel: Color, + val backgroundTertiary: Color, + val backgroundCodeBlock: Color, + val backgroundModal: Color, + val backgroundSuccess: Color, + val backgroundError: Color, + val backgroundWarning: Color, + val backgroundProgressing: Color, + val iconPrimary: Color, + val iconSecondary: Color, + val iconTertiary: Color, + val iconDisable: Color, + val iconActivated: Color, + val divider: Color, +) + +val affineLightScheme = AFFiNEColorScheme( + textPrimary = AFFiNEColorTokens.Grey900, + textSecondary = AFFiNEColorTokens.Grey600, + textDisable = AFFiNEColorTokens.Grey400, + textEmphasis = AFFiNEColorTokens.AFFiNE600, + backgroundPrimary = AFFiNEColorTokens.BaseWhite, + backgroundSecondary = AFFiNEColorTokens.Grey100, + backgroundOverlayPanel = AFFiNEColorTokens.Grey50, + backgroundTertiary = AFFiNEColorTokens.Grey300, + backgroundCodeBlock = AFFiNEColorTokens.Grey50, + backgroundModal = AFFiNEColorTokens.TransparentBlack700, + backgroundSuccess = AFFiNEColorTokens.Emerald50, + backgroundError = AFFiNEColorTokens.Rose50, + backgroundWarning = AFFiNEColorTokens.Orange25, + backgroundProgressing = AFFiNEColorTokens.Indigo50, + iconPrimary = AFFiNEColorTokens.Grey600, + iconSecondary = AFFiNEColorTokens.Grey400, + iconTertiary = AFFiNEColorTokens.Grey300, + iconDisable = AFFiNEColorTokens.Grey400, + iconActivated = AFFiNEColorTokens.AFFiNE600, + divider = AFFiNEColorTokens.TransparentGrey400, +) + +val affineDarkScheme = AFFiNEColorScheme( + textPrimary = AFFiNEColorTokens.Grey200, + textSecondary = AFFiNEColorTokens.Grey500, + textDisable = AFFiNEColorTokens.Grey700, + textEmphasis = AFFiNEColorTokens.AFFiNE500, + backgroundPrimary = AFFiNEColorTokens.Grey950, + backgroundSecondary = AFFiNEColorTokens.Grey900, + backgroundOverlayPanel = AFFiNEColorTokens.Grey900, + backgroundTertiary = AFFiNEColorTokens.Grey700, + backgroundCodeBlock = AFFiNEColorTokens.Grey900, + backgroundModal = AFFiNEColorTokens.TransparentBlack400, + backgroundSuccess = AFFiNEColorTokens.Emerald950, + backgroundError = AFFiNEColorTokens.Rose950, + backgroundWarning = AFFiNEColorTokens.Orange950, + backgroundProgressing = AFFiNEColorTokens.Indigo950, + iconPrimary = AFFiNEColorTokens.Grey100, + iconSecondary = AFFiNEColorTokens.Grey300, + iconTertiary = AFFiNEColorTokens.Grey700, + iconDisable = AFFiNEColorTokens.Grey800, + iconActivated = AFFiNEColorTokens.AFFiNE600, + divider = AFFiNEColorTokens.TransparentGrey300, +) + +val LocalAFFiNEColors = staticCompositionLocalOf { affineLightScheme } + +object AFFiNEColorTokens { + val BaseWhite = Color(0XFFFFFFFF) + val BaseBlack = Color(0XFF000000) + + val Grey25 = Color(0XFFF9F9F9) + val Grey50 = Color(0XFFF5F5F5) + val Grey100 = Color(0XFFF3F3F3) + val Grey200 = Color(0XFFE6E6E6) + val Grey300 = Color(0XFFCDCDCD) + val Grey400 = Color(0XFFB3B3B3) + val Grey500 = Color(0XFF929292) + val Grey600 = Color(0XFF7A7A7A) + val Grey700 = Color(0XFF565656) + val Grey800 = Color(0XFF414141) + val Grey900 = Color(0XFF252525) + val Grey950 = Color(0XFF141414) + + val Red25 = Color(0XFFFFF4F5) + val Red50 = Color(0XFFFCE5E6) + val Red100 = Color(0XFFFFD1D1) + val Red200 = Color(0XFFFBB7B7) + val Red300 = Color(0XFFFF9A9A) + val Red400 = Color(0XFFFC7979) + val Red500 = Color(0XFFF45252) + val Red600 = Color(0XFFED3F3F) + val Red700 = Color(0XFFC83030) + val Red800 = Color(0XFF9F2D2D) + val Red900 = Color(0XFF761717) + val Red950 = Color(0XFF460606) + + val Orange25 = Color(0XFFFFF7EE) + val Orange50 = Color(0XFFFFEBD5) + val Orange100 = Color(0XFFFFDDB5) + val Orange200 = Color(0XFFFFD3AB) + val Orange300 = Color(0XFFFFC58F) + val Orange400 = Color(0XFFFFB978) + val Orange500 = Color(0XFFFFAD63) + val Orange600 = Color(0XFFFF8C39) + val Orange700 = Color(0XFFF37C26) + val Orange800 = Color(0XFFC96317) + val Orange900 = Color(0XFFA65113) + val Orange950 = Color(0XFF843A06) + + val Amber25 = Color(0XFFFDFCF4) + val Amber50 = Color(0XFFFFFBEB) + val Amber100 = Color(0XFFFEF3C7) + val Amber200 = Color(0XFFFCE68A) + val Amber300 = Color(0XFFFCD34D) + val Amber400 = Color(0XFFFABF24) + val Amber500 = Color(0XFFF59E0A) + val Amber600 = Color(0XFFD97705) + val Amber700 = Color(0XFFB55309) + val Amber800 = Color(0XFF92400F) + val Amber900 = Color(0XFF78350F) + val Amber950 = Color(0XFF461A02) + + val Yellow25 = Color(0XFFFFFAEC) + val Yellow50 = Color(0XFFFBF3D8) + val Yellow100 = Color(0XFFFFEFB6) + val Yellow200 = Color(0XFFFFE898) + val Yellow300 = Color(0XFFFEE483) + val Yellow400 = Color(0XFFFFDE6B) + val Yellow500 = Color(0XFFFEDC61) + val Yellow600 = Color(0XFFFFD337) + val Yellow700 = Color(0XFFE0AA00) + val Yellow800 = Color(0XFFAC7400) + val Yellow900 = Color(0XFF704200) + val Yellow950 = Color(0XFF321A04) + + val Lime25 = Color(0XFFFBFEF1) + val Lime50 = Color(0XFFF9FEE7) + val Lime100 = Color(0XFFF0FCCB) + val Lime200 = Color(0XFFE0F99D) + val Lime300 = Color(0XFFC9F263) + val Lime400 = Color(0XFFB2E634) + val Lime500 = Color(0XFF92CC16) + val Lime600 = Color(0XFF72A10E) + val Lime700 = Color(0XFF577C0E) + val Lime800 = Color(0XFF466213) + val Lime900 = Color(0XFF3B5314) + val Lime950 = Color(0XFF1A2E06) + + val Green25 = Color(0XFFEFFFEC) + val Green50 = Color(0XFFDCFDD7) + val Green100 = Color(0XFFC9F8C1) + val Green200 = Color(0XFFB8F0AF) + val Green300 = Color(0XFFA8E79C) + val Green400 = Color(0XFF9CDA91) + val Green500 = Color(0XFF91CE87) + val Green600 = Color(0XFF7CC270) + val Green700 = Color(0XFF64BA56) + val Green800 = Color(0XFF439036) + val Green900 = Color(0XFF225C18) + val Green950 = Color(0XFF10340A) + + val Emerald25 = Color(0XFFEDFDF5) + val Emerald50 = Color(0XFFF1FDF4) + val Emerald100 = Color(0XFFDDFAE3) + val Emerald200 = Color(0XFFBEF3CC) + val Emerald300 = Color(0XFF9DE6B4) + val Emerald400 = Color(0XFF7FD295) + val Emerald500 = Color(0XFF69B87D) + val Emerald600 = Color(0XFF539566) + val Emerald700 = Color(0XFF447756) + val Emerald800 = Color(0XFF375F46) + val Emerald900 = Color(0XFF2E4F3C) + val Emerald950 = Color(0XFF1D3027) + + val Teal25 = Color(0XFFEFFFFD) + val Teal50 = Color(0XFFE1FDF9) + val Teal100 = Color(0XFFD7FBF6) + val Teal200 = Color(0XFFC6F8F2) + val Teal300 = Color(0XFFB5F5EE) + val Teal400 = Color(0XFFA7F1E9) + val Teal500 = Color(0XFF8AE7DD) + val Teal600 = Color(0XFF7AE1D5) + val Teal700 = Color(0XFF5CC7BA) + val Teal800 = Color(0XFF448E86) + val Teal900 = Color(0XFF1C6B63) + val Teal950 = Color(0XFF0E4841) + + val Blue25 = Color(0XFFF0F9FF) + val Blue50 = Color(0XFFE6F5FF) + val Blue100 = Color(0XFFDAF0FF) + val Blue200 = Color(0XFFCEECFF) + val Blue300 = Color(0XFFBAE4FF) + val Blue400 = Color(0XFFAADEFF) + val Blue500 = Color(0XFFA0D9FF) + val Blue600 = Color(0XFF84CFFF) + val Blue700 = Color(0XFF53B2EF) + val Blue800 = Color(0XFF2F94D5) + val Blue900 = Color(0XFF1C70A5) + val Blue950 = Color(0XFF004B7B) + + val AFFiNE25 = Color(0XFFE2F4FF) + val AFFiNE50 = Color(0XFFCAE9FF) + val AFFiNE100 = Color(0XFF8FD1FF) + val AFFiNE200 = Color(0XFF79C8FF) + val AFFiNE300 = Color(0XFF5EBCFF) + val AFFiNE400 = Color(0XFF49B1FA) + val AFFiNE500 = Color(0XFF29A2FA) + val AFFiNE600 = Color(0XFF1D96EB) + val AFFiNE700 = Color(0XFF158ADE) + val AFFiNE800 = Color(0XFF035F9F) + val AFFiNE900 = Color(0XFF003C67) + val AFFiNE950 = Color(0XFF002742) + + val Indigo25 = Color(0XFFF5F7FF) + val Indigo50 = Color(0XFFEEF2FF) + val Indigo100 = Color(0XFFE0E7FF) + val Indigo200 = Color(0XFFC7D2FE) + val Indigo300 = Color(0XFFA5B4FC) + val Indigo400 = Color(0XFF818CF8) + val Indigo500 = Color(0XFF6366F1) + val Indigo600 = Color(0XFF4F46E5) + val Indigo700 = Color(0XFF4338CA) + val Indigo800 = Color(0XFF3730A3) + val Indigo900 = Color(0XFF312E81) + val Indigo950 = Color(0XFF1E1B4B) + + val Violet25 = Color(0XFFF9F7FF) + val Violet50 = Color(0XFFF5F3FF) + val Violet100 = Color(0XFFEDE9FE) + val Violet200 = Color(0XFFDDD6FE) + val Violet300 = Color(0XFFC5B5FD) + val Violet400 = Color(0XFFA78BFA) + val Violet500 = Color(0XFF8B5CF6) + val Violet600 = Color(0XFF7C3AED) + val Violet700 = Color(0XFF6E28D9) + val Violet800 = Color(0XFF5A21B6) + val Violet900 = Color(0XFF4B1E95) + val Violet950 = Color(0XFF2E1065) + + val Purple25 = Color(0XFFF0ECFF) + val Purple50 = Color(0XFFDED6FF) + val Purple100 = Color(0XFFCBBEFF) + val Purple200 = Color(0XFFB5A5EF) + val Purple300 = Color(0XFFA593F3) + val Purple400 = Color(0XFF9681EF) + val Purple500 = Color(0XFF846CE9) + val Purple600 = Color(0XFF6E52DF) + val Purple700 = Color(0XFF5739D1) + val Purple800 = Color(0XFF4A2EBC) + val Purple900 = Color(0XFF321994) + val Purple950 = Color(0XFF25136D) + + val Fuchsia25 = Color(0XFFFEFAFF) + val Fuchsia50 = Color(0XFFFCF4FF) + val Fuchsia100 = Color(0XFFFAE8FF) + val Fuchsia200 = Color(0XFFF5D0FE) + val Fuchsia300 = Color(0XFFF0AAFC) + val Fuchsia400 = Color(0XFFE879F9) + val Fuchsia500 = Color(0XFFD946EF) + val Fuchsia600 = Color(0XFFC025D3) + val Fuchsia700 = Color(0XFFA21EAF) + val Fuchsia800 = Color(0XFF86198F) + val Fuchsia900 = Color(0XFF701A75) + val Fuchsia950 = Color(0XFF4A044E) + + val Magenta25 = Color(0XFFFFECF6) + val Magenta50 = Color(0XFFFFDAED) + val Magenta100 = Color(0XFFFFC0E0) + val Magenta200 = Color(0XFFFCA2D0) + val Magenta300 = Color(0XFFF58EC3) + val Magenta400 = Color(0XFFF37FBA) + val Magenta500 = Color(0XFFE96CAA) + val Magenta600 = Color(0XFFE660A4) + val Magenta700 = Color(0XFFCC4187) + val Magenta800 = Color(0XFFBA2B72) + val Magenta900 = Color(0XFFA91E65) + val Magenta950 = Color(0XFF89124F) + + val Rose25 = Color(0XFFFFFAFA) + val Rose50 = Color(0XFFFFF1F1) + val Rose100 = Color(0XFFFEE4E5) + val Rose200 = Color(0XFFFECDD1) + val Rose300 = Color(0XFFFDA2AB) + val Rose400 = Color(0XFFFB7181) + val Rose500 = Color(0XFFF43F48) + val Rose600 = Color(0XFFE11E41) + val Rose700 = Color(0XFFBE1237) + val Rose800 = Color(0XFF9F1235) + val Rose900 = Color(0XFF881331) + val Rose950 = Color(0XFF4D051A) + + val TransparentWhite25 = Color(0XFFFFFFFF).copy(alpha = 0.03f) + val TransparentWhite50 = Color(0XFFFFFFFF).copy(alpha = 0.05f) + val TransparentWhite100 = Color(0XFFFFFFFF).copy(alpha = 0.09f) + val TransparentWhite200 = Color(0XFFFFFFFF).copy(alpha = 0.13f) + val TransparentWhite300 = Color(0XFFFFFFFF).copy(alpha = 0.17f) + val TransparentWhite400 = Color(0XFFFFFFFF).copy(alpha = 0.23f) + val TransparentWhite500 = Color(0XFFFFFFFF).copy(alpha = 0.56f) + val TransparentWhite600 = Color(0XFFFFFFFF).copy(alpha = 0.67f) + val TransparentWhite700 = Color(0XFFFFFFFF).copy(alpha = 0.72f) + val TransparentWhite800 = Color(0XFFFFFFFF).copy(alpha = 0.82f) + val TransparentWhite900 = Color(0XFFFFFFFF).copy(alpha = 0.90f) + val TransparentWhite950 = Color(0XFFFFFFFF).copy(alpha = 0.98f) + + val TransparentBlack25 = Color(0XFF000000).copy(alpha = 0.03f) + val TransparentBlack50 = Color(0XFF000000).copy(alpha = 0.05f) + val TransparentBlack100 = Color(0XFF000000).copy(alpha = 0.07f) + val TransparentBlack200 = Color(0XFF000000).copy(alpha = 0.10f) + val TransparentBlack300 = Color(0XFF000000).copy(alpha = 0.17f) + val TransparentBlack400 = Color(0XFF000000).copy(alpha = 0.22f) + val TransparentBlack500 = Color(0XFF000000).copy(alpha = 0.52f) + val TransparentBlack600 = Color(0XFF000000).copy(alpha = 0.65f) + val TransparentBlack700 = Color(0XFF000000).copy(alpha = 0.70f) + val TransparentBlack800 = Color(0XFF000000).copy(alpha = 0.80f) + val TransparentBlack900 = Color(0XFF000000).copy(alpha = 0.90f) + val TransparentBlack950 = Color(0XFF000000).copy(alpha = 0.95f) + + val TransparentGrey25 = Color(0XFF929292).copy(alpha = 0.03f) + val TransparentGrey50 = Color(0XFF929292).copy(alpha = 0.05f) + val TransparentGrey100 = Color(0XFF929292).copy(alpha = 0.07f) + val TransparentGrey200 = Color(0XFF929292).copy(alpha = 0.10f) + val TransparentGrey300 = Color(0XFF929292).copy(alpha = 0.17f) + val TransparentGrey400 = Color(0XFF929292).copy(alpha = 0.22f) + val TransparentGrey500 = Color(0XFF929292).copy(alpha = 0.52f) + val TransparentGrey600 = Color(0XFF929292).copy(alpha = 0.65f) + val TransparentGrey700 = Color(0XFF929292).copy(alpha = 0.70f) + val TransparentGrey800 = Color(0XFF929292).copy(alpha = 0.80f) + val TransparentGrey900 = Color(0XFF929292).copy(alpha = 0.90f) + val TransparentGrey950 = Color(0XFF929292).copy(alpha = 0.95f) + + +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Theme.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Theme.kt index ef249affe..36854e34c 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Theme.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Theme.kt @@ -1,31 +1,49 @@ package app.affine.pro.theme -import android.annotation.SuppressLint import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable -val AffineDarkColorScheme = darkColorScheme( - background = Dark.BackgroundPrimary, - onSurface = Dark.TextPrimary, - onSurfaceVariant = Dark.IconPrimary, - surfaceContainer = Dark.Surface, - surface = Dark.BackgroundPrimary, - inverseSurface = Dark.InverseBackgroundPrimary, -) -val AffineLightColorScheme = lightColorScheme() +object AFFiNETheme { + val colors: AFFiNEColorScheme + @ReadOnlyComposable + @Composable + get() = LocalAFFiNEColors.current + + val typography: AFFiNETypography + @ReadOnlyComposable + @Composable + get() = LocalAFFiNETypography.current +} -@SuppressLint("NewApi") @Composable -fun AffineTheme( - isDarkTheme: Boolean = isSystemInDarkTheme(), +fun AFFiNETheme( + mode: ThemeMode = ThemeMode.System, content: @Composable () -> Unit ) { - MaterialTheme( - colorScheme = if (isDarkTheme) AffineDarkColorScheme else AffineLightColorScheme, - typography = AffineTypography, - content = content - ) + val colors = when (mode) { + ThemeMode.Light -> affineLightScheme + ThemeMode.Dark -> affineDarkScheme + ThemeMode.System -> if (isSystemInDarkTheme()) affineDarkScheme else affineLightScheme + } + + CompositionLocalProvider(LocalAFFiNEColors provides colors) { + MaterialTheme { + content() + } + } +} + +enum class ThemeMode(name: String) { + Light("light"), + Dark("dark"), + System("system"); + + fun of(name: String) = when (name) { + "light" -> Light + "dark" -> Dark + else -> System + } } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Typography.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Typography.kt index 61a0f175f..2cfb7af07 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Typography.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/theme/Typography.kt @@ -1,5 +1,58 @@ package app.affine.pro.theme -import androidx.compose.material3.Typography +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp -val AffineTypography = Typography() \ No newline at end of file +@Immutable +data class AFFiNETypography( + val body: TextStyle, + val h1: TextStyle, + val h2: TextStyle, + val h3: TextStyle, + val h4: TextStyle, + val h5: TextStyle, + val h6: TextStyle, +) + +val affineTypography = AFFiNETypography( + body = TextStyle( + fontSize = 16.sp, + lineHeight = 21.sp, + fontWeight = FontWeight.Normal, + ), + h1 = TextStyle( + fontSize = 28.sp, + lineHeight = 34.sp, + fontWeight = FontWeight.SemiBold, + ), + h2 = TextStyle( + fontSize = 22.sp, + lineHeight = 28.sp, + fontWeight = FontWeight.SemiBold, + ), + h3 = TextStyle( + fontSize = 20.sp, + lineHeight = 25.sp, + fontWeight = FontWeight.SemiBold, + ), + h4 = TextStyle( + fontSize = 17.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.SemiBold, + ), + h5 = TextStyle( + fontSize = 17.sp, + lineHeight = 22.sp, + fontWeight = FontWeight.SemiBold, + ), + h6 = TextStyle( + fontSize = 16.sp, + lineHeight = 21.sp, + fontWeight = FontWeight.SemiBold, + ), +) + +val LocalAFFiNETypography = staticCompositionLocalOf { affineTypography } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_ai.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_ai.xml index 4e8e57e99..d5214226f 100644 --- a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_ai.xml +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_ai.xml @@ -4,9 +4,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M11.281,5.491C11.24,5.13 10.935,4.858 10.572,4.857C10.209,4.857 9.903,5.129 9.862,5.49C9.591,7.835 8.89,9.484 7.759,10.616C6.627,11.747 4.978,12.448 2.632,12.719C2.272,12.761 2,13.066 2,13.429C2,13.792 2.273,14.097 2.634,14.138C4.941,14.4 6.625,15.101 7.783,16.238C8.937,17.371 9.653,19.02 9.86,21.349C9.893,21.718 10.202,22 10.572,22C10.943,22 11.251,21.716 11.283,21.347C11.481,19.056 12.197,17.373 13.356,16.213C14.516,15.054 16.199,14.339 18.49,14.14C18.859,14.108 19.142,13.8 19.143,13.429C19.143,13.059 18.861,12.75 18.492,12.717C16.163,12.51 14.514,11.794 13.381,10.64C12.243,9.482 11.542,7.798 11.281,5.491Z" + android:fillColor="#7A7A7A"/> + android:pathData="M18.943,2.247C18.927,2.106 18.808,2 18.667,2C18.526,2 18.407,2.106 18.391,2.246C18.285,3.158 18.013,3.8 17.573,4.239C17.133,4.679 16.491,4.952 15.579,5.057C15.439,5.074 15.333,5.192 15.333,5.334C15.333,5.475 15.439,5.593 15.58,5.609C16.477,5.711 17.132,5.984 17.582,6.426C18.031,6.866 18.309,7.508 18.39,8.413C18.403,8.557 18.523,8.667 18.667,8.667C18.811,8.667 18.931,8.556 18.943,8.413C19.021,7.522 19.299,6.867 19.75,6.416C20.201,5.965 20.855,5.687 21.746,5.61C21.89,5.598 22,5.478 22,5.334C22,5.19 21.89,5.069 21.747,5.057C20.841,4.976 20.2,4.698 19.759,4.249C19.317,3.799 19.044,3.144 18.943,2.247Z" + android:fillColor="#7A7A7A"/> diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_down_small.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_down_small.xml new file mode 100644 index 000000000..17d72eb30 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_down_small.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_up_big.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_up_big.xml new file mode 100644 index 000000000..43f2050ba --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_arrow_up_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_broom.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_broom.xml new file mode 100644 index 000000000..cb00cbee6 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_broom.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bubble.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bubble.xml new file mode 100644 index 000000000..3daf6208d --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bubble.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_01.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_01.xml new file mode 100644 index 000000000..0859a52e4 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_01.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_02.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_02.xml new file mode 100644 index 000000000..7aa15f1df --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_02.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_03.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_03.xml new file mode 100644 index 000000000..1f34d38a1 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_03.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_04.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_04.xml new file mode 100644 index 000000000..1171a4018 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_bulleted_list_04.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_camera.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 000000000..b134608e7 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_close.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..38161f435 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_image.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_image.xml new file mode 100644 index 000000000..15c5b70d8 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_image.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_more_horizontal.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_more_horizontal.xml new file mode 100644 index 000000000..3dd093e12 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_more_horizontal.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_send.xml b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_send.xml new file mode 100644 index 000000000..14aafa913 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/res/drawable/ic_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/frontend/apps/android/App/app/src/main/res/values/colors.xml b/packages/frontend/apps/android/App/app/src/main/res/values/colors.xml index 994b81e59..7463c48f6 100644 --- a/packages/frontend/apps/android/App/app/src/main/res/values/colors.xml +++ b/packages/frontend/apps/android/App/app/src/main/res/values/colors.xml @@ -2,4 +2,5 @@ #FFFFFF #141414 + #1E96EB \ No newline at end of file diff --git a/packages/frontend/apps/android/App/gradle/libs.versions.toml b/packages/frontend/apps/android/App/gradle/libs.versions.toml index 4844b8c64..04d4af870 100644 --- a/packages/frontend/apps/android/App/gradle/libs.versions.toml +++ b/packages/frontend/apps/android/App/gradle/libs.versions.toml @@ -34,6 +34,7 @@ ksp = "2.1.20-2.0.1" minSdk = "22" mozilla-rust-android = "0.9.6" okhttp-bom = "5.0.0-alpha.14" +richtext = "1.0.0-alpha02" # @keep targetSdk = "35" timber = "5.0.1" @@ -90,6 +91,8 @@ okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" } okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse" } +richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } +richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } [plugins]