Skip to content

Instantly share code, notes, and snippets.

@kaaneneskpc
Last active May 1, 2024 04:24
Show Gist options
  • Save kaaneneskpc/b058fa8ad5035b2ebdc2ad94ae5301b5 to your computer and use it in GitHub Desktop.
Save kaaneneskpc/b058fa8ad5035b2ebdc2ad94ae5301b5 to your computer and use it in GitHub Desktop.
package com.kaaneneskpc.richtexteditor
import android.annotation.SuppressLint
import android.os.Environment
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatAlignLeft
import androidx.compose.material.icons.automirrored.filled.FormatAlignRight
import androidx.compose.material.icons.filled.AddLink
import androidx.compose.material.icons.filled.FormatAlignCenter
import androidx.compose.material.icons.filled.FormatBold
import androidx.compose.material.icons.filled.FormatColorText
import androidx.compose.material.icons.filled.FormatItalic
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.filled.FormatUnderlined
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Title
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
import java.io.File
import java.io.FileWriter
/**
Be sure that you have those two dependencies:
// Rich Text Editor
implementation("com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-beta03")
// Extension Icons
implementation("androidx.compose.material:material-icons-extended:1.6.6")
*/
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreen() {
val state = rememberRichTextState()
val titleSize = MaterialTheme.typography.displaySmall.fontSize
val subtitleSize = MaterialTheme.typography.titleLarge.fontSize
var showExportDialog by remember { mutableStateOf(false) }
Scaffold {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 20.dp)
.padding(bottom = it.calculateBottomPadding())
.padding(top = it.calculateTopPadding()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
EditorControls(
modifier = Modifier.weight(2f),
state = state,
onBoldClick = {
state.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold))
},
onItalicClick = {
state.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic))
},
onUnderlineClick = {
state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
},
onTitleClick = {
state.toggleSpanStyle(SpanStyle(fontSize = titleSize))
},
onSubtitleClick = {
state.toggleSpanStyle(SpanStyle(fontSize = subtitleSize))
},
onTextColorClick = {
state.toggleSpanStyle(SpanStyle(color = Color.Red))
},
onStartAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Start))
},
onEndAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.End))
},
onCenterAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Center))
},
onExportClick = {
showExportDialog = !showExportDialog
}
)
RichTextEditor(
modifier = Modifier
.fillMaxWidth()
.weight(8f),
state = state,
)
if (showExportDialog) {
ExportDialog(state = state) {
showExportDialog = false
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditorControls(
modifier: Modifier = Modifier,
state: RichTextState,
onBoldClick: () -> Unit,
onItalicClick: () -> Unit,
onUnderlineClick: () -> Unit,
onTitleClick: () -> Unit,
onSubtitleClick: () -> Unit,
onTextColorClick: () -> Unit,
onStartAlignClick: () -> Unit,
onEndAlignClick: () -> Unit,
onCenterAlignClick: () -> Unit,
onExportClick: () -> Unit,
) {
var boldSelected by rememberSaveable { mutableStateOf(false) }
var italicSelected by rememberSaveable { mutableStateOf(false) }
var underlineSelected by rememberSaveable { mutableStateOf(false) }
var titleSelected by rememberSaveable { mutableStateOf(false) }
var subtitleSelected by rememberSaveable { mutableStateOf(false) }
var textColorSelected by rememberSaveable { mutableStateOf(false) }
var linkSelected by rememberSaveable { mutableStateOf(false) }
var alignmentSelected by rememberSaveable { mutableIntStateOf(0) }
var showLinkDialog by remember { mutableStateOf(false) }
AnimatedVisibility(visible = showLinkDialog) {
LinkDialog(
onDismissRequest = {
showLinkDialog = false
linkSelected = false
},
onConfirmation = { linkText, link ->
state.addLink(
text = linkText,
url = link
)
showLinkDialog = false
linkSelected = false
}
)
}
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(all = 10.dp)
.padding(bottom = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ControlWrapper(
selected = boldSelected,
onChangeClick = { boldSelected = it },
onClick = onBoldClick
) {
Icon(
imageVector = Icons.Default.FormatBold,
contentDescription = "Bold Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = italicSelected,
onChangeClick = { italicSelected = it },
onClick = onItalicClick
) {
Icon(
imageVector = Icons.Default.FormatItalic,
contentDescription = "Italic Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = underlineSelected,
onChangeClick = { underlineSelected = it },
onClick = onUnderlineClick
) {
Icon(
imageVector = Icons.Default.FormatUnderlined,
contentDescription = "Underline Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = titleSelected,
onChangeClick = { titleSelected = it },
onClick = onTitleClick
) {
Icon(
imageVector = Icons.Default.Title,
contentDescription = "Title Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = subtitleSelected,
onChangeClick = { subtitleSelected = it },
onClick = onSubtitleClick
) {
Icon(
imageVector = Icons.Default.FormatSize,
contentDescription = "Subtitle Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = textColorSelected,
onChangeClick = { textColorSelected = it },
onClick = onTextColorClick
) {
Icon(
imageVector = Icons.Default.FormatColorText,
contentDescription = "Text Color Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = linkSelected,
onChangeClick = { linkSelected = it },
onClick = { showLinkDialog = true }
) {
Icon(
imageVector = Icons.Default.AddLink,
contentDescription = "Link Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 0,
onChangeClick = { alignmentSelected = 0 },
onClick = onStartAlignClick
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.FormatAlignLeft,
contentDescription = "Start Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 1,
onChangeClick = { alignmentSelected = 1 },
onClick = onCenterAlignClick
) {
Icon(
imageVector = Icons.Default.FormatAlignCenter,
contentDescription = "Center Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 2,
onChangeClick = { alignmentSelected = 2 },
onClick = onEndAlignClick
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.FormatAlignRight,
contentDescription = "End Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = true,
selectedColor = MaterialTheme.colorScheme.tertiary,
onChangeClick = { },
onClick = onExportClick
) {
Icon(
imageVector = Icons.Default.Save,
contentDescription = "Export Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
fun LinkDialog(
onDismissRequest: () -> Unit,
onConfirmation: (String, String) -> Unit
) {
var linkText by remember { mutableStateOf("") }
var linkUrl by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = "Add Link") },
text = {
Column {
OutlinedTextField(
value = linkText,
onValueChange = { linkText = it },
label = { Text("Link Text") }
)
OutlinedTextField(
value = linkUrl,
onValueChange = { linkUrl = it },
label = { Text("URL") }
)
}
},
confirmButton = {
Button(
onClick = {
onConfirmation(linkText, linkUrl)
onDismissRequest()
}
) {
Text("Add")
}
},
dismissButton = {
Button(onClick = onDismissRequest) {
Text("Cancel")
}
}
)
}
@Composable
fun ControlWrapper(
selected: Boolean,
selectedColor: Color = MaterialTheme.colorScheme.primary,
unselectedColor: Color = MaterialTheme.colorScheme.inversePrimary,
onChangeClick: (Boolean) -> Unit,
onClick: () -> Unit,
content: @Composable () -> Unit
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(size = 6.dp))
.clickable {
onClick()
onChangeClick(!selected)
}
.background(
if (selected) selectedColor
else unselectedColor
)
.border(
width = 1.dp,
color = Color.LightGray,
shape = RoundedCornerShape(size = 6.dp)
)
.padding(all = 8.dp),
contentAlignment = Alignment.Center
) {
content()
}
}
fun saveRichTextContentToDownloads(content: String, fileName: String) {
// Get the Downloads folder
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
// Create a File for your text file within the Downloads folder
val file = File(downloadsDir, fileName)
// Write content to the file
FileWriter(file).use { writer ->
writer.write(content)
}
}
@Composable
fun ExportDialog(state: RichTextState, onClose: () -> Unit) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = { onClose() },
title = {
Text(text = "Exported Content")
},
text = {
Text(text = state.annotatedString)
},
confirmButton = {
Button(onClick = {
val richTextContent = state.annotatedString // Get the content from your RichTextEditor
val fileName = "MyRichTextContent.txt" // Specify the desired filename
saveRichTextContentToDownloads(richTextContent.toString(), fileName)
Toast.makeText(context, "Content saved to Downloads folder!", Toast.LENGTH_SHORT).show()
}) {
Text("Save")
}
},
dismissButton = {
Button(onClick = { onClose() }) {
Text("Close")
}
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment