Improve ergonomics of editor:

- Show indicator in title for when file has unsaved changes
- Fix issues with scrolling on preview
  - Default to disable locked swiping between tabs
  - Disable gestures on navigation drawer
 - Fix text selection and typing in editor
This commit is contained in:
William Brawner 2024-05-05 19:44:36 -06:00
parent f4c7057daf
commit c86d2c2f6a
Signed by: wbrawner
GPG key ID: 8FF12381C6C90D35
2 changed files with 82 additions and 55 deletions

View file

@ -22,11 +22,19 @@ data class EditorState(
val fileName: String = "Untitled.md",
val markdown: String = "",
val path: URI? = null,
val dirty: Boolean = false,
val toast: String? = null,
val alert: AlertDialogModel? = null,
val saveCallback: (() -> Unit)? = null
)
val saveCallback: (() -> Unit)? = null,
/**
* Used to signal to the view that it should reload due to an external change, like loading
* a new file
*/
val reloadToggle: Int = 0,
private val initialMarkdown: String = "",
) {
val dirty: Boolean
get() = markdown != initialMarkdown
}
class MarkdownViewModel(
private val fileHelper: FileHelper,
@ -45,7 +53,6 @@ class MarkdownViewModel(
fun updateMarkdown(markdown: String?) {
_state.value = _state.value.copy(
markdown = markdown ?: "",
dirty = true
)
}
@ -81,11 +88,13 @@ class MarkdownViewModel(
val uri = URI.create(actualLoadPath)
fileHelper.open(uri)
?.let { (name, content) ->
_state.value = _state.value.copy(
val currentState = _state.value
_state.value = currentState.copy(
path = uri,
fileName = name,
markdown = content,
dirty = false,
initialMarkdown = content,
reloadToggle = currentState.reloadToggle.inv(),
toast = "Successfully loaded $name"
)
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
@ -120,7 +129,7 @@ class MarkdownViewModel(
_state.value = currentState.copy(
fileName = name,
path = actualSavePath,
dirty = false,
initialMarkdown = currentState.markdown,
toast = if (interactive) "Successfully saved $name" else null
)
Timber.i("Saved file $name to uri $actualSavePath")

View file

@ -50,6 +50,7 @@ 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.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -100,7 +101,10 @@ fun MainScreen(
) {
val coroutineScope = rememberCoroutineScope()
val fileName by viewModel.collectAsState(EditorState::fileName, "")
val initialMarkdown by viewModel.collectAsState(EditorState::markdown, "")
val reloadToggle by viewModel.collectAsState(EditorState::reloadToggle, 0)
val markdown by viewModel.collectAsState(EditorState::markdown, "")
val dirty by viewModel.collectAsState(EditorState::dirty, false)
val alert by viewModel.collectAsState(EditorState::alert, null)
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
LaunchedEffect(enableAutosave) {
@ -112,7 +116,10 @@ fun MainScreen(
}
val toast by viewModel.collectAsState(EditorState::toast, null)
MainScreen(
dirty = dirty,
fileName = fileName,
reloadToggle = reloadToggle,
initialMarkdown = initialMarkdown,
markdown = markdown,
setMarkdown = viewModel::updateMarkdown,
message = toast,
@ -145,6 +152,9 @@ fun MainScreen(
@Composable
private fun MainScreen(
fileName: String = "Untitled.md",
dirty: Boolean = false,
reloadToggle: Int = 0,
initialMarkdown: String = "",
markdown: String = "",
setMarkdown: (String) -> Unit = {},
message: String? = null,
@ -160,7 +170,7 @@ private fun MainScreen(
enableWideLayout: Boolean = false,
enableReadability: Boolean = false
) {
var lockSwiping by remember { mutableStateOf(false) }
var lockSwiping by remember { mutableStateOf(true) }
val openFileLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
loadFile(it)
@ -212,7 +222,8 @@ private fun MainScreen(
Scaffold(
topBar = {
val context = LocalContext.current
MarkdownTopAppBar(title = fileName,
MarkdownTopAppBar(
title = if (dirty) "$fileName*" else fileName,
backAsUp = false,
goBack = navigateBack,
drawerState = drawerState,
@ -290,6 +301,7 @@ private fun MainScreen(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
reload = reloadToggle,
markdown = markdown,
setMarkdown = setMarkdown,
enableReadability = enableReadability
@ -314,10 +326,12 @@ private fun MainScreen(
.padding(paddingValues)
) {
TabbedMarkdownEditor(
initialMarkdown = initialMarkdown,
markdown = markdown,
setMarkdown = setMarkdown,
lockSwiping = lockSwiping,
enableReadability = enableReadability
enableReadability = enableReadability,
reloadToggle = reloadToggle
)
}
}
@ -328,10 +342,12 @@ private fun MainScreen(
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun TabbedMarkdownEditor(
initialMarkdown: String,
markdown: String,
setMarkdown: (String) -> Unit,
lockSwiping: Boolean,
enableReadability: Boolean
enableReadability: Boolean,
reloadToggle: Int
) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState { 2 }
@ -357,9 +373,10 @@ private fun TabbedMarkdownEditor(
if (page == 0) {
MarkdownTextField(
modifier = Modifier.fillMaxSize(),
markdown = markdown,
markdown = initialMarkdown,
setMarkdown = setMarkdown,
enableReadability = enableReadability
enableReadability = enableReadability,
reload = reloadToggle
)
} else {
MarkdownText(modifier = Modifier.fillMaxSize(), markdown)
@ -367,7 +384,8 @@ private fun TabbedMarkdownEditor(
}
}
private fun String.annotateReadability(): AnnotatedString {
private fun String.annotate(enableReadability: Boolean): AnnotatedString {
if (!enableReadability) return AnnotatedString(this)
val readability = Readability(this)
val annotated = AnnotatedString.Builder(this)
for (sentence in readability.sentences()) {
@ -385,43 +403,46 @@ fun MarkdownNavigationDrawer(
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
DismissibleDrawerSheet {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(96.dp),
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Simple Markdown",
style = MaterialTheme.typography.titleLarge
)
}
Route.entries.forEach { route ->
if (route == Route.EDITOR) {
return@forEach
DismissibleNavigationDrawer(
gesturesEnabled = false,
drawerState = drawerState,
drawerContent = {
DismissibleDrawerSheet {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(96.dp),
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Simple Markdown",
style = MaterialTheme.typography.titleLarge
)
}
NavigationDrawerItem(
icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(route.title) },
selected = false,
onClick = {
navigate(route)
coroutineScope.launch {
drawerState.close()
}
Route.entries.forEach { route ->
if (route == Route.EDITOR) {
return@forEach
}
)
NavigationDrawerItem(
icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(route.title) },
selected = false,
onClick = {
navigate(route)
coroutineScope.launch {
drawerState.close()
}
}
)
}
}
}
}) {
}) {
content(drawerState)
}
}
@ -469,23 +490,20 @@ fun MarkdownTopAppBar(
@Composable
fun MarkdownTextField(
modifier: Modifier,
reload: Int,
markdown: String,
setMarkdown: (String) -> Unit,
enableReadability: Boolean = false
) {
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
val textFieldValue = remember(markdown) {
val annotatedMarkdown = if (enableReadability) {
markdown.annotateReadability()
} else {
AnnotatedString(markdown)
}
TextFieldValue(annotatedMarkdown, selection, composition)
val (textFieldValue, setTextFieldValue) = remember(reload) {
mutableStateOf(TextFieldValue(markdown.annotate(enableReadability), selection, composition))
}
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
setSelection(it.selection)
setComposition(it.composition)
setTextFieldValue(it.copy(annotatedString = it.text.annotate(enableReadability)))
setMarkdown(it.text)
}