Fitur Markdown (RichEditorState) pada catatan dan penyeesuaian Selection Handle

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-12-19 00:29:13 +07:00
parent b8d9a71664
commit c0bbd3e54f
8 changed files with 639 additions and 213 deletions

View File

@ -68,6 +68,8 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.animation.core)
implementation(libs.androidx.glance)
implementation(libs.androidx.animation)
implementation(libs.androidx.ui.graphics)
// Untuk integrasi Gemini AI (optional - uncomment jika sudah ada API key)
// implementation("com.google.ai.client.generativeai:generativeai:0.1.2")

View File

@ -16,6 +16,7 @@
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:theme="@style/Theme.NotesAI">
<intent-filter>

View File

@ -1,50 +1,45 @@
package com.example.notesai.presentation.screens.note
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.layout.imeNestedScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor // ✅ ADD
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import com.example.notesai.data.model.Note
import com.example.notesai.presentation.screens.note.components.DraggableMiniMarkdownToolbar
import com.example.notesai.presentation.screens.note.editor.RichEditorState
import com.example.notesai.util.MarkdownParser
import com.example.notesai.util.MarkdownSerializer
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.*
import kotlin.math.roundToInt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun EditableFullScreenNoteView(
note: Note,
@ -55,196 +50,239 @@ fun EditableFullScreenNoteView(
onPinToggle: () -> Unit
) {
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.content) }
var showArchiveDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
var isContentFocused by remember { mutableStateOf(false) }
// Dialog Konfirmasi Arsip
if (showArchiveDialog) {
AlertDialog(
onDismissRequest = { showArchiveDialog = false },
title = {
Text(
text = "Arsipkan Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dipindahkan ke arsip. Anda masih bisa mengaksesnya nanti.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onArchive()
showArchiveDialog = false
}
) {
Text("Arsipkan", color = MaterialTheme.colorScheme.primary)
}
},
dismissButton = {
TextButton(onClick = { showArchiveDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
val editorState = remember(note.id) {
RichEditorState(MarkdownParser.parse(note.content))
}
// Dialog Konfirmasi Hapus
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = {
Text(
text = "Hapus Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dihapus permanen dan tidak bisa dikembalikan.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
onDelete()
showDeleteDialog = false
}
) {
Text("Hapus", color = Color(0xFFEF4444))
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
val focusRequester = remember { FocusRequester() }
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val keyboard = LocalSoftwareKeyboardController.current
fun ensureFocus() {
focusRequester.requestFocus()
keyboard?.show()
}
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onBack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
}
},
actions = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onPinToggle()
}) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin Catatan",
tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
)
}
IconButton(onClick = { showArchiveDialog = true }) {
Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
}
IconButton(onClick = { showDeleteDialog = true }) {
Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
fun saveNote() {
if (title.isNotBlank()) {
onSave(
title,
MarkdownSerializer.toMarkdown(editorState.value.annotatedString)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp)
) {
TextField(
value = title,
onValueChange = { title = it },
textStyle = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
),
placeholder = {
Text(
"Judul",
style = MaterialTheme.typography.headlineLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(12.dp))
// 🔥 AUTO SAVE SAAT APP BACKGROUND / KELUAR
val lifecycleOwner = LocalLifecycleOwner.current
Text(
text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_STOP) {
saveNote()
}
}
Divider(
modifier = Modifier.padding(vertical = 20.dp),
color = MaterialTheme.colorScheme.surface
)
lifecycleOwner.lifecycle.addObserver(observer)
TextField(
value = content,
onValueChange = { content = it },
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
lineHeight = 28.sp
),
placeholder = {
Text(
"Mulai menulis...",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
val dateFormat = remember {
SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
}
val density = LocalDensity.current
val config = LocalConfiguration.current
val screenWidthPx = with(density) { config.screenWidthDp.dp.toPx() }
val screenHeightPx = with(density) { config.screenHeightDp.dp.toPx() }
val marginPx = with(density) { 16.dp.toPx() }
val imeBottomPx = with(density) {
WindowInsets.ime.getBottom(this).toFloat()
}
var toolbarSizePx by remember {
mutableStateOf(androidx.compose.ui.geometry.Size.Zero)
}
var toolbarOffset by remember {
mutableStateOf(Offset(marginPx, screenHeightPx * 0.6f))
}
fun moveToolbar(dx: Float, dy: Float) {
toolbarOffset = toolbarOffset.copy(
x = toolbarOffset.x + dx,
y = toolbarOffset.y + dy
)
}
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
saveNote()
onBack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
}
},
title = {},
actions = {
IconButton(onClick = {
saveNote()
onPinToggle()
}) {
Icon(
if (note.isPinned) Icons.Filled.Star
else Icons.Outlined.StarBorder,
null
)
}
IconButton(onClick = onArchive) {
Icon(Icons.Default.Archive, null)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, null)
}
}
)
},
contentWindowInsets = WindowInsets(0)
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 400.dp)
)
.fillMaxSize()
.padding(paddingValues)
.imeNestedScroll()
.verticalScroll(scrollState)
.padding(horizontal = 20.dp)
.padding(
bottom = WindowInsets.ime
.asPaddingValues()
.calculateBottomPadding()
)
) {
Spacer(modifier = Modifier.height(100.dp))
TextField(
value = title,
onValueChange = { title = it },
textStyle = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
),
placeholder = { Text("Judul") },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
Spacer(Modifier.height(12.dp))
Text(
"Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)
)
HorizontalDivider(Modifier.padding(vertical = 20.dp))
// ✅ FIX UTAMA: set cursorBrush agar insertion cursor muncul
BasicTextField(
value = editorState.value,
onValueChange = {
editorState.onValueChange(it)
scope.launch {
bringIntoViewRequester.bringIntoView()
}
},
cursorBrush = SolidColor(Color(0xFFA855F7)), // ✅ ADD THIS
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onBackground,
lineHeight = 28.sp
),
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 400.dp)
.focusRequester(focusRequester)
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusChanged {
isContentFocused = it.isFocused
if (it.isFocused) {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
},
decorationBox = { innerTextField ->
Box {
if (editorState.value.text.isEmpty()) {
Text(
"Mulai menulis...",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.4f)
)
}
innerTextField()
}
}
)
Spacer(Modifier.height(180.dp))
}
}
if (isContentFocused) {
DraggableMiniMarkdownToolbar(
modifier = Modifier
.align(Alignment.TopStart)
.offset {
val maxX =
(screenWidthPx - toolbarSizePx.width - marginPx)
.coerceAtLeast(marginPx)
val maxY =
(screenHeightPx - imeBottomPx - toolbarSizePx.height)
.coerceAtLeast(marginPx)
IntOffset(
toolbarOffset.x.coerceIn(marginPx, maxX).roundToInt(),
toolbarOffset.y.coerceIn(marginPx, maxY).roundToInt()
)
}
.onSizeChanged {
toolbarSizePx = androidx.compose.ui.geometry.Size(
it.width.toFloat(),
it.height.toFloat()
)
},
isBoldActive = editorState.isBoldActive(),
isItalicActive = editorState.isItalicActive(),
onDrag = ::moveToolbar,
onBold = {
ensureFocus()
editorState.toggleBold()
},
onItalic = {
ensureFocus()
editorState.toggleItalic()
},
onHeading = {},
onBullet = {},
onCodeBlock = {}
)
}
}
}

