Last active
May 1, 2024 04:24
-
-
Save kaaneneskpc/b058fa8ad5035b2ebdc2ad94ae5301b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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