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:
parent
f4c7057daf
commit
c86d2c2f6a
2 changed files with 82 additions and 55 deletions
|
@ -22,11 +22,19 @@ data class EditorState(
|
||||||
val fileName: String = "Untitled.md",
|
val fileName: String = "Untitled.md",
|
||||||
val markdown: String = "",
|
val markdown: String = "",
|
||||||
val path: URI? = null,
|
val path: URI? = null,
|
||||||
val dirty: Boolean = false,
|
|
||||||
val toast: String? = null,
|
val toast: String? = null,
|
||||||
val alert: AlertDialogModel? = 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(
|
class MarkdownViewModel(
|
||||||
private val fileHelper: FileHelper,
|
private val fileHelper: FileHelper,
|
||||||
|
@ -45,7 +53,6 @@ class MarkdownViewModel(
|
||||||
fun updateMarkdown(markdown: String?) {
|
fun updateMarkdown(markdown: String?) {
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
markdown = markdown ?: "",
|
markdown = markdown ?: "",
|
||||||
dirty = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,11 +88,13 @@ class MarkdownViewModel(
|
||||||
val uri = URI.create(actualLoadPath)
|
val uri = URI.create(actualLoadPath)
|
||||||
fileHelper.open(uri)
|
fileHelper.open(uri)
|
||||||
?.let { (name, content) ->
|
?.let { (name, content) ->
|
||||||
_state.value = _state.value.copy(
|
val currentState = _state.value
|
||||||
|
_state.value = currentState.copy(
|
||||||
path = uri,
|
path = uri,
|
||||||
fileName = name,
|
fileName = name,
|
||||||
markdown = content,
|
markdown = content,
|
||||||
dirty = false,
|
initialMarkdown = content,
|
||||||
|
reloadToggle = currentState.reloadToggle.inv(),
|
||||||
toast = "Successfully loaded $name"
|
toast = "Successfully loaded $name"
|
||||||
)
|
)
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
||||||
|
@ -120,7 +129,7 @@ class MarkdownViewModel(
|
||||||
_state.value = currentState.copy(
|
_state.value = currentState.copy(
|
||||||
fileName = name,
|
fileName = name,
|
||||||
path = actualSavePath,
|
path = actualSavePath,
|
||||||
dirty = false,
|
initialMarkdown = currentState.markdown,
|
||||||
toast = if (interactive) "Successfully saved $name" else null
|
toast = if (interactive) "Successfully saved $name" else null
|
||||||
)
|
)
|
||||||
Timber.i("Saved file $name to uri $actualSavePath")
|
Timber.i("Saved file $name to uri $actualSavePath")
|
||||||
|
|
|
@ -50,6 +50,7 @@ import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
@ -100,7 +101,10 @@ fun MainScreen(
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val fileName by viewModel.collectAsState(EditorState::fileName, "")
|
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 markdown by viewModel.collectAsState(EditorState::markdown, "")
|
||||||
|
val dirty by viewModel.collectAsState(EditorState::dirty, false)
|
||||||
val alert by viewModel.collectAsState(EditorState::alert, null)
|
val alert by viewModel.collectAsState(EditorState::alert, null)
|
||||||
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
|
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
|
||||||
LaunchedEffect(enableAutosave) {
|
LaunchedEffect(enableAutosave) {
|
||||||
|
@ -112,7 +116,10 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
val toast by viewModel.collectAsState(EditorState::toast, null)
|
val toast by viewModel.collectAsState(EditorState::toast, null)
|
||||||
MainScreen(
|
MainScreen(
|
||||||
|
dirty = dirty,
|
||||||
fileName = fileName,
|
fileName = fileName,
|
||||||
|
reloadToggle = reloadToggle,
|
||||||
|
initialMarkdown = initialMarkdown,
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = viewModel::updateMarkdown,
|
setMarkdown = viewModel::updateMarkdown,
|
||||||
message = toast,
|
message = toast,
|
||||||
|
@ -145,6 +152,9 @@ fun MainScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainScreen(
|
private fun MainScreen(
|
||||||
fileName: String = "Untitled.md",
|
fileName: String = "Untitled.md",
|
||||||
|
dirty: Boolean = false,
|
||||||
|
reloadToggle: Int = 0,
|
||||||
|
initialMarkdown: String = "",
|
||||||
markdown: String = "",
|
markdown: String = "",
|
||||||
setMarkdown: (String) -> Unit = {},
|
setMarkdown: (String) -> Unit = {},
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
|
@ -160,7 +170,7 @@ private fun MainScreen(
|
||||||
enableWideLayout: Boolean = false,
|
enableWideLayout: Boolean = false,
|
||||||
enableReadability: Boolean = false
|
enableReadability: Boolean = false
|
||||||
) {
|
) {
|
||||||
var lockSwiping by remember { mutableStateOf(false) }
|
var lockSwiping by remember { mutableStateOf(true) }
|
||||||
val openFileLauncher =
|
val openFileLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||||
loadFile(it)
|
loadFile(it)
|
||||||
|
@ -212,7 +222,8 @@ private fun MainScreen(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
MarkdownTopAppBar(title = fileName,
|
MarkdownTopAppBar(
|
||||||
|
title = if (dirty) "$fileName*" else fileName,
|
||||||
backAsUp = false,
|
backAsUp = false,
|
||||||
goBack = navigateBack,
|
goBack = navigateBack,
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
|
@ -290,6 +301,7 @@ private fun MainScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
|
reload = reloadToggle,
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
enableReadability = enableReadability
|
enableReadability = enableReadability
|
||||||
|
@ -314,10 +326,12 @@ private fun MainScreen(
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
TabbedMarkdownEditor(
|
TabbedMarkdownEditor(
|
||||||
|
initialMarkdown = initialMarkdown,
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
lockSwiping = lockSwiping,
|
lockSwiping = lockSwiping,
|
||||||
enableReadability = enableReadability
|
enableReadability = enableReadability,
|
||||||
|
reloadToggle = reloadToggle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,10 +342,12 @@ private fun MainScreen(
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
private fun TabbedMarkdownEditor(
|
private fun TabbedMarkdownEditor(
|
||||||
|
initialMarkdown: String,
|
||||||
markdown: String,
|
markdown: String,
|
||||||
setMarkdown: (String) -> Unit,
|
setMarkdown: (String) -> Unit,
|
||||||
lockSwiping: Boolean,
|
lockSwiping: Boolean,
|
||||||
enableReadability: Boolean
|
enableReadability: Boolean,
|
||||||
|
reloadToggle: Int
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState { 2 }
|
val pagerState = rememberPagerState { 2 }
|
||||||
|
@ -357,9 +373,10 @@ private fun TabbedMarkdownEditor(
|
||||||
if (page == 0) {
|
if (page == 0) {
|
||||||
MarkdownTextField(
|
MarkdownTextField(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
markdown = markdown,
|
markdown = initialMarkdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
enableReadability = enableReadability
|
enableReadability = enableReadability,
|
||||||
|
reload = reloadToggle
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MarkdownText(modifier = Modifier.fillMaxSize(), markdown)
|
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 readability = Readability(this)
|
||||||
val annotated = AnnotatedString.Builder(this)
|
val annotated = AnnotatedString.Builder(this)
|
||||||
for (sentence in readability.sentences()) {
|
for (sentence in readability.sentences()) {
|
||||||
|
@ -385,43 +403,46 @@ fun MarkdownNavigationDrawer(
|
||||||
) {
|
) {
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
|
DismissibleNavigationDrawer(
|
||||||
DismissibleDrawerSheet {
|
gesturesEnabled = false,
|
||||||
Row(
|
drawerState = drawerState,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
drawerContent = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
DismissibleDrawerSheet {
|
||||||
) {
|
Row(
|
||||||
Icon(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.size(96.dp),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
) {
|
||||||
contentDescription = null,
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
modifier = Modifier.size(96.dp),
|
||||||
)
|
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||||
Text(
|
contentDescription = null,
|
||||||
text = "Simple Markdown",
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
style = MaterialTheme.typography.titleLarge
|
)
|
||||||
)
|
Text(
|
||||||
}
|
text = "Simple Markdown",
|
||||||
Route.entries.forEach { route ->
|
style = MaterialTheme.typography.titleLarge
|
||||||
if (route == Route.EDITOR) {
|
)
|
||||||
return@forEach
|
|
||||||
}
|
}
|
||||||
NavigationDrawerItem(
|
Route.entries.forEach { route ->
|
||||||
icon = {
|
if (route == Route.EDITOR) {
|
||||||
Icon(imageVector = route.icon, contentDescription = null)
|
return@forEach
|
||||||
},
|
|
||||||
label = { Text(route.title) },
|
|
||||||
selected = false,
|
|
||||||
onClick = {
|
|
||||||
navigate(route)
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
NavigationDrawerItem(
|
||||||
|
icon = {
|
||||||
|
Icon(imageVector = route.icon, contentDescription = null)
|
||||||
|
},
|
||||||
|
label = { Text(route.title) },
|
||||||
|
selected = false,
|
||||||
|
onClick = {
|
||||||
|
navigate(route)
|
||||||
|
coroutineScope.launch {
|
||||||
|
drawerState.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}) {
|
||||||
}) {
|
|
||||||
content(drawerState)
|
content(drawerState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -469,23 +490,20 @@ fun MarkdownTopAppBar(
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownTextField(
|
fun MarkdownTextField(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
reload: Int,
|
||||||
markdown: String,
|
markdown: String,
|
||||||
setMarkdown: (String) -> Unit,
|
setMarkdown: (String) -> Unit,
|
||||||
enableReadability: Boolean = false
|
enableReadability: Boolean = false
|
||||||
) {
|
) {
|
||||||
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
|
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
|
||||||
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
|
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
|
||||||
val textFieldValue = remember(markdown) {
|
val (textFieldValue, setTextFieldValue) = remember(reload) {
|
||||||
val annotatedMarkdown = if (enableReadability) {
|
mutableStateOf(TextFieldValue(markdown.annotate(enableReadability), selection, composition))
|
||||||
markdown.annotateReadability()
|
|
||||||
} else {
|
|
||||||
AnnotatedString(markdown)
|
|
||||||
}
|
|
||||||
TextFieldValue(annotatedMarkdown, selection, composition)
|
|
||||||
}
|
}
|
||||||
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
|
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
|
||||||
setSelection(it.selection)
|
setSelection(it.selection)
|
||||||
setComposition(it.composition)
|
setComposition(it.composition)
|
||||||
|
setTextFieldValue(it.copy(annotatedString = it.text.annotate(enableReadability)))
|
||||||
setMarkdown(it.text)
|
setMarkdown(it.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue