editor-fixes #94
8 changed files with 222 additions and 196 deletions
|
@ -9,7 +9,6 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||||
import androidx.test.espresso.action.ViewActions.*
|
|
||||||
import androidx.test.espresso.intent.Intents.intending
|
import androidx.test.espresso.intent.Intents.intending
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
import androidx.test.espresso.intent.rule.IntentsRule
|
import androidx.test.espresso.intent.rule.IntentsRule
|
||||||
|
@ -233,4 +232,23 @@ class MarkdownTests {
|
||||||
checkTitleEquals("temp.md")
|
checkTitleEquals("temp.md")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun editAndViewHelpMarkdownTest() = runTest {
|
||||||
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
|
onMainScreen(composeRule) {
|
||||||
|
checkTitleEquals("Untitled.md")
|
||||||
|
typeMarkdown("# Header test")
|
||||||
|
checkMarkdownEquals("# Header test")
|
||||||
|
openDrawer()
|
||||||
|
} onNavigationDrawer {
|
||||||
|
openHelpPage()
|
||||||
|
} onHelpScreen {
|
||||||
|
checkTitleEquals("Help")
|
||||||
|
verifyH1("Headings/Titles")
|
||||||
|
pressBack()
|
||||||
|
} onMainScreen {
|
||||||
|
checkMarkdownEquals("# Header test")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,10 @@ fun onMainScreen(composeRule: ComposeTestRule, block: MainScreenRobot.() -> Unit
|
||||||
suspend fun CoroutineScope.onMainScreen(
|
suspend fun CoroutineScope.onMainScreen(
|
||||||
composeRule: ComposeTestRule,
|
composeRule: ComposeTestRule,
|
||||||
block: suspend MainScreenRobot.() -> Unit
|
block: suspend MainScreenRobot.() -> Unit
|
||||||
) {
|
): MainScreenRobot {
|
||||||
block.invoke(MainScreenRobot(composeRule))
|
val mainScreenRobot = MainScreenRobot(composeRule)
|
||||||
|
block.invoke(mainScreenRobot)
|
||||||
|
return mainScreenRobot
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainScreenRobot(private val composeRule: ComposeTestRule) :
|
class MainScreenRobot(private val composeRule: ComposeTestRule) :
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
package com.wbrawner.simplemarkdown.robot
|
package com.wbrawner.simplemarkdown.robot
|
||||||
|
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
|
||||||
class MarkdownInfoScreenRobot(private val composeTestRule: ComposeTestRule) :
|
class MarkdownInfoScreenRobot(private val composeTestRule: ComposeTestRule) :
|
||||||
TopAppBarRobot by ComposeTopAppBarRobot(composeTestRule),
|
TopAppBarRobot by ComposeTopAppBarRobot(composeTestRule),
|
||||||
WebViewRobot by EspressoWebViewRobot()
|
WebViewRobot by EspressoWebViewRobot() {
|
||||||
|
fun pressBack() = composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||||
|
|
||||||
|
infix fun onMainScreen(block: MainScreenRobot.() -> Unit) =
|
||||||
|
MainScreenRobot(composeTestRule).apply(block)
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
|
import android.app.ComponentCaller
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
@ -26,11 +28,13 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
@ -42,6 +46,7 @@ import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||||
|
@ -60,8 +65,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
setContent {
|
setContent {
|
||||||
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
|
||||||
.collectAsState()
|
|
||||||
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
LaunchedEffect(darkModePreference) {
|
LaunchedEffect(darkModePreference) {
|
||||||
|
@ -91,6 +94,10 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
LaunchedEffect(errorReporterPreference) {
|
LaunchedEffect(errorReporterPreference) {
|
||||||
ACRA.errorReporter.setEnabled(errorReporterPreference)
|
ACRA.errorReporter.setEnabled(errorReporterPreference)
|
||||||
}
|
}
|
||||||
|
val intentData = remember(intent) { intent?.data }
|
||||||
|
LaunchedEffect(intentData) {
|
||||||
|
viewModel.load(intentData?.toString())
|
||||||
|
}
|
||||||
val windowSizeClass = calculateWindowSizeClass(this)
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
SimpleMarkdownTheme {
|
SimpleMarkdownTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
@ -121,7 +128,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
|
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
|
||||||
enableAutosave = autosaveEnabled,
|
enableAutosave = autosaveEnabled,
|
||||||
enableReadability = readabilityEnabled
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Route.SETTINGS.path) {
|
composable(Route.SETTINGS.path) {
|
||||||
|
@ -155,6 +161,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||||
|
super.onNewIntent(intent, caller)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
intent.data?.let {
|
||||||
|
viewModel.load(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Route(
|
enum class Route(
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import com.wbrawner.simplemarkdown.core.LocalOnlyException
|
import com.wbrawner.simplemarkdown.core.LocalOnlyException
|
||||||
|
import com.wbrawner.simplemarkdown.model.Readability
|
||||||
import com.wbrawner.simplemarkdown.utility.FileHelper
|
import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||||
|
@ -23,21 +28,17 @@ import java.net.URI
|
||||||
|
|
||||||
data class EditorState(
|
data class EditorState(
|
||||||
val fileName: String = "Untitled.md",
|
val fileName: String = "Untitled.md",
|
||||||
val markdown: String = "",
|
val markdown: TextFieldValue = TextFieldValue(""),
|
||||||
val path: URI? = null,
|
val path: URI? = null,
|
||||||
val toast: ParameterizedText? = null,
|
val toast: ParameterizedText? = 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,
|
|
||||||
val lockSwiping: Boolean = false,
|
val lockSwiping: Boolean = false,
|
||||||
private val initialMarkdown: String = "",
|
val enableReadability: Boolean = false,
|
||||||
|
val initialMarkdown: String = "",
|
||||||
) {
|
) {
|
||||||
val dirty: Boolean
|
val dirty: Boolean
|
||||||
get() = markdown != initialMarkdown
|
get() = markdown.text != initialMarkdown
|
||||||
}
|
}
|
||||||
|
|
||||||
class MarkdownViewModel(
|
class MarkdownViewModel(
|
||||||
|
@ -51,27 +52,41 @@ class MarkdownViewModel(
|
||||||
init {
|
init {
|
||||||
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
|
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
|
||||||
.onEach {
|
.onEach {
|
||||||
_state.value = _state.value.copy(lockSwiping = it)
|
updateState { copy(lockSwiping = it) }
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||||
|
.onEach {
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
enableReadability = it,
|
||||||
|
markdown = markdown.copy(annotatedString = markdown.text.annotate(it)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMarkdown(markdown: String?) {
|
fun updateMarkdown(markdown: String?) = updateMarkdown(TextFieldValue(markdown.orEmpty()))
|
||||||
_state.value = _state.value.copy(
|
|
||||||
markdown = markdown ?: "",
|
fun updateMarkdown(markdown: TextFieldValue) {
|
||||||
)
|
updateState {
|
||||||
|
copy(
|
||||||
|
markdown = markdown.copy(annotatedString = markdown.text.annotate(enableReadability)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissToast() {
|
fun dismissToast() {
|
||||||
_state.value = _state.value.copy(toast = null)
|
updateState { copy(toast = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissAlert() {
|
fun dismissAlert() {
|
||||||
_state.value = _state.value.copy(alert = null)
|
updateState { copy(alert = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unsetSaveCallback() {
|
private fun unsetSaveCallback() {
|
||||||
_state.value = _state.value.copy(saveCallback = null)
|
updateState { copy(saveCallback = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun load(loadPath: String?) {
|
suspend fun load(loadPath: String?) {
|
||||||
|
@ -94,25 +109,30 @@ class MarkdownViewModel(
|
||||||
val uri = URI.create(actualLoadPath)
|
val uri = URI.create(actualLoadPath)
|
||||||
fileHelper.open(uri)
|
fileHelper.open(uri)
|
||||||
?.let { (name, content) ->
|
?.let { (name, content) ->
|
||||||
val currentState = _state.value
|
updateState {
|
||||||
_state.value = currentState.copy(
|
copy(
|
||||||
path = uri,
|
path = uri,
|
||||||
fileName = name,
|
fileName = name,
|
||||||
markdown = content,
|
markdown = TextFieldValue(content),
|
||||||
initialMarkdown = content,
|
initialMarkdown = content,
|
||||||
reloadToggle = currentState.reloadToggle.inv(),
|
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
|
||||||
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
|
)
|
||||||
)
|
}
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
||||||
} ?: throw IllegalStateException("Opened file was null")
|
} ?: throw IllegalStateException("Opened file was null")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
|
Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
|
||||||
_state.value = _state.value.copy(
|
updateState {
|
||||||
alert = AlertDialogModel(
|
copy(
|
||||||
text = ParameterizedText(R.string.file_load_error),
|
alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(ParameterizedText(R.string.ok), onClick = ::dismissAlert)
|
text = ParameterizedText(R.string.file_load_error),
|
||||||
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
|
ParameterizedText(R.string.ok),
|
||||||
|
onClick = ::dismissAlert
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,35 +144,44 @@ class MarkdownViewModel(
|
||||||
?: run {
|
?: run {
|
||||||
Timber.w("Attempted to save file with empty path")
|
Timber.w("Attempted to save file with empty path")
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
_state.value = _state.value.copy(saveCallback = ::unsetSaveCallback)
|
updateState {
|
||||||
|
copy(saveCallback = ::unsetSaveCallback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return@withLock false
|
return@withLock false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Timber.i("Saving file to $actualSavePath...")
|
Timber.i("Saving file to $actualSavePath...")
|
||||||
val currentState = _state.value
|
val currentState = _state.value
|
||||||
val name = fileHelper.save(actualSavePath, currentState.markdown)
|
val name = fileHelper.save(actualSavePath, currentState.markdown.text)
|
||||||
_state.value = currentState.copy(
|
updateState {
|
||||||
fileName = name,
|
currentState.copy(
|
||||||
path = actualSavePath,
|
fileName = name,
|
||||||
initialMarkdown = currentState.markdown,
|
path = actualSavePath,
|
||||||
toast = if (interactive) ParameterizedText(R.string.file_saved, arrayOf(name)) else null
|
initialMarkdown = currentState.markdown.text,
|
||||||
)
|
toast = if (interactive) ParameterizedText(
|
||||||
|
R.string.file_saved,
|
||||||
|
arrayOf(name)
|
||||||
|
) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
Timber.i("Saved file $name to uri $actualSavePath")
|
Timber.i("Saved file $name to uri $actualSavePath")
|
||||||
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
|
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
|
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to save file to $actualSavePath")
|
Timber.e(e, "Failed to save file to $actualSavePath")
|
||||||
_state.value = _state.value.copy(
|
updateState {
|
||||||
alert = AlertDialogModel(
|
copy(
|
||||||
text = ParameterizedText(R.string.file_save_error),
|
alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(
|
text = ParameterizedText(R.string.file_save_error),
|
||||||
text = ParameterizedText(R.string.ok),
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = ::dismissAlert
|
text = ParameterizedText(R.string.ok),
|
||||||
|
onClick = ::dismissAlert
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +213,7 @@ class MarkdownViewModel(
|
||||||
// to an internal storage location, thus marking it as not dirty, but no longer able to
|
// to an internal storage location, thus marking it as not dirty, but no longer able to
|
||||||
// access the file if the accidentally go to create a new file without properly saving
|
// access the file if the accidentally go to create a new file without properly saving
|
||||||
// the current one
|
// the current one
|
||||||
fileHelper.save(file, _state.value.markdown)
|
fileHelper.save(file, _state.value.markdown.text)
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = file
|
preferenceHelper[Preference.AUTOSAVE_URI] = file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,33 +222,35 @@ class MarkdownViewModel(
|
||||||
fun reset(untitledFileName: String, force: Boolean = false) {
|
fun reset(untitledFileName: String, force: Boolean = false) {
|
||||||
Timber.i("Resetting view model to default state")
|
Timber.i("Resetting view model to default state")
|
||||||
if (!force && _state.value.dirty) {
|
if (!force && _state.value.dirty) {
|
||||||
_state.value = _state.value.copy(alert = AlertDialogModel(
|
updateState {
|
||||||
text = ParameterizedText(R.string.prompt_save_changes),
|
copy(alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(
|
text = ParameterizedText(R.string.prompt_save_changes),
|
||||||
text = ParameterizedText(R.string.yes),
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = {
|
text = ParameterizedText(R.string.yes),
|
||||||
_state.value = _state.value.copy(
|
onClick = {
|
||||||
saveCallback = {
|
_state.value = _state.value.copy(
|
||||||
reset(untitledFileName, false)
|
saveCallback = {
|
||||||
}
|
reset(untitledFileName, false)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
),
|
}
|
||||||
dismissButton = AlertDialogModel.ButtonModel(
|
),
|
||||||
text = ParameterizedText(R.string.no),
|
dismissButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = {
|
text = ParameterizedText(R.string.no),
|
||||||
reset(untitledFileName, true)
|
onClick = {
|
||||||
}
|
reset(untitledFileName, true)
|
||||||
)
|
}
|
||||||
))
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_state.value =
|
updateState {
|
||||||
EditorState(
|
EditorState(
|
||||||
fileName = untitledFileName,
|
fileName = untitledFileName,
|
||||||
reloadToggle = _state.value.reloadToggle.inv(),
|
|
||||||
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
|
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Timber.i("Removing autosave uri from shared prefs")
|
Timber.i("Removing autosave uri from shared prefs")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
||||||
}
|
}
|
||||||
|
@ -228,6 +259,10 @@ class MarkdownViewModel(
|
||||||
preferenceHelper[Preference.LOCK_SWIPING] = enabled
|
preferenceHelper[Preference.LOCK_SWIPING] = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateState(block: EditorState.() -> EditorState) {
|
||||||
|
_state.value = _state.value.block()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun factory(
|
fun factory(
|
||||||
fileHelper: FileHelper,
|
fileHelper: FileHelper,
|
||||||
|
@ -270,4 +305,17 @@ data class ParameterizedText(@StringRes val text: Int, val params: Array<Any> =
|
||||||
result = 31 * result + params.contentHashCode()
|
result = 31 * result + params.contentHashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
var color = Color.Transparent
|
||||||
|
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
|
||||||
|
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
|
||||||
|
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
|
||||||
|
}
|
||||||
|
return annotated.toAnnotatedString()
|
||||||
}
|
}
|
|
@ -52,6 +52,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -61,7 +62,6 @@ import com.wbrawner.simplemarkdown.MarkdownViewModel
|
||||||
import com.wbrawner.simplemarkdown.ParameterizedText
|
import com.wbrawner.simplemarkdown.ParameterizedText
|
||||||
import com.wbrawner.simplemarkdown.R
|
import com.wbrawner.simplemarkdown.R
|
||||||
import com.wbrawner.simplemarkdown.Route
|
import com.wbrawner.simplemarkdown.Route
|
||||||
import com.wbrawner.simplemarkdown.utility.activity
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -76,21 +76,14 @@ fun MainScreen(
|
||||||
viewModel: MarkdownViewModel,
|
viewModel: MarkdownViewModel,
|
||||||
enableWideLayout: Boolean,
|
enableWideLayout: Boolean,
|
||||||
enableAutosave: Boolean,
|
enableAutosave: Boolean,
|
||||||
enableReadability: Boolean
|
|
||||||
) {
|
) {
|
||||||
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 markdown by viewModel.collectAsState(EditorState::markdown, TextFieldValue(""))
|
||||||
val reloadToggle by viewModel.collectAsState(EditorState::reloadToggle, 0)
|
|
||||||
val markdown by viewModel.collectAsState(EditorState::markdown, "")
|
|
||||||
val dirty by viewModel.collectAsState(EditorState::dirty, false)
|
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)
|
||||||
val lockSwiping by viewModel.collectAsState(EditorState::lockSwiping, false)
|
val lockSwiping by viewModel.collectAsState(EditorState::lockSwiping, false)
|
||||||
val intentData = LocalContext.current.activity?.intent?.data
|
|
||||||
LaunchedEffect(intentData) {
|
|
||||||
viewModel.load(intentData?.toString())
|
|
||||||
}
|
|
||||||
LaunchedEffect(enableAutosave) {
|
LaunchedEffect(enableAutosave) {
|
||||||
if (!enableAutosave) return@LaunchedEffect
|
if (!enableAutosave) return@LaunchedEffect
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
|
@ -102,8 +95,6 @@ fun MainScreen(
|
||||||
MainScreen(
|
MainScreen(
|
||||||
dirty = dirty,
|
dirty = dirty,
|
||||||
fileName = fileName,
|
fileName = fileName,
|
||||||
reloadToggle = reloadToggle,
|
|
||||||
initialMarkdown = initialMarkdown,
|
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = viewModel::updateMarkdown,
|
setMarkdown = viewModel::updateMarkdown,
|
||||||
lockSwiping = lockSwiping,
|
lockSwiping = lockSwiping,
|
||||||
|
@ -131,7 +122,6 @@ fun MainScreen(
|
||||||
viewModel.reset("Untitled.md")
|
viewModel.reset("Untitled.md")
|
||||||
},
|
},
|
||||||
enableWideLayout = enableWideLayout,
|
enableWideLayout = enableWideLayout,
|
||||||
enableReadability = enableReadability,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,10 +130,8 @@ fun MainScreen(
|
||||||
private fun MainScreen(
|
private fun MainScreen(
|
||||||
fileName: String = "Untitled.md",
|
fileName: String = "Untitled.md",
|
||||||
dirty: Boolean = false,
|
dirty: Boolean = false,
|
||||||
reloadToggle: Int = 0,
|
markdown: TextFieldValue = TextFieldValue(""),
|
||||||
initialMarkdown: String = "",
|
setMarkdown: (TextFieldValue) -> Unit = {},
|
||||||
markdown: String = "",
|
|
||||||
setMarkdown: (String) -> Unit = {},
|
|
||||||
lockSwiping: Boolean,
|
lockSwiping: Boolean,
|
||||||
toggleLockSwiping: (Boolean) -> Unit,
|
toggleLockSwiping: (Boolean) -> Unit,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
|
@ -157,7 +145,6 @@ private fun MainScreen(
|
||||||
saveCallback: (() -> Unit)? = null,
|
saveCallback: (() -> Unit)? = null,
|
||||||
reset: () -> Unit = {},
|
reset: () -> Unit = {},
|
||||||
enableWideLayout: Boolean = false,
|
enableWideLayout: Boolean = false,
|
||||||
enableReadability: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val openFileLauncher =
|
val openFileLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||||
|
@ -218,7 +205,7 @@ private fun MainScreen(
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown)
|
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown.text)
|
||||||
shareIntent.type = "text/plain"
|
shareIntent.type = "text/plain"
|
||||||
startActivity(
|
startActivity(
|
||||||
context, Intent.createChooser(
|
context, Intent.createChooser(
|
||||||
|
@ -291,10 +278,8 @@ 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,
|
|
||||||
)
|
)
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -306,7 +291,7 @@ private fun MainScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
markdown = markdown
|
markdown = markdown.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -316,12 +301,9 @@ 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,
|
|
||||||
reloadToggle = reloadToggle,
|
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -333,12 +315,9 @@ private fun MainScreen(
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun TabbedMarkdownEditor(
|
private fun TabbedMarkdownEditor(
|
||||||
initialMarkdown: String,
|
markdown: TextFieldValue,
|
||||||
markdown: String,
|
setMarkdown: (TextFieldValue) -> Unit,
|
||||||
setMarkdown: (String) -> Unit,
|
|
||||||
lockSwiping: Boolean,
|
lockSwiping: Boolean,
|
||||||
enableReadability: Boolean,
|
|
||||||
reloadToggle: Int,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior
|
scrollBehavior: TopAppBarScrollBehavior
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
@ -368,17 +347,15 @@ private fun TabbedMarkdownEditor(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
markdown = initialMarkdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
enableReadability = enableReadability,
|
|
||||||
reload = reloadToggle,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
markdown
|
markdown.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,23 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
@ -35,28 +25,14 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.simplemarkdown.R
|
import com.wbrawner.simplemarkdown.R
|
||||||
import com.wbrawner.simplemarkdown.model.Readability
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownTextField(
|
fun MarkdownTextField(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
markdown: String,
|
markdown: TextFieldValue,
|
||||||
setMarkdown: (String) -> Unit,
|
setMarkdown: (TextFieldValue) -> Unit,
|
||||||
reload: Int = 0,
|
|
||||||
enableReadability: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
|
|
||||||
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
val colors = TextFieldDefaults.colors(
|
val colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
@ -70,57 +46,37 @@ fun MarkdownTextField(
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Column(
|
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||||
modifier = modifier
|
BasicTextField(
|
||||||
.fillMaxSize()
|
value = markdown,
|
||||||
.imePadding()
|
modifier = modifier.imePadding(),
|
||||||
.verticalScroll(rememberScrollState())
|
onValueChange = setMarkdown,
|
||||||
) {
|
enabled = true,
|
||||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
readOnly = false,
|
||||||
BasicTextField(
|
textStyle = textStyle,
|
||||||
value = textFieldValue,
|
cursorBrush = SolidColor(colors.cursorColor),
|
||||||
modifier = Modifier.fillMaxSize(),
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||||
onValueChange = setTextFieldAndViewModelValues,
|
keyboardActions = KeyboardActions.Default,
|
||||||
enabled = true,
|
interactionSource = interactionSource,
|
||||||
readOnly = false,
|
singleLine = false,
|
||||||
textStyle = textStyle,
|
maxLines = Int.MAX_VALUE,
|
||||||
cursorBrush = SolidColor(colors.cursorColor),
|
minLines = 1,
|
||||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
decorationBox = @Composable { innerTextField ->
|
||||||
keyboardActions = KeyboardActions.Default,
|
// places leading icon, text field with label and placeholder, trailing icon
|
||||||
interactionSource = interactionSource,
|
TextFieldDefaults.DecorationBox(
|
||||||
singleLine = false,
|
value = markdown.text,
|
||||||
maxLines = Int.MAX_VALUE,
|
visualTransformation = VisualTransformation.None,
|
||||||
minLines = 1,
|
innerTextField = innerTextField,
|
||||||
decorationBox = @Composable { innerTextField ->
|
placeholder = {
|
||||||
// places leading icon, text field with label and placeholder, trailing icon
|
Text(stringResource(R.string.markdown_here))
|
||||||
TextFieldDefaults.DecorationBox(
|
},
|
||||||
value = textFieldValue.text,
|
singleLine = false,
|
||||||
visualTransformation = VisualTransformation.None,
|
enabled = true,
|
||||||
innerTextField = innerTextField,
|
interactionSource = interactionSource,
|
||||||
placeholder = {
|
colors = colors,
|
||||||
Text(stringResource(R.string.markdown_here))
|
contentPadding = PaddingValues(8.dp)
|
||||||
},
|
)
|
||||||
singleLine = false,
|
},
|
||||||
enabled = true,
|
)
|
||||||
interactionSource = interactionSource,
|
|
||||||
colors = colors,
|
|
||||||
contentPadding = PaddingValues(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()) {
|
|
||||||
var color = Color.Transparent
|
|
||||||
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
|
|
||||||
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
|
|
||||||
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
|
|
||||||
}
|
|
||||||
return annotated.toAnnotatedString()
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
|
@ -52,9 +53,9 @@ class MarkdownViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMarkdownUpdate() = runTest {
|
fun testMarkdownUpdate() = runTest {
|
||||||
assertEquals("", viewModel.state.value.markdown)
|
assertEquals("".asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
viewModel.updateMarkdown("Updated content")
|
viewModel.updateMarkdown("Updated content")
|
||||||
assertEquals("Updated content", viewModel.state.value.markdown)
|
assertEquals("Updated content".asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -68,11 +69,11 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Untitled.md")
|
val uri = URI.create("file:///home/user/Untitled.md")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
|
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
|
||||||
viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty)
|
viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty)
|
||||||
viewModelScope.advanceUntilIdle()
|
viewModel.load(null)
|
||||||
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
||||||
val (fileName, contents) = fileHelper.file
|
val (fileName, contents) = fileHelper.file
|
||||||
assertEquals(fileName, viewModel.state.value.fileName)
|
assertEquals(fileName, viewModel.state.value.fileName)
|
||||||
assertEquals(contents, viewModel.state.value.markdown)
|
assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -82,7 +83,7 @@ class MarkdownViewModelTest {
|
||||||
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
||||||
val (fileName, contents) = fileHelper.file
|
val (fileName, contents) = fileHelper.file
|
||||||
assertEquals(fileName, viewModel.state.value.fileName)
|
assertEquals(fileName, viewModel.state.value.fileName)
|
||||||
assertEquals(contents, viewModel.state.value.markdown)
|
assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -126,7 +127,7 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Saved.md")
|
val uri = URI.create("file:///home/user/Saved.md")
|
||||||
val testMarkdown = "# Test"
|
val testMarkdown = "# Test"
|
||||||
viewModel.updateMarkdown(testMarkdown)
|
viewModel.updateMarkdown(testMarkdown)
|
||||||
assertEquals(testMarkdown, viewModel.state.value.markdown)
|
assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
assertTrue(viewModel.save(uri))
|
assertTrue(viewModel.save(uri))
|
||||||
assertEquals("Saved.md", viewModel.state.value.fileName)
|
assertEquals("Saved.md", viewModel.state.value.fileName)
|
||||||
assertEquals(uri, fileHelper.savedData.last().uri)
|
assertEquals(uri, fileHelper.savedData.last().uri)
|
||||||
|
@ -139,7 +140,7 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Untitled.md")
|
val uri = URI.create("file:///home/user/Untitled.md")
|
||||||
val testMarkdown = "# Test"
|
val testMarkdown = "# Test"
|
||||||
viewModel.updateMarkdown(testMarkdown)
|
viewModel.updateMarkdown(testMarkdown)
|
||||||
assertEquals(testMarkdown, viewModel.state.value.markdown)
|
assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
fileHelper.errorOnSave = true
|
fileHelper.errorOnSave = true
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
assertFalse(viewModel.save(uri))
|
assertFalse(viewModel.save(uri))
|
||||||
|
@ -159,7 +160,7 @@ class MarkdownViewModelTest {
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
with(viewModel.state.value) {
|
with(viewModel.state.value) {
|
||||||
assertEquals("New.md", fileName)
|
assertEquals("New.md", fileName)
|
||||||
assertEquals("", markdown)
|
assertEquals("".asTextFieldValue(), markdown)
|
||||||
assertNull(path)
|
assertNull(path)
|
||||||
assertNull(saveCallback)
|
assertNull(saveCallback)
|
||||||
assertNull(alert)
|
assertNull(alert)
|
||||||
|
@ -181,7 +182,7 @@ class MarkdownViewModelTest {
|
||||||
requireNotNull(onClick)
|
requireNotNull(onClick)
|
||||||
onClick.invoke()
|
onClick.invoke()
|
||||||
}
|
}
|
||||||
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
|
assertEquals(viewModel.state.value, EditorState())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -200,7 +201,7 @@ class MarkdownViewModelTest {
|
||||||
viewModel.save(uri)
|
viewModel.save(uri)
|
||||||
assertNotNull(viewModel.state.value.saveCallback)
|
assertNotNull(viewModel.state.value.saveCallback)
|
||||||
requireNotNull(viewModel.state.value.saveCallback).invoke()
|
requireNotNull(viewModel.state.value.saveCallback).invoke()
|
||||||
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
|
assertEquals(viewModel.state.value, EditorState())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -217,7 +218,7 @@ class MarkdownViewModelTest {
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
with(viewModel.state.value) {
|
with(viewModel.state.value) {
|
||||||
assertEquals("Unsaved.md", fileName)
|
assertEquals("Unsaved.md", fileName)
|
||||||
assertEquals("", markdown)
|
assertEquals("".asTextFieldValue(), markdown)
|
||||||
assertNull(path)
|
assertNull(path)
|
||||||
assertNull(saveCallback)
|
assertNull(saveCallback)
|
||||||
assertNull(alert)
|
assertNull(alert)
|
||||||
|
@ -303,4 +304,6 @@ class MarkdownViewModelTest {
|
||||||
assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean)
|
assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean)
|
||||||
assertFalse(viewModel.state.value.lockSwiping)
|
assertFalse(viewModel.state.value.lockSwiping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.asTextFieldValue() = TextFieldValue(this)
|
||||||
}
|
}
|
Loading…
Reference in a new issue