Finish compose migration and remove unnecessary assets/code
This commit is contained in:
parent
493444aaab
commit
ae5b13dfd0
23 changed files with 458 additions and 982 deletions
|
@ -24,7 +24,6 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.getText
|
|||
import androidx.test.espresso.web.webdriver.Locator
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import com.wbrawner.simplemarkdown.view.activity.MainActivity
|
||||
import org.hamcrest.Matchers.containsString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
android:theme="@style/Theme.App.Starting"
|
||||
tools:ignore="AllowBackup"
|
||||
tools:targetApi="n">
|
||||
<activity android:name=".view.activity.MainActivity"
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name_short">
|
||||
<intent-filter>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package com.wbrawner.simplemarkdown.view.activity
|
||||
package com.wbrawner.simplemarkdown
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
|
@ -22,7 +20,8 @@ import androidx.compose.material.icons.filled.Help
|
|||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.PrivacyTip
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
|
@ -31,34 +30,28 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.wbrawner.plausible.android.Plausible
|
||||
import com.wbrawner.simplemarkdown.R
|
||||
import com.wbrawner.simplemarkdown.MarkdownApplication.Companion.fileHelper
|
||||
import com.wbrawner.simplemarkdown.MarkdownApplication.Companion.preferenceHelper
|
||||
import com.wbrawner.simplemarkdown.ui.MainScreen
|
||||
import com.wbrawner.simplemarkdown.ui.MarkdownInfoScreen
|
||||
import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
||||
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
||||
import com.wbrawner.simplemarkdown.utility.Preference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
const val KEY_AUTOSAVE = "autosave"
|
||||
|
||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
private val viewModel: MarkdownViewModel by viewModels()
|
||||
private val viewModel: MarkdownViewModel by viewModels { MarkdownViewModel.factory(fileHelper, preferenceHelper) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
val darkMode = withContext(Dispatchers.IO) {
|
||||
val darkModeValue = getStringPref(
|
||||
R.string.pref_key_dark_mode,
|
||||
getString(R.string.pref_value_auto)
|
||||
)
|
||||
val darkModeValue = preferenceHelper[Preference.DARK_MODE] as String
|
||||
|
||||
return@withContext when {
|
||||
darkModeValue.equals(
|
||||
|
@ -83,18 +76,20 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
|||
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val preferences = mutableMapOf<String, String>()
|
||||
preferences["Autosave"] = sharedPreferences.getBoolean("autosave", true).toString()
|
||||
val usingCustomCss = !getStringPref(R.string.pref_custom_css, null).isNullOrBlank()
|
||||
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
|
||||
val usingCustomCss = !(preferenceHelper[Preference.CUSTOM_CSS] as String?).isNullOrBlank()
|
||||
preferences["Custom CSS"] = usingCustomCss.toString()
|
||||
val darkModeSetting = getStringPref(R.string.pref_key_dark_mode, "auto").toString()
|
||||
val darkModeSetting = preferenceHelper[Preference.DARK_MODE].toString()
|
||||
preferences["Dark Mode"] = darkModeSetting
|
||||
preferences["Error Reports"] =
|
||||
getBooleanPref(R.string.pref_key_error_reports_enabled, true).toString()
|
||||
preferences["Readability"] = getBooleanPref(R.string.readability_enabled, false).toString()
|
||||
preferences["Error Reports"] = preferenceHelper[Preference.ERROR_REPORTS_ENABLED].toString()
|
||||
preferences["Readability"] = preferenceHelper[Preference.READABILITY_ENABLED].toString()
|
||||
Plausible.event("settings", props = preferences, url = "/")
|
||||
setContent {
|
||||
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
||||
.collectAsState()
|
||||
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||
.collectAsState()
|
||||
SimpleMarkdownTheme {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
|
@ -121,10 +116,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
|||
}
|
||||
) {
|
||||
composable(Route.EDITOR.path) {
|
||||
MainScreen(navController = navController, viewModel = viewModel)
|
||||
MainScreen(
|
||||
navController = navController,
|
||||
viewModel = viewModel,
|
||||
enableAutosave = autosaveEnabled,
|
||||
enableReadability = readabilityEnabled
|
||||
)
|
||||
}
|
||||
composable(Route.SETTINGS.path) {
|
||||
SettingsScreen(navController = navController)
|
||||
SettingsScreen(navController = navController, preferenceHelper)
|
||||
}
|
||||
composable(Route.SUPPORT.path) {
|
||||
SupportScreen(navController = navController)
|
||||
|
@ -155,16 +155,4 @@ enum class Route(
|
|||
HELP("/help", "Help", Icons.Default.Help),
|
||||
ABOUT("/about", "About", Icons.Default.Info),
|
||||
PRIVACY("/privacy", "Privacy", Icons.Default.PrivacyTip),
|
||||
}
|
||||
|
||||
fun Context.getBooleanPref(@StringRes key: Int, defaultValue: Boolean) =
|
||||
PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
|
||||
getString(key),
|
||||
defaultValue
|
||||
)
|
||||
|
||||
fun Context.getStringPref(@StringRes key: Int, defaultValue: String?) =
|
||||
PreferenceManager.getDefaultSharedPreferences(this).getString(
|
||||
getString(key),
|
||||
defaultValue
|
||||
)
|
||||
}
|
|
@ -3,7 +3,11 @@ package com.wbrawner.simplemarkdown
|
|||
import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import com.wbrawner.plausible.android.Plausible
|
||||
import com.wbrawner.simplemarkdown.utility.AndroidFileHelper
|
||||
import com.wbrawner.simplemarkdown.utility.AndroidPreferenceHelper
|
||||
import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||
import com.wbrawner.simplemarkdown.utility.PersistentTree
|
||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||
import com.wbrawner.simplemarkdown.utility.ReviewHelper
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -11,6 +15,7 @@ import timber.log.Timber
|
|||
import java.io.File
|
||||
|
||||
class MarkdownApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
|
||||
|
@ -40,5 +45,14 @@ class MarkdownApplication : Application() {
|
|||
"Flavor" to BuildConfig.FLAVOR,
|
||||
"App Version" to BuildConfig.VERSION_NAME
|
||||
))
|
||||
fileHelper = AndroidFileHelper(this)
|
||||
preferenceHelper = AndroidPreferenceHelper(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var fileHelper: FileHelper
|
||||
private set
|
||||
lateinit var preferenceHelper: PreferenceHelper
|
||||
private set
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
package com.wbrawner.simplemarkdown
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||
import com.wbrawner.simplemarkdown.utility.Preference
|
||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MarkdownViewModel(
|
||||
private val fileHelper: FileHelper,
|
||||
private val preferenceHelper: PreferenceHelper
|
||||
) : ViewModel() {
|
||||
private val _fileName = MutableStateFlow("Untitled.md")
|
||||
val fileName = _fileName.asStateFlow()
|
||||
private val _markdown = MutableStateFlow("")
|
||||
val markdown = _markdown.asStateFlow()
|
||||
private val path = MutableStateFlow<URI?>(null)
|
||||
private val isDirty = AtomicBoolean(false)
|
||||
private val _effects = MutableSharedFlow<Effect>()
|
||||
val effects = _effects.asSharedFlow()
|
||||
private val saveMutex = Mutex()
|
||||
|
||||
fun updateMarkdown(markdown: String?) = viewModelScope.launch {
|
||||
this@MarkdownViewModel._markdown.emit(markdown ?: "")
|
||||
isDirty.set(true)
|
||||
}
|
||||
|
||||
suspend fun load(loadPath: String?) {
|
||||
if (loadPath.isNullOrBlank()) {
|
||||
Timber.i("No URI provided to load, attempting to load last autosaved file")
|
||||
preferenceHelper[Preference.AUTOSAVE_URI]
|
||||
?.let {
|
||||
val autosaveUri = it as? String
|
||||
if (autosaveUri.isNullOrBlank()) {
|
||||
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
||||
} else {
|
||||
Timber.d("Using uri from shared preferences: $it")
|
||||
load(autosaveUri)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
val uri = URI.create(loadPath)
|
||||
fileHelper.open(uri)
|
||||
?.let { (name, content) ->
|
||||
path.emit(uri)
|
||||
_effects.emit(Effect.ClearText)
|
||||
_fileName.emit(name)
|
||||
_markdown.emit(content)
|
||||
isDirty.set(false)
|
||||
preferenceHelper[Preference.AUTOSAVE_URI] = loadPath
|
||||
} ?: _effects.emit(Effect.Error("Failed to open file at path: $loadPath"))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to open file at path: $loadPath")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(savePath: URI? = path.value, promptSavePath: Boolean = true): Boolean {
|
||||
return saveMutex.withLock {
|
||||
if (savePath == null) {
|
||||
Timber.w("Attempted to save file with empty path")
|
||||
if (promptSavePath) {
|
||||
_effects.emit(Effect.OpenSaveDialog {})
|
||||
}
|
||||
return false
|
||||
}
|
||||
try {
|
||||
val name = fileHelper.save(savePath, markdown.value)
|
||||
_fileName.emit(name)
|
||||
path.emit(savePath)
|
||||
isDirty.set(false)
|
||||
Timber.i("Saved file ${fileName.value} to uri $savePath")
|
||||
Timber.i("Persisting autosave uri in shared prefs: $savePath")
|
||||
preferenceHelper[Preference.AUTOSAVE_URI] = savePath
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
val message = "Failed to save file to $savePath"
|
||||
Timber.e(e, message)
|
||||
_effects.emit(Effect.Error(message))
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun autosave() {
|
||||
if (!isDirty.get()) {
|
||||
Timber.d("Ignoring autosave as contents haven't changed")
|
||||
return
|
||||
}
|
||||
if (saveMutex.isLocked) {
|
||||
Timber.i("Ignoring autosave since manual save is already in progress")
|
||||
return
|
||||
}
|
||||
val isAutoSaveEnabled = preferenceHelper[Preference.AUTOSAVE_ENABLED] as Boolean
|
||||
if (!isAutoSaveEnabled) {
|
||||
Timber.i("Ignoring autosave as autosave not enabled")
|
||||
return
|
||||
}
|
||||
if (!save(promptSavePath = false)) {
|
||||
// The user has left the app, with autosave enabled, and we don't already have a
|
||||
// Uri for them or for some reason we were unable to save to the original Uri. In
|
||||
// this case, we need to just save to internal file storage so that we can recover
|
||||
val file = File(fileHelper.defaultDirectory, fileName.value).toURI()
|
||||
Timber.i("No cached uri for autosave, saving to $file instead")
|
||||
save(file)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reset(untitledFileName: String, force: Boolean = false) {
|
||||
Timber.i("Resetting view model to default state")
|
||||
if (!force && isDirty.get()) {
|
||||
_effects.emit(Effect.Prompt(
|
||||
"Would you like to save your changes?",
|
||||
confirm = {
|
||||
viewModelScope.launch {
|
||||
_effects.emit(Effect.OpenSaveDialog {
|
||||
reset(untitledFileName, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel = {
|
||||
viewModelScope.launch {
|
||||
reset(untitledFileName, true)
|
||||
}
|
||||
}
|
||||
))
|
||||
return
|
||||
}
|
||||
_fileName.emit(untitledFileName)
|
||||
_markdown.emit("")
|
||||
path.emit(null)
|
||||
_effects.emit(Effect.ClearText)
|
||||
isDirty.set(false)
|
||||
Timber.i("Removing autosave uri from shared prefs")
|
||||
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun factory(fileHelper: FileHelper, preferenceHelper: PreferenceHelper): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(
|
||||
modelClass: Class<T>,
|
||||
extras: CreationExtras
|
||||
): T {
|
||||
return MarkdownViewModel(fileHelper, preferenceHelper) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data class OpenSaveDialog(val postSaveBlock: suspend () -> Unit) : Effect
|
||||
data class Prompt(val text: String, val confirm: () -> Unit, val cancel: () -> Unit) :
|
||||
Effect
|
||||
|
||||
data object ClearText : Effect
|
||||
data class Error(val text: String) : Effect
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package com.wbrawner.simplemarkdown.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.text.SpannableString
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.ArrowBack
|
|||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DismissibleDrawerSheet
|
||||
import androidx.compose.material3.DismissibleNavigationDrawer
|
||||
|
@ -35,9 +36,11 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -46,7 +49,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -59,21 +61,95 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.navigation.NavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.wbrawner.simplemarkdown.MarkdownViewModel
|
||||
import com.wbrawner.simplemarkdown.R
|
||||
import com.wbrawner.simplemarkdown.Route
|
||||
import com.wbrawner.simplemarkdown.model.Readability
|
||||
import com.wbrawner.simplemarkdown.view.activity.Route
|
||||
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.net.URI
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
||||
fun MainScreen(
|
||||
navController: NavController,
|
||||
viewModel: MarkdownViewModel,
|
||||
enableAutosave: Boolean,
|
||||
enableReadability: Boolean,
|
||||
) {
|
||||
var lockSwiping by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val fileName by viewModel.fileName.collectAsState()
|
||||
val openFileLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
coroutineScope.launch {
|
||||
viewModel.load(it.toString())
|
||||
}
|
||||
}
|
||||
val saveFileLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/*")) {
|
||||
it?.let {
|
||||
coroutineScope.launch {
|
||||
viewModel.save(URI.create(it.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var promptEffect by remember { mutableStateOf<MarkdownViewModel.Effect.Prompt?>(null) }
|
||||
var clearText by remember { mutableStateOf(0) }
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is MarkdownViewModel.Effect.OpenSaveDialog -> saveFileLauncher.launch(fileName)
|
||||
is MarkdownViewModel.Effect.Error -> errorMessage = effect.text
|
||||
is MarkdownViewModel.Effect.Prompt -> promptEffect = effect
|
||||
is MarkdownViewModel.Effect.ClearText -> clearText++
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(enableAutosave) {
|
||||
if (!enableAutosave) return@LaunchedEffect
|
||||
while (isActive) {
|
||||
delay(30_000)
|
||||
viewModel.autosave()
|
||||
}
|
||||
}
|
||||
errorMessage?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { errorMessage = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { errorMessage = null }) {
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
text = { Text(message) }
|
||||
)
|
||||
}
|
||||
promptEffect?.let { prompt ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { errorMessage = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
prompt.confirm()
|
||||
promptEffect = null
|
||||
}) {
|
||||
Text("Yes")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
prompt.cancel()
|
||||
promptEffect = null
|
||||
}) {
|
||||
Text("No")
|
||||
}
|
||||
},
|
||||
text = { Text(prompt.text) }
|
||||
)
|
||||
}
|
||||
MarkdownNavigationDrawer(navigate = { navController.navigate(it.path) }) { drawerState ->
|
||||
Scaffold(topBar = {
|
||||
val fileName by viewModel.fileName.collectAsState()
|
||||
val context = LocalContext.current
|
||||
MarkdownTopAppBar(title = fileName,
|
||||
backAsUp = false,
|
||||
|
@ -82,7 +158,7 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
|||
actions = {
|
||||
IconButton(onClick = {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdown.value)
|
||||
shareIntent.type = "text/plain"
|
||||
startActivity(
|
||||
context, Intent.createChooser(
|
||||
|
@ -101,16 +177,24 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
|||
onDismissRequest = { menuExpanded = false }) {
|
||||
DropdownMenuItem(text = { Text("New") }, onClick = {
|
||||
menuExpanded = false
|
||||
coroutineScope.launch {
|
||||
viewModel.reset("Untitled.md")
|
||||
}
|
||||
})
|
||||
DropdownMenuItem(text = { Text("Open") }, onClick = {
|
||||
menuExpanded = false
|
||||
openFileLauncher.launch(arrayOf("text/*"))
|
||||
})
|
||||
DropdownMenuItem(text = { Text("Save") }, onClick = {
|
||||
menuExpanded = false
|
||||
coroutineScope.launch {
|
||||
viewModel.save()
|
||||
}
|
||||
})
|
||||
DropdownMenuItem(text = { Text("Save as…") },
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
saveFileLauncher.launch(fileName)
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -127,13 +211,7 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
|||
}
|
||||
})
|
||||
}) { paddingValues ->
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
val context = LocalContext.current
|
||||
val enableReadability = remember {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(PREF_KEY_READABILITY, false)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -151,8 +229,8 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
|||
modifier = Modifier.weight(1f), state = pagerState,
|
||||
userScrollEnabled = !lockSwiping
|
||||
) { page ->
|
||||
val markdown by viewModel.markdownUpdates.collectAsState()
|
||||
var textFieldValue by remember {
|
||||
val markdown by viewModel.markdown.collectAsState()
|
||||
var textFieldValue by remember(clearText) {
|
||||
val annotatedMarkdown = if (enableReadability) {
|
||||
markdown.annotateReadability()
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package com.wbrawner.simplemarkdown.ui
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -13,8 +11,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
@ -29,27 +25,18 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.navigation.NavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||
|
||||
const val PREF_KEY_AUTOSAVE = "autosave"
|
||||
const val PREF_KEY_DARK_MODE = "darkMode"
|
||||
const val PREF_KEY_ERROR_REPORTS = "crashlytics.enable"
|
||||
const val PREF_KEY_ANALYTICS = "analytics.enable"
|
||||
const val PREF_KEY_READABILITY = "readability.enable"
|
||||
import com.wbrawner.simplemarkdown.utility.Preference
|
||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(navController: NavController) {
|
||||
fun SettingsScreen(navController: NavController, preferenceHelper: PreferenceHelper) {
|
||||
Scaffold(topBar = {
|
||||
MarkdownTopAppBar(title = "Settings", navController = navController)
|
||||
}) { paddingValues ->
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -60,37 +47,35 @@ fun SettingsScreen(navController: NavController) {
|
|||
title = "Autosave",
|
||||
enabledDescription = "Files will be saved automatically",
|
||||
disabledDescription = "Files will not be saved automatically",
|
||||
preferenceKey = PREF_KEY_AUTOSAVE,
|
||||
sharedPreferences = sharedPreferences
|
||||
preference = Preference.AUTOSAVE_ENABLED,
|
||||
preferenceHelper = preferenceHelper
|
||||
)
|
||||
ListPreference(
|
||||
title = "Dark mode",
|
||||
options = listOf("Auto", "Dark", "Light"),
|
||||
defaultValue = "Auto",
|
||||
preferenceKey = PREF_KEY_DARK_MODE,
|
||||
sharedPreferences = sharedPreferences
|
||||
options = listOf("auto", "dark", "light"),
|
||||
preference = Preference.DARK_MODE,
|
||||
preferenceHelper = preferenceHelper
|
||||
)
|
||||
BooleanPreference(
|
||||
title = "Send crash reports",
|
||||
enabledDescription = "Error reports will be sent",
|
||||
disabledDescription = "Error reports will not be sent",
|
||||
preferenceKey = PREF_KEY_ERROR_REPORTS,
|
||||
sharedPreferences = sharedPreferences
|
||||
preference = Preference.ERROR_REPORTS_ENABLED,
|
||||
preferenceHelper = preferenceHelper
|
||||
)
|
||||
BooleanPreference(
|
||||
title = "Send analytics",
|
||||
enabledDescription = "Analytics events will be sent",
|
||||
disabledDescription = "Analytics events will not be sent",
|
||||
preferenceKey = PREF_KEY_ANALYTICS,
|
||||
sharedPreferences = sharedPreferences
|
||||
preference = Preference.ANALYTICS_ENABLED,
|
||||
preferenceHelper = preferenceHelper
|
||||
)
|
||||
BooleanPreference(
|
||||
title = "Readability highlighting",
|
||||
enabledDescription = "Readability highlighting is on",
|
||||
disabledDescription = "Readability highlighting is off",
|
||||
preferenceKey = PREF_KEY_READABILITY,
|
||||
sharedPreferences = sharedPreferences,
|
||||
defaultValue = false
|
||||
preference = Preference.READABILITY_ENABLED,
|
||||
preferenceHelper = preferenceHelper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -101,16 +86,11 @@ fun BooleanPreference(
|
|||
title: String,
|
||||
enabledDescription: String,
|
||||
disabledDescription: String,
|
||||
preferenceKey: String,
|
||||
sharedPreferences: SharedPreferences,
|
||||
defaultValue: Boolean = true
|
||||
preference: Preference,
|
||||
preferenceHelper: PreferenceHelper
|
||||
) {
|
||||
var enabled by remember {
|
||||
mutableStateOf(
|
||||
sharedPreferences.getBoolean(
|
||||
preferenceKey, defaultValue
|
||||
)
|
||||
)
|
||||
mutableStateOf(preferenceHelper[preference] as Boolean)
|
||||
}
|
||||
BooleanPreference(title = title,
|
||||
enabledDescription = enabledDescription,
|
||||
|
@ -118,9 +98,7 @@ fun BooleanPreference(
|
|||
enabled = enabled,
|
||||
setEnabled = {
|
||||
enabled = it
|
||||
sharedPreferences.edit {
|
||||
putBoolean(preferenceKey, it)
|
||||
}
|
||||
preferenceHelper[preference] = it
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -156,27 +134,19 @@ fun BooleanPreference(
|
|||
fun ListPreference(
|
||||
title: String,
|
||||
options: List<String>,
|
||||
defaultValue: String,
|
||||
preferenceKey: String,
|
||||
sharedPreferences: SharedPreferences
|
||||
preference: Preference,
|
||||
preferenceHelper: PreferenceHelper
|
||||
) {
|
||||
var selected by remember {
|
||||
mutableStateOf(
|
||||
sharedPreferences.getString(
|
||||
preferenceKey, defaultValue
|
||||
) ?: defaultValue
|
||||
)
|
||||
mutableStateOf(preferenceHelper[preference] as String)
|
||||
}
|
||||
|
||||
ListPreference(title = title, options = options, selected = selected, setSelected = {
|
||||
selected = it
|
||||
sharedPreferences.edit {
|
||||
putString(preferenceKey, it)
|
||||
}
|
||||
preferenceHelper[preference] = it
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListPreference(
|
||||
title: String, options: List<String>, selected: String, setSelected: (String) -> Unit
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.wbrawner.simplemarkdown.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.net.URI
|
||||
|
||||
interface FileHelper {
|
||||
val defaultDirectory: File
|
||||
|
||||
/**
|
||||
* Opens a file at the given path
|
||||
* @param path The path of the file to open
|
||||
* @return A [Pair] of the file name to the file's contents
|
||||
*/
|
||||
suspend fun open(source: URI): Pair<String, String>?
|
||||
|
||||
/**
|
||||
* Saves the given content to the given path
|
||||
* @param path
|
||||
* @param content
|
||||
* @return The name of the saved file
|
||||
*/
|
||||
suspend fun save(destination: URI, content: String): String
|
||||
}
|
||||
|
||||
class AndroidFileHelper(private val context: Context) : FileHelper {
|
||||
override val defaultDirectory: File = context.filesDir
|
||||
|
||||
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val uri = source.toString().toUri()
|
||||
context.contentResolver.openFileDescriptor(uri, "r")
|
||||
?.use {
|
||||
uri.getName(context) to FileInputStream(it.fileDescriptor).reader()
|
||||
.use(Reader::readText)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(destination: URI, content: String): String = withContext(Dispatchers.IO) {
|
||||
val uri = destination.toString().toUri()
|
||||
context.contentResolver.openOutputStream(uri, "rwt")
|
||||
?.writer()
|
||||
?.use {
|
||||
it.write(content)
|
||||
}
|
||||
?: run {
|
||||
Timber.w("Open output stream returned null for uri: $uri")
|
||||
throw IOException("Failed to save to $destination")
|
||||
}
|
||||
return@withContext uri.getName(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.wbrawner.simplemarkdown.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
interface PreferenceHelper {
|
||||
operator fun get(preference: Preference): Any?
|
||||
|
||||
operator fun set(preference: Preference, value: Any?)
|
||||
|
||||
fun <T> observe(preference: Preference): StateFlow<T>
|
||||
}
|
||||
|
||||
class AndroidPreferenceHelper(context: Context, val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
|
||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val states = mapOf(
|
||||
Preference.ANALYTICS_ENABLED to MutableStateFlow(get(Preference.ANALYTICS_ENABLED)),
|
||||
Preference.AUTOSAVE_ENABLED to MutableStateFlow(get(Preference.AUTOSAVE_ENABLED)),
|
||||
Preference.AUTOSAVE_URI to MutableStateFlow(get(Preference.AUTOSAVE_URI)),
|
||||
Preference.CUSTOM_CSS to MutableStateFlow(get(Preference.CUSTOM_CSS)),
|
||||
Preference.DARK_MODE to MutableStateFlow(get(Preference.DARK_MODE)),
|
||||
Preference.ERROR_REPORTS_ENABLED to MutableStateFlow(get(Preference.ERROR_REPORTS_ENABLED)),
|
||||
Preference.READABILITY_ENABLED to MutableStateFlow(get(Preference.READABILITY_ENABLED)),
|
||||
)
|
||||
|
||||
override fun get(preference: Preference): Any? = sharedPreferences.all[preference.key]?: preference.default
|
||||
|
||||
override fun set(preference: Preference, value: Any?) {
|
||||
sharedPreferences.edit {
|
||||
when (value) {
|
||||
is Boolean -> putBoolean(preference.key, value)
|
||||
is Float -> putFloat(preference.key, value)
|
||||
is Int -> putInt(preference.key, value)
|
||||
is Long -> putLong(preference.key, value)
|
||||
is String -> putString(preference.key, value)
|
||||
null -> remove(preference.key)
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
states[preference]!!.emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T>
|
||||
}
|
||||
|
||||
enum class Preference(val key: String, val default: Any?) {
|
||||
ANALYTICS_ENABLED("analytics.enable", true),
|
||||
AUTOSAVE_ENABLED("autosave", true),
|
||||
AUTOSAVE_URI("autosave.uri", null),
|
||||
CUSTOM_CSS("pref.custom_css", null),
|
||||
DARK_MODE("darkMode", "Auto"),
|
||||
ERROR_REPORTS_ENABLED("crashlytics.enable", true),
|
||||
READABILITY_ENABLED("readability.enable", false)
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
//package com.wbrawner.simplemarkdown.view.fragment
|
||||
//
|
||||
//import android.annotation.SuppressLint
|
||||
//import android.graphics.Color
|
||||
//import android.os.Bundle
|
||||
//import android.text.Editable
|
||||
//import android.text.SpannableString
|
||||
//import android.text.TextWatcher
|
||||
//import android.text.style.BackgroundColorSpan
|
||||
//import android.view.LayoutInflater
|
||||
//import android.view.MotionEvent
|
||||
//import android.view.View
|
||||
//import android.view.ViewGroup
|
||||
//import android.widget.EditText
|
||||
//import android.widget.TextView
|
||||
//import androidx.core.widget.NestedScrollView
|
||||
//import androidx.fragment.app.Fragment
|
||||
//import androidx.fragment.app.viewModels
|
||||
//import androidx.lifecycle.Observer
|
||||
//import androidx.lifecycle.lifecycleScope
|
||||
//import androidx.preference.PreferenceManager
|
||||
//import com.wbrawner.simplemarkdown.R
|
||||
//import com.wbrawner.simplemarkdown.model.Readability
|
||||
//import com.wbrawner.simplemarkdown.utility.hideKeyboard
|
||||
//import com.wbrawner.simplemarkdown.utility.showKeyboard
|
||||
//import com.wbrawner.simplemarkdown.view.ViewPagerPage
|
||||
//import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
||||
//import kotlinx.coroutines.*
|
||||
//import timber.log.Timber
|
||||
//import kotlin.math.abs
|
||||
//
|
||||
//class EditFragment : Fragment(), ViewPagerPage {
|
||||
// private var markdownEditor: EditText? = null
|
||||
// private var markdownEditorScroller: NestedScrollView? = null
|
||||
// private val viewModel: MarkdownViewModel by viewModels({ requireParentFragment() })
|
||||
// private var readabilityWatcher: TextWatcher? = null
|
||||
// private val markdownWatcher = object : TextWatcher {
|
||||
// private var searchFor = ""
|
||||
//
|
||||
// override fun afterTextChanged(s: Editable?) {
|
||||
// val searchText = s.toString().trim()
|
||||
// if (searchText == searchFor)
|
||||
// return
|
||||
//
|
||||
// searchFor = searchText
|
||||
//
|
||||
// lifecycleScope.launch {
|
||||
// delay(50)
|
||||
// if (searchText != searchFor)
|
||||
// return@launch
|
||||
// viewModel.updateMarkdown(searchText)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// }
|
||||
//
|
||||
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @SuppressLint("ClickableViewAccessibility")
|
||||
// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
// savedInstanceState: Bundle?): View? =
|
||||
// inflater.inflate(R.layout.fragment_edit, container, false)
|
||||
//
|
||||
// @SuppressLint("ClickableViewAccessibility")
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated(view, savedInstanceState)
|
||||
// markdownEditor = view.findViewById(R.id.markdown_edit)
|
||||
// markdownEditorScroller = view.findViewById(R.id.markdown_edit_container)
|
||||
// markdownEditor?.addTextChangedListener(markdownWatcher)
|
||||
//
|
||||
// var touchDown = 0L
|
||||
// var oldX = 0f
|
||||
// var oldY = 0f
|
||||
// markdownEditorScroller!!.setOnTouchListener { _, event ->
|
||||
// // The ScrollView's onClickListener doesn't seem to be called, so I've had to
|
||||
// // implement a sort of custom click listener that checks that the tap was both quick
|
||||
// // and didn't drag.
|
||||
// when (event?.action) {
|
||||
// MotionEvent.ACTION_DOWN -> {
|
||||
// touchDown = System.currentTimeMillis()
|
||||
// oldX = event.rawX
|
||||
// oldY = event.rawY
|
||||
// }
|
||||
// MotionEvent.ACTION_UP -> {
|
||||
// if (System.currentTimeMillis() - touchDown < 150
|
||||
// && abs(event.rawX - oldX) < 25
|
||||
// && abs(event.rawY - oldY) < 25)
|
||||
// markdownEditor?.showKeyboard()
|
||||
// }
|
||||
// }
|
||||
// false
|
||||
// }
|
||||
// markdownEditor?.setText(viewModel.markdownUpdates.value)
|
||||
// viewModel.editorActions.observe(viewLifecycleOwner, Observer {
|
||||
// if (it.consumed.getAndSet(true)) return@Observer
|
||||
// if (it is MarkdownViewModel.EditorAction.Load) {
|
||||
// markdownEditor?.apply {
|
||||
// removeTextChangedListener(markdownWatcher)
|
||||
// setText(it.markdown)
|
||||
// addTextChangedListener(markdownWatcher)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// lifecycleScope.launch {
|
||||
// val enableReadability = withContext(Dispatchers.IO) {
|
||||
// context?.let {
|
||||
// PreferenceManager.getDefaultSharedPreferences(it)
|
||||
// .getBoolean(getString(R.string.readability_enabled), false)
|
||||
// } ?: false
|
||||
// }
|
||||
// if (enableReadability) {
|
||||
// if (readabilityWatcher == null) {
|
||||
// readabilityWatcher = ReadabilityTextWatcher()
|
||||
// }
|
||||
// markdownEditor?.addTextChangedListener(readabilityWatcher)
|
||||
// } else {
|
||||
// readabilityWatcher?.let {
|
||||
// markdownEditor?.removeTextChangedListener(it)
|
||||
// }
|
||||
// readabilityWatcher = null
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onSelected() {
|
||||
// markdownEditor?.showKeyboard()
|
||||
// }
|
||||
//
|
||||
// override fun onDeselected() {
|
||||
// markdownEditor?.hideKeyboard()
|
||||
// }
|
||||
//
|
||||
// inner class ReadabilityTextWatcher : TextWatcher {
|
||||
// private var previousValue = ""
|
||||
// private var searchFor = ""
|
||||
//
|
||||
// override fun afterTextChanged(s: Editable?) {
|
||||
// val searchText = s.toString().trim()
|
||||
// if (searchText == searchFor)
|
||||
// return
|
||||
//
|
||||
// searchFor = searchText
|
||||
//
|
||||
// lifecycleScope.launch {
|
||||
// delay(250)
|
||||
// if (searchText != searchFor)
|
||||
// return@launch
|
||||
// val start = System.currentTimeMillis()
|
||||
// if (searchFor.isEmpty()) return@launch
|
||||
// if (previousValue == searchFor) return@launch
|
||||
// val readability = Readability(searchFor)
|
||||
// val span = SpannableString(searchFor)
|
||||
// for (sentence in readability.sentences()) {
|
||||
// var color = Color.TRANSPARENT
|
||||
// if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42)
|
||||
// if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66)
|
||||
// Timber.d("Sentence start: ${sentence.start()} end: ${
|
||||
// sentence
|
||||
// .end()
|
||||
// }")
|
||||
// span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0)
|
||||
// }
|
||||
// markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE)
|
||||
// previousValue = searchFor
|
||||
// val timeTakenMs = System.currentTimeMillis() - start
|
||||
// Timber.d("Handled markdown in $timeTakenMs ms")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// }
|
||||
//
|
||||
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -1,335 +0,0 @@
|
|||
//package com.wbrawner.simplemarkdown.view.fragment
|
||||
//
|
||||
//import android.app.Activity
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.content.pm.PackageManager
|
||||
//import android.content.res.Configuration
|
||||
//import android.os.Build
|
||||
//import android.os.Bundle
|
||||
//import android.view.*
|
||||
//import android.webkit.MimeTypeMap
|
||||
//import android.widget.Toast
|
||||
//import androidx.appcompat.app.AlertDialog
|
||||
//import androidx.appcompat.app.AppCompatActivity
|
||||
//import androidx.core.app.ActivityCompat
|
||||
//import androidx.fragment.app.Fragment
|
||||
//import androidx.fragment.app.viewModels
|
||||
//import androidx.lifecycle.lifecycleScope
|
||||
//import androidx.navigation.fragment.findNavController
|
||||
//import androidx.navigation.ui.AppBarConfiguration
|
||||
//import androidx.navigation.ui.setupWithNavController
|
||||
//import androidx.preference.PreferenceManager
|
||||
//import com.wbrawner.plausible.android.Plausible
|
||||
//import com.wbrawner.simplemarkdown.R
|
||||
//import com.wbrawner.simplemarkdown.databinding.FragmentMainBinding
|
||||
//import com.wbrawner.simplemarkdown.utility.ErrorHandler
|
||||
//import com.wbrawner.simplemarkdown.utility.errorHandlerImpl
|
||||
//import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter
|
||||
//import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.launch
|
||||
//import kotlinx.coroutines.withContext
|
||||
//import timber.log.Timber
|
||||
//
|
||||
//class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
//
|
||||
// private val viewModel: MarkdownViewModel by viewModels()
|
||||
// private var appBarConfiguration: AppBarConfiguration? = null
|
||||
// private val errorHandler: ErrorHandler by errorHandlerImpl()
|
||||
// private var _binding: FragmentMainBinding? = null
|
||||
// private val binding: FragmentMainBinding
|
||||
// get() = _binding!!
|
||||
//
|
||||
// override fun onAttach(context: Context) {
|
||||
// super.onAttach(context)
|
||||
// if (context !is Activity) return
|
||||
// lifecycleScope.launch {
|
||||
// viewModel.load(context, context.intent?.data)
|
||||
// context.intent?.data = null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// super.onCreate(savedInstanceState)
|
||||
// setHasOptionsMenu(true)
|
||||
// }
|
||||
//
|
||||
// override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// inflater.inflate(R.menu.menu_edit, menu)
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// menu.findItem(R.id.action_save_as)
|
||||
// ?.setAlphabeticShortcut('S', KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onCreateView(
|
||||
// inflater: LayoutInflater,
|
||||
// container: ViewGroup?,
|
||||
// savedInstanceState: Bundle?
|
||||
// ): View {
|
||||
// _binding = FragmentMainBinding.inflate(inflater, container, false)
|
||||
// return binding.root
|
||||
// }
|
||||
//
|
||||
// override fun onDestroyView() {
|
||||
// super.onDestroyView()
|
||||
// _binding = null
|
||||
// }
|
||||
//
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// with(findNavController()) {
|
||||
// appBarConfiguration = AppBarConfiguration(graph, binding.drawerLayout)
|
||||
// binding.toolbar.setupWithNavController(this, appBarConfiguration!!)
|
||||
// (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
// binding.navigationView.setupWithNavController(this)
|
||||
// }
|
||||
// val adapter = EditPagerAdapter(childFragmentManager, view.context)
|
||||
// binding.pager.adapter = adapter
|
||||
// binding.pager.addOnPageChangeListener(adapter)
|
||||
// binding.pager.pageMargin = 1
|
||||
// binding.pager.setPageMarginDrawable(R.color.colorAccent)
|
||||
// binding.tabLayout.setupWithViewPager(binding.pager)
|
||||
// if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// binding.tabLayout.visibility = View.GONE
|
||||
// }
|
||||
// @Suppress("CAST_NEVER_SUCCEEDS")
|
||||
// viewModel.fileName.observe(viewLifecycleOwner) {
|
||||
// binding.toolbar.title = it
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// return when (item.itemId) {
|
||||
// android.R.id.home -> {
|
||||
// binding.drawerLayout.open()
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_save -> {
|
||||
// Timber.d("Save clicked")
|
||||
// lifecycleScope.launch {
|
||||
// if (!viewModel.save(requireContext())) {
|
||||
// requestFileOp(REQUEST_SAVE_FILE)
|
||||
// } else {
|
||||
// Toast.makeText(
|
||||
// requireContext(),
|
||||
// getString(R.string.file_saved, viewModel.fileName.value),
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// }
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_save_as -> {
|
||||
// Timber.d("Save as clicked")
|
||||
// requestFileOp(REQUEST_SAVE_FILE)
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_share -> {
|
||||
// Timber.d("Share clicked")
|
||||
// val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
// shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
|
||||
// shareIntent.type = "text/plain"
|
||||
// startActivity(
|
||||
// Intent.createChooser(
|
||||
// shareIntent,
|
||||
// getString(R.string.share_file)
|
||||
// )
|
||||
// )
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_load -> {
|
||||
// Timber.d("Load clicked")
|
||||
// requestFileOp(REQUEST_OPEN_FILE)
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_new -> {
|
||||
// Timber.d("New clicked")
|
||||
// promptSaveOrDiscardChanges()
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// R.id.action_lock_swipe -> {
|
||||
// Timber.d("Lock swiping clicked")
|
||||
// item.isChecked = !item.isChecked
|
||||
// binding.pager.setSwipeLocked(item.isChecked)
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// else -> super.onOptionsItemSelected(item)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onStart() {
|
||||
// super.onStart()
|
||||
// Plausible.pageView("")
|
||||
// lifecycleScope.launch {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// val enableErrorReports =
|
||||
// PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
// .getBoolean(getString(R.string.pref_key_error_reports_enabled), true)
|
||||
// Timber.d("MainFragment started. Error reports enabled? $enableErrorReports")
|
||||
// errorHandler.enable(enableErrorReports)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onStop() {
|
||||
// super.onStop()
|
||||
// val context = context?.applicationContext ?: return
|
||||
// lifecycleScope.launch {
|
||||
// viewModel.autosave(context, PreferenceManager.getDefaultSharedPreferences(context))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
// super.onConfigurationChanged(newConfig)
|
||||
// if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// Timber.d("Orientation changed to landscape, hiding tabs")
|
||||
// binding.tabLayout.visibility = View.GONE
|
||||
// } else {
|
||||
// Timber.d("Orientation changed to portrait, showing tabs")
|
||||
// binding.tabLayout.visibility = View.VISIBLE
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onRequestPermissionsResult(
|
||||
// requestCode: Int,
|
||||
// permissions: Array<String>,
|
||||
// grantResults: IntArray
|
||||
// ) {
|
||||
// when (requestCode) {
|
||||
// REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> {
|
||||
// // If request is cancelled, the result arrays are empty.
|
||||
// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// // Permission granted, open file save dialog
|
||||
// Timber.d("Storage permissions granted")
|
||||
// requestFileOp(requestCode)
|
||||
// } else {
|
||||
// // Permission denied, do nothing
|
||||
// Timber.d("Storage permissions denied, unable to save or load files")
|
||||
// context?.let {
|
||||
// Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT)
|
||||
// .show()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// when (requestCode) {
|
||||
// REQUEST_OPEN_FILE -> {
|
||||
// if (resultCode != Activity.RESULT_OK || data?.data == null) {
|
||||
// Timber.w(
|
||||
// "Unable to open file. Result ok? %b Intent uri: %s",
|
||||
// resultCode == Activity.RESULT_OK,
|
||||
// data?.data?.toString()
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// lifecycleScope.launch {
|
||||
// context?.let {
|
||||
// if (!viewModel.load(it, data.data)) {
|
||||
// Toast.makeText(it, R.string.file_load_error, Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// REQUEST_SAVE_FILE -> {
|
||||
// if (resultCode != Activity.RESULT_OK || data?.data == null) {
|
||||
// Timber.w(
|
||||
// "Unable to save file. Result ok? %b Intent uri: %s",
|
||||
// resultCode == Activity.RESULT_OK,
|
||||
// data?.data?.toString()
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// lifecycleScope.launch {
|
||||
// context?.let {
|
||||
// viewModel.save(it, data.data)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// super.onActivityResult(requestCode, resultCode, data)
|
||||
// }
|
||||
//
|
||||
// private fun promptSaveOrDiscardChanges() {
|
||||
// if (!viewModel.shouldPromptSave()) {
|
||||
// viewModel.reset(
|
||||
// "Untitled.md",
|
||||
// PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
// val context = context ?: run {
|
||||
// Timber.w("Context is null, unable to show prompt for save or discard")
|
||||
// return
|
||||
// }
|
||||
// AlertDialog.Builder(context)
|
||||
// .setTitle(R.string.save_changes)
|
||||
// .setMessage(R.string.prompt_save_changes)
|
||||
// .setNegativeButton(R.string.action_discard) { _, _ ->
|
||||
// Timber.d("Discarding changes")
|
||||
// viewModel.reset(
|
||||
// "Untitled.md",
|
||||
// PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
// )
|
||||
// }
|
||||
// .setPositiveButton(R.string.action_save) { _, _ ->
|
||||
// Timber.d("Saving changes")
|
||||
// requestFileOp(REQUEST_SAVE_FILE)
|
||||
// }
|
||||
// .create()
|
||||
// .show()
|
||||
// }
|
||||
//
|
||||
// private fun requestFileOp(requestType: Int) {
|
||||
// val intent = when (requestType) {
|
||||
// REQUEST_SAVE_FILE -> {
|
||||
// Timber.d("Requesting save op")
|
||||
// Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
// type = "text/markdown"
|
||||
// putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// REQUEST_OPEN_FILE -> {
|
||||
// Timber.d("Requesting open op")
|
||||
// Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
// type = "*/*"
|
||||
// if (MimeTypeMap.getSingleton().hasMimeType("md")) {
|
||||
// // If the device doesn't recognize markdown files then we're not going to be
|
||||
// // able to open them at all, so there's no sense in filtering them out.
|
||||
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown"))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// else -> {
|
||||
// Timber.w("Ignoring unknown file op request: $requestType")
|
||||
// null
|
||||
// }
|
||||
// } ?: return
|
||||
// intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
// startActivityForResult(
|
||||
// intent,
|
||||
// requestType
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// companion object {
|
||||
// // Request codes
|
||||
// const val REQUEST_OPEN_FILE = 1
|
||||
// const val REQUEST_SAVE_FILE = 2
|
||||
// }
|
||||
//}
|
|
@ -1,96 +0,0 @@
|
|||
//package com.wbrawner.simplemarkdown.view.fragment
|
||||
//
|
||||
//import android.content.Context
|
||||
//import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
//import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
//import android.os.Bundle
|
||||
//import android.view.LayoutInflater
|
||||
//import android.view.View
|
||||
//import android.view.ViewGroup
|
||||
//import android.webkit.WebView
|
||||
//import androidx.appcompat.app.AppCompatDelegate
|
||||
//import androidx.fragment.app.Fragment
|
||||
//import androidx.fragment.app.viewModels
|
||||
//import androidx.lifecycle.lifecycleScope
|
||||
//import androidx.preference.PreferenceManager
|
||||
//import com.wbrawner.simplemarkdown.BuildConfig
|
||||
//import com.wbrawner.simplemarkdown.R
|
||||
//import com.wbrawner.simplemarkdown.utility.toHtml
|
||||
//import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
||||
//import kotlinx.coroutines.*
|
||||
//
|
||||
//class PreviewFragment : Fragment() {
|
||||
// private val viewModel: MarkdownViewModel by viewModels({ requireParentFragment() })
|
||||
// private var markdownPreview: WebView? = null
|
||||
// private var style: String = ""
|
||||
//
|
||||
// override fun onCreateView(
|
||||
// inflater: LayoutInflater,
|
||||
// container: ViewGroup?,
|
||||
// savedInstanceState: Bundle?
|
||||
// ): View? = inflater.inflate(R.layout.fragment_preview, container, false)
|
||||
//
|
||||
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// markdownPreview = view.findViewById(R.id.markdown_view)
|
||||
// WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||
// lifecycleScope.launch {
|
||||
// val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
|
||||
// AppCompatDelegate.MODE_NIGHT_YES
|
||||
// || requireContext().resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
|
||||
// val defaultCssId = if (isNightMode) {
|
||||
// R.string.pref_custom_css_default_dark
|
||||
// } else {
|
||||
// R.string.pref_custom_css_default
|
||||
// }
|
||||
// val css = withContext(Dispatchers.IO) {
|
||||
// val context = context ?: return@withContext null
|
||||
// @Suppress("ConstantConditionIf")
|
||||
// if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
||||
// context.getString(defaultCssId)
|
||||
// } else {
|
||||
// PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// .getString(
|
||||
// getString(R.string.pref_custom_css),
|
||||
// getString(defaultCssId)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// style = String.format(FORMAT_CSS, css ?: "")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onAttach(context: Context) {
|
||||
// super.onAttach(context)
|
||||
// updateWebContent(viewModel.markdownUpdates.value ?: "")
|
||||
//// viewModel.markdownUpdates.observe(this, {
|
||||
//// updateWebContent(it)
|
||||
//// })
|
||||
// }
|
||||
//
|
||||
// private fun updateWebContent(markdown: String) {
|
||||
// markdownPreview?.post {
|
||||
// lifecycleScope.launch {
|
||||
// markdownPreview?.loadDataWithBaseURL(null,
|
||||
// style + markdown.toHtml(),
|
||||
// "text/html",
|
||||
// "UTF-8", null
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onDestroyView() {
|
||||
// markdownPreview?.let {
|
||||
// (it.parent as ViewGroup).removeView(it)
|
||||
// it.destroy()
|
||||
// markdownPreview = null
|
||||
// }
|
||||
// super.onDestroyView()
|
||||
// }
|
||||
//
|
||||
// companion object {
|
||||
// var FORMAT_CSS = "<style>" +
|
||||
// "%s" +
|
||||
// "</style>"
|
||||
// }
|
||||
//}
|
|
@ -1,178 +0,0 @@
|
|||
package com.wbrawner.simplemarkdown.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.wbrawner.simplemarkdown.utility.getName
|
||||
import com.wbrawner.simplemarkdown.view.activity.KEY_AUTOSAVE
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.Reader
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
const val PREF_KEY_AUTOSAVE_URI = "autosave.uri"
|
||||
|
||||
class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel() {
|
||||
val fileName = MutableStateFlow("Untitled.md")
|
||||
val markdownUpdates = MutableStateFlow("")
|
||||
val editorActions = MutableLiveData<EditorAction>()
|
||||
val uri = MutableLiveData<Uri?>()
|
||||
private val isDirty = AtomicBoolean(false)
|
||||
private val saveMutex = Mutex()
|
||||
|
||||
init {
|
||||
markdownUpdates
|
||||
}
|
||||
|
||||
fun updateMarkdown(markdown: String?) = viewModelScope.launch {
|
||||
markdownUpdates.emit(markdown ?: "")
|
||||
isDirty.set(true)
|
||||
}
|
||||
|
||||
suspend fun load(
|
||||
context: Context,
|
||||
uri: Uri?,
|
||||
sharedPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
): Boolean {
|
||||
if (uri == null) {
|
||||
timber.i("No URI provided to load, attempting to load last autosaved file")
|
||||
sharedPrefs.getString(PREF_KEY_AUTOSAVE_URI, null)
|
||||
?.let {
|
||||
Timber.d("Using uri from shared preferences: $it")
|
||||
return load(context, Uri.parse(it), sharedPrefs)
|
||||
} ?: return false
|
||||
}
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||
val fileInput = FileInputStream(it.fileDescriptor)
|
||||
val fileName = uri.getName(context)
|
||||
val content = fileInput.reader().use(Reader::readText)
|
||||
if (content.isBlank()) {
|
||||
// If we don't get anything back, then we can assume that reading the file failed
|
||||
timber.i("Ignoring load for empty file $fileName from $fileInput")
|
||||
return@withContext false
|
||||
}
|
||||
editorActions.postValue(EditorAction.Load(content))
|
||||
markdownUpdates.emit(content)
|
||||
this@MarkdownViewModel.fileName.emit(fileName)
|
||||
this@MarkdownViewModel.uri.postValue(uri)
|
||||
timber.i("Loaded file $fileName from $fileInput")
|
||||
timber.v("File contents:\n$content")
|
||||
isDirty.set(false)
|
||||
timber.i("Persisting autosave uri in shared prefs: $uri")
|
||||
sharedPrefs.edit()
|
||||
.putString(PREF_KEY_AUTOSAVE_URI, uri.toString())
|
||||
.apply()
|
||||
true
|
||||
} ?: run {
|
||||
timber.w("Open file descriptor returned null for uri: $uri")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
timber.e(e, "Failed to open file descriptor for uri: $uri")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(
|
||||
context: Context,
|
||||
givenUri: Uri? = null,
|
||||
sharedPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
): Boolean = saveMutex.withLock {
|
||||
val uri = givenUri?.let {
|
||||
timber.i("Saving file with given uri: $it")
|
||||
it
|
||||
} ?: this.uri.value?.let {
|
||||
timber.i("Saving file with cached uri: $it")
|
||||
it
|
||||
} ?: run {
|
||||
timber.w("Save called with no uri")
|
||||
return@save false
|
||||
}
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val fileName = uri.getName(context)
|
||||
context.contentResolver.openOutputStream(uri, "rwt")
|
||||
?.writer()
|
||||
?.use {
|
||||
it.write(markdownUpdates.value ?: "")
|
||||
}
|
||||
?: run {
|
||||
timber.w("Open output stream returned null for uri: $uri")
|
||||
return@withContext false
|
||||
}
|
||||
this@MarkdownViewModel.fileName.emit(fileName)
|
||||
this@MarkdownViewModel.uri.postValue(uri)
|
||||
isDirty.set(false)
|
||||
timber.i("Saved file $fileName to uri $uri")
|
||||
timber.i("Persisting autosave uri in shared prefs: $uri")
|
||||
sharedPrefs.edit()
|
||||
.putString(PREF_KEY_AUTOSAVE_URI, uri.toString())
|
||||
.apply()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
timber.e(e, "Failed to save file at uri: $uri")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun autosave(context: Context, sharedPrefs: SharedPreferences) {
|
||||
if (saveMutex.isLocked) {
|
||||
timber.i("Ignoring autosave since manual save is already in progress")
|
||||
return
|
||||
}
|
||||
val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
|
||||
timber.d("Autosave called. isEnabled? $isAutoSaveEnabled")
|
||||
if (!isDirty.get() || !isAutoSaveEnabled) {
|
||||
timber.i("Ignoring call to autosave. Contents haven't changed or autosave not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
if (save(context)) {
|
||||
timber.i("Autosave with cached uri succeeded: ${uri.value}")
|
||||
} else {
|
||||
// The user has left the app, with autosave enabled, and we don't already have a
|
||||
// Uri for them or for some reason we were unable to save to the original Uri. In
|
||||
// this case, we need to just save to internal file storage so that we can recover
|
||||
val fileUri = Uri.fromFile(File(context.filesDir, fileName.value ?: "Untitled.md"))
|
||||
timber.i("No cached uri for autosave, saving to $fileUri instead")
|
||||
save(context, fileUri)
|
||||
}
|
||||
}
|
||||
|
||||
fun reset(untitledFileName: String, sharedPrefs: SharedPreferences) = viewModelScope.launch{
|
||||
timber.i("Resetting view model to default state")
|
||||
fileName.tryEmit(untitledFileName)
|
||||
uri.postValue(null)
|
||||
markdownUpdates.emit("")
|
||||
editorActions.postValue(EditorAction.Load(""))
|
||||
isDirty.set(false)
|
||||
timber.i("Removing autosave uri from shared prefs")
|
||||
sharedPrefs.edit {
|
||||
remove(PREF_KEY_AUTOSAVE_URI)
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldPromptSave() = isDirty.get()
|
||||
|
||||
sealed class EditorAction {
|
||||
val consumed = AtomicBoolean(false)
|
||||
|
||||
data class Load(val markdown: String) : EditorAction()
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorOnBackground"
|
||||
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorOnBackground"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||
</vector>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:top="0dp"
|
||||
android:bottom="0dp"
|
||||
android:left="0dp"
|
||||
android:right="0dp"
|
||||
android:drawable="@color/colorPrimary"/>
|
||||
<item
|
||||
android:drawable="@drawable/splash_fg"
|
||||
android:gravity="center"/>
|
||||
</layer-list>
|
|
@ -9,7 +9,7 @@ import androidx.core.content.edit
|
|||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.play.core.review.ReviewManager
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.wbrawner.simplemarkdown.view.activity.MainActivity
|
||||
import com.wbrawner.simplemarkdown.MainActivity
|
||||
import timber.log.Timber
|
||||
|
||||
private const val KEY_TIME_IN_APP = "timeInApp"
|
||||
|
|
Loading…
Reference in a new issue