View File

@ -0,0 +1,127 @@
package com.example.notesai.presentation.screens.note.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
@Composable
fun DraggableMiniMarkdownToolbar(
modifier: Modifier = Modifier,
onDrag: (dx: Float, dy: Float) -> Unit,
// STATE
isBoldActive: Boolean,
isItalicActive: Boolean,
// ACTIONS
onBold: () -> Unit,
onItalic: () -> Unit,
onHeading: () -> Unit,
onBullet: () -> Unit,
onCodeBlock: () -> Unit
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
shadowElevation = 6.dp
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
// 🔹 DRAG HANDLE
Box(
modifier = Modifier
.size(36.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
onDrag(dragAmount.x, dragAmount.y)
}
},
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.DragIndicator,
contentDescription = "Drag",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
modifier = Modifier.size(18.dp)
)
}
ToolbarIcon(
icon = Icons.Default.FormatBold,
isActive = isBoldActive,
onClick = onBold
)
ToolbarIcon(
icon = Icons.Default.FormatItalic,
isActive = isItalicActive,
onClick = onItalic
)
ToolbarIcon(
icon = Icons.Default.Title,
onClick = onHeading
)
ToolbarIcon(
icon = Icons.Default.FormatListBulleted,
onClick = onBullet
)
ToolbarIcon(
icon = Icons.Default.Code,
onClick = onCodeBlock
)
}
}
}
@Composable
private fun ToolbarIcon(
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
isActive: Boolean = false
) {
val activeBg = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
val activeColor = MaterialTheme.colorScheme.primary
Box(
modifier = Modifier
.size(36.dp)
.background(
color = if (isActive) activeBg else androidx.compose.ui.graphics.Color.Transparent,
shape = RoundedCornerShape(10.dp)
),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = onClick,
modifier = Modifier.size(36.dp)
) {
Icon(
icon,
contentDescription = null,
tint = if (isActive) activeColor else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(18.dp)
)
}
}
}

View File

@ -0,0 +1,163 @@
package com.example.notesai.presentation.screens.note.editor
import androidx.compose.runtime.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
@Stable
class RichEditorState(initial: AnnotatedString) {
var value by mutableStateOf(
TextFieldValue(
annotatedString = initial,
selection = TextRange(initial.length)
)
)
// 🔑 ACTIVE TYPING STYLES (sticky)
private val activeStyles = mutableStateListOf<SpanStyle>()
/* ---------------------------
VALUE CHANGE (typing)
--------------------------- */
fun onValueChange(newValue: TextFieldValue) {
val old = value
// Cursor / IME update only
if (newValue.text == old.text) {
value = old.copy(
selection = newValue.selection,
composition = newValue.composition
)
return
}
val builder = AnnotatedString.Builder(newValue.text)
// 1⃣ copy old spans
old.annotatedString.spanStyles.forEach {
val start = it.start.coerceAtMost(newValue.text.length)
val end = it.end.coerceAtMost(newValue.text.length)
if (start < end) builder.addStyle(it.item, start, end)
}
// 2⃣ apply active typing styles to NEW CHAR
val insertStart = newValue.selection.start - 1
val insertEnd = newValue.selection.start
if (insertStart >= 0 && insertEnd > insertStart) {
activeStyles.forEach { style ->
builder.addStyle(style, insertStart, insertEnd)
}
}
value = TextFieldValue(
annotatedString = builder.toAnnotatedString(),
selection = newValue.selection,
composition = newValue.composition
)
}
/* ---------------------------
TOGGLE STYLES
--------------------------- */
fun toggleBold() =
toggleStyle(SpanStyle(fontWeight = FontWeight.Bold))
fun toggleItalic() =
toggleStyle(SpanStyle(fontStyle = FontStyle.Italic))
private fun toggleStyle(style: SpanStyle) {
val sel = value.selection.normalized()
val hasSelection = !sel.collapsed
if (hasSelection) {
applyStyleToSelection(style)
} else {
toggleTypingStyle(style)
}
}
/* ---------------------------
APPLY TO SELECTION
--------------------------- */
private fun applyStyleToSelection(style: SpanStyle) {
val sel = value.selection.normalized()
val start = sel.start
val end = sel.end
if (start >= end) return
val builder = AnnotatedString.Builder(value.text)
value.annotatedString.spanStyles.forEach {
val overlap = it.start < end && it.end > start
val same =
it.item.fontWeight == style.fontWeight &&
it.item.fontStyle == style.fontStyle
if (!(overlap && same)) {
builder.addStyle(it.item, it.start, it.end)
}
}
builder.addStyle(style, start, end)
value = value.copy(
annotatedString = builder.toAnnotatedString(),
selection = TextRange(end)
)
}
/* ---------------------------
TYPING MODE (sticky)
--------------------------- */
private fun toggleTypingStyle(style: SpanStyle) {
val index = activeStyles.indexOfFirst {
it.fontWeight == style.fontWeight &&
it.fontStyle == style.fontStyle
}
if (index >= 0) {
activeStyles.removeAt(index)
} else {
activeStyles.add(style)
}
}
/* ---------------------------
TOOLBAR STATE
--------------------------- */
fun isBoldActive(): Boolean =
isStyleActive(FontWeight.Bold, null)
fun isItalicActive(): Boolean =
isStyleActive(null, FontStyle.Italic)
private fun isStyleActive(
weight: FontWeight?,
style: FontStyle?
): Boolean {
val sel = value.selection
if (!sel.collapsed) {
return value.annotatedString.spanStyles.any {
it.start <= sel.start &&
it.end >= sel.end &&
(weight == null || it.item.fontWeight == weight) &&
(style == null || it.item.fontStyle == style)
}
}
return activeStyles.any {
(weight == null || it.fontWeight == weight) &&
(style == null || it.fontStyle == style)
}
}
}
/* -------- helper -------- */
private fun TextRange.normalized(): TextRange =
if (start <= end) this else TextRange(end, start)

View File

@ -0,0 +1,58 @@
package com.example.notesai.util
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
object MarkdownParser {
fun parse(markdown: String): AnnotatedString {
val builder = AnnotatedString.Builder()
var i = 0
while (i < markdown.length) {
when {
markdown.startsWith("**", i) -> {
val end = markdown.indexOf("**", i + 2)
if (end != -1) {
val content = markdown.substring(i + 2, end)
val start = builder.length
builder.append(content)
builder.addStyle(
SpanStyle(fontWeight = FontWeight.Bold),
start,
start + content.length
)
i = end + 2
} else {
builder.append(markdown[i++])
}
}
markdown.startsWith("*", i) -> {
val end = markdown.indexOf("*", i + 1)
if (end != -1) {
val content = markdown.substring(i + 1, end)
val start = builder.length
builder.append(content)
builder.addStyle(
SpanStyle(fontStyle = FontStyle.Italic),
start,
start + content.length
)
i = end + 1
} else {
builder.append(markdown[i++])
}
}
else -> {
builder.append(markdown[i++])
}
}
}
return builder.toAnnotatedString()
}
}

View File

@ -0,0 +1,35 @@
package com.example.notesai.util
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
object MarkdownSerializer {
fun toMarkdown(text: AnnotatedString): String {
val raw = text.text
if (text.spanStyles.isEmpty()) return raw
val markers = Array(raw.length + 1) { mutableListOf<String>() }
text.spanStyles.forEach { span ->
if (span.item.fontWeight == FontWeight.Bold) {
markers[span.start].add("**")
markers[span.end].add("**")
}
if (span.item.fontStyle == FontStyle.Italic) {
markers[span.start].add("*")
markers[span.end].add("*")
}
}
val sb = StringBuilder()
for (i in raw.indices) {
markers[i].forEach { sb.append(it) }
sb.append(raw[i])
}
markers[raw.length].forEach { sb.append(it) }
return sb.toString()
}
}

View File

@ -16,7 +16,8 @@ firebaseAnnotations = "17.0.0"
firebaseFirestoreKtx = "26.0.2"
uiGraphics = "1.10.0"
roomCompiler = "2.8.4"
foundationLayout = "1.10.0"
glance = "1.1.1"
animation = "1.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -34,7 +35,8 @@ firebase-annotations = { group = "com.google.firebase", name = "firebase-annotat
firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebaseFirestoreKtx" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomCompiler" }
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-glance = { group = "androidx.glance", name = "glance", version.ref = "glance" }
androidx-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }