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.espresso.web.webdriver.Locator
|
||||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import com.wbrawner.simplemarkdown.view.activity.MainActivity
|
|
||||||
import org.hamcrest.Matchers.containsString
|
import org.hamcrest.Matchers.containsString
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
tools:ignore="AllowBackup"
|
tools:ignore="AllowBackup"
|
||||||
tools:targetApi="n">
|
tools:targetApi="n">
|
||||||
<activity android:name=".view.activity.MainActivity"
|
<activity android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name_short">
|
android:label="@string/app_name_short">
|
||||||
<intent-filter>
|
<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.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
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.Info
|
||||||
import androidx.compose.material.icons.filled.PrivacyTip
|
import androidx.compose.material.icons.filled.PrivacyTip
|
||||||
import androidx.compose.material.icons.filled.Settings
|
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.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
@ -31,34 +30,28 @@ 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
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.wbrawner.plausible.android.Plausible
|
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.MainScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.MarkdownInfoScreen
|
import com.wbrawner.simplemarkdown.ui.MarkdownInfoScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
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.viewmodel.MarkdownViewModel
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
const val KEY_AUTOSAVE = "autosave"
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val darkMode = withContext(Dispatchers.IO) {
|
val darkMode = withContext(Dispatchers.IO) {
|
||||||
val darkModeValue = getStringPref(
|
val darkModeValue = preferenceHelper[Preference.DARK_MODE] as String
|
||||||
R.string.pref_key_dark_mode,
|
|
||||||
getString(R.string.pref_value_auto)
|
|
||||||
)
|
|
||||||
|
|
||||||
return@withContext when {
|
return@withContext when {
|
||||||
darkModeValue.equals(
|
darkModeValue.equals(
|
||||||
|
@ -83,18 +76,20 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
AppCompatDelegate.setDefaultNightMode(darkMode)
|
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||||
}
|
}
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
val preferences = mutableMapOf<String, String>()
|
val preferences = mutableMapOf<String, String>()
|
||||||
preferences["Autosave"] = sharedPreferences.getBoolean("autosave", true).toString()
|
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
|
||||||
val usingCustomCss = !getStringPref(R.string.pref_custom_css, null).isNullOrBlank()
|
val usingCustomCss = !(preferenceHelper[Preference.CUSTOM_CSS] as String?).isNullOrBlank()
|
||||||
preferences["Custom CSS"] = usingCustomCss.toString()
|
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["Dark Mode"] = darkModeSetting
|
||||||
preferences["Error Reports"] =
|
preferences["Error Reports"] = preferenceHelper[Preference.ERROR_REPORTS_ENABLED].toString()
|
||||||
getBooleanPref(R.string.pref_key_error_reports_enabled, true).toString()
|
preferences["Readability"] = preferenceHelper[Preference.READABILITY_ENABLED].toString()
|
||||||
preferences["Readability"] = getBooleanPref(R.string.readability_enabled, false).toString()
|
|
||||||
Plausible.event("settings", props = preferences, url = "/")
|
Plausible.event("settings", props = preferences, url = "/")
|
||||||
setContent {
|
setContent {
|
||||||
|
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
||||||
|
.collectAsState()
|
||||||
|
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||||
|
.collectAsState()
|
||||||
SimpleMarkdownTheme {
|
SimpleMarkdownTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
NavHost(
|
NavHost(
|
||||||
|
@ -121,10 +116,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
composable(Route.EDITOR.path) {
|
composable(Route.EDITOR.path) {
|
||||||
MainScreen(navController = navController, viewModel = viewModel)
|
MainScreen(
|
||||||
|
navController = navController,
|
||||||
|
viewModel = viewModel,
|
||||||
|
enableAutosave = autosaveEnabled,
|
||||||
|
enableReadability = readabilityEnabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Route.SETTINGS.path) {
|
composable(Route.SETTINGS.path) {
|
||||||
SettingsScreen(navController = navController)
|
SettingsScreen(navController = navController, preferenceHelper)
|
||||||
}
|
}
|
||||||
composable(Route.SUPPORT.path) {
|
composable(Route.SUPPORT.path) {
|
||||||
SupportScreen(navController = navController)
|
SupportScreen(navController = navController)
|
||||||
|
@ -156,15 +156,3 @@ enum class Route(
|
||||||
ABOUT("/about", "About", Icons.Default.Info),
|
ABOUT("/about", "About", Icons.Default.Info),
|
||||||
PRIVACY("/privacy", "Privacy", Icons.Default.PrivacyTip),
|
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.app.Application
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import com.wbrawner.plausible.android.Plausible
|
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.PersistentTree
|
||||||
|
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||||
import com.wbrawner.simplemarkdown.utility.ReviewHelper
|
import com.wbrawner.simplemarkdown.utility.ReviewHelper
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -11,6 +15,7 @@ import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class MarkdownApplication : Application() {
|
class MarkdownApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
|
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
|
||||||
|
@ -40,5 +45,14 @@ class MarkdownApplication : Application() {
|
||||||
"Flavor" to BuildConfig.FLAVOR,
|
"Flavor" to BuildConfig.FLAVOR,
|
||||||
"App Version" to BuildConfig.VERSION_NAME
|
"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
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.SpannableString
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import android.text.style.BackgroundColorSpan
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Menu
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DismissibleDrawerSheet
|
import androidx.compose.material3.DismissibleDrawerSheet
|
||||||
import androidx.compose.material3.DismissibleNavigationDrawer
|
import androidx.compose.material3.DismissibleNavigationDrawer
|
||||||
|
@ -35,9 +36,11 @@ import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
@ -46,7 +49,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
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.platform.LocalContext
|
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.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.preference.PreferenceManager
|
import com.wbrawner.simplemarkdown.MarkdownViewModel
|
||||||
import com.wbrawner.simplemarkdown.R
|
import com.wbrawner.simplemarkdown.R
|
||||||
|
import com.wbrawner.simplemarkdown.Route
|
||||||
import com.wbrawner.simplemarkdown.model.Readability
|
import com.wbrawner.simplemarkdown.model.Readability
|
||||||
import com.wbrawner.simplemarkdown.view.activity.Route
|
import kotlinx.coroutines.delay
|
||||||
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import java.net.URI
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
fun MainScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: MarkdownViewModel,
|
||||||
|
enableAutosave: Boolean,
|
||||||
|
enableReadability: Boolean,
|
||||||
|
) {
|
||||||
var lockSwiping by remember { mutableStateOf(false) }
|
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 ->
|
MarkdownNavigationDrawer(navigate = { navController.navigate(it.path) }) { drawerState ->
|
||||||
Scaffold(topBar = {
|
Scaffold(topBar = {
|
||||||
val fileName by viewModel.fileName.collectAsState()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
MarkdownTopAppBar(title = fileName,
|
MarkdownTopAppBar(title = fileName,
|
||||||
backAsUp = false,
|
backAsUp = false,
|
||||||
|
@ -82,7 +158,7 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
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"
|
shareIntent.type = "text/plain"
|
||||||
startActivity(
|
startActivity(
|
||||||
context, Intent.createChooser(
|
context, Intent.createChooser(
|
||||||
|
@ -101,16 +177,24 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
||||||
onDismissRequest = { menuExpanded = false }) {
|
onDismissRequest = { menuExpanded = false }) {
|
||||||
DropdownMenuItem(text = { Text("New") }, onClick = {
|
DropdownMenuItem(text = { Text("New") }, onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.reset("Untitled.md")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = { Text("Open") }, onClick = {
|
DropdownMenuItem(text = { Text("Open") }, onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
|
openFileLauncher.launch(arrayOf("text/*"))
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = { Text("Save") }, onClick = {
|
DropdownMenuItem(text = { Text("Save") }, onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.save()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = { Text("Save as…") },
|
DropdownMenuItem(text = { Text("Save as…") },
|
||||||
onClick = {
|
onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
|
saveFileLauncher.launch(fileName)
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
@ -127,13 +211,7 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val pagerState = rememberPagerState { 2 }
|
val pagerState = rememberPagerState { 2 }
|
||||||
val context = LocalContext.current
|
|
||||||
val enableReadability = remember {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getBoolean(PREF_KEY_READABILITY, false)
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
@ -151,8 +229,8 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
|
||||||
modifier = Modifier.weight(1f), state = pagerState,
|
modifier = Modifier.weight(1f), state = pagerState,
|
||||||
userScrollEnabled = !lockSwiping
|
userScrollEnabled = !lockSwiping
|
||||||
) { page ->
|
) { page ->
|
||||||
val markdown by viewModel.markdownUpdates.collectAsState()
|
val markdown by viewModel.markdown.collectAsState()
|
||||||
var textFieldValue by remember {
|
var textFieldValue by remember(clearText) {
|
||||||
val annotatedMarkdown = if (enableReadability) {
|
val annotatedMarkdown = if (enableReadability) {
|
||||||
markdown.annotateReadability()
|
markdown.annotateReadability()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
@ -29,27 +25,18 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||||
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
const val PREF_KEY_AUTOSAVE = "autosave"
|
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||||
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"
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(navController: NavController) {
|
fun SettingsScreen(navController: NavController, preferenceHelper: PreferenceHelper) {
|
||||||
Scaffold(topBar = {
|
Scaffold(topBar = {
|
||||||
MarkdownTopAppBar(title = "Settings", navController = navController)
|
MarkdownTopAppBar(title = "Settings", navController = navController)
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
val context = LocalContext.current
|
|
||||||
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
@ -60,37 +47,35 @@ fun SettingsScreen(navController: NavController) {
|
||||||
title = "Autosave",
|
title = "Autosave",
|
||||||
enabledDescription = "Files will be saved automatically",
|
enabledDescription = "Files will be saved automatically",
|
||||||
disabledDescription = "Files will not be saved automatically",
|
disabledDescription = "Files will not be saved automatically",
|
||||||
preferenceKey = PREF_KEY_AUTOSAVE,
|
preference = Preference.AUTOSAVE_ENABLED,
|
||||||
sharedPreferences = sharedPreferences
|
preferenceHelper = preferenceHelper
|
||||||
)
|
)
|
||||||
ListPreference(
|
ListPreference(
|
||||||
title = "Dark mode",
|
title = "Dark mode",
|
||||||
options = listOf("Auto", "Dark", "Light"),
|
options = listOf("auto", "dark", "light"),
|
||||||
defaultValue = "Auto",
|
preference = Preference.DARK_MODE,
|
||||||
preferenceKey = PREF_KEY_DARK_MODE,
|
preferenceHelper = preferenceHelper
|
||||||
sharedPreferences = sharedPreferences
|
|
||||||
)
|
)
|
||||||
BooleanPreference(
|
BooleanPreference(
|
||||||
title = "Send crash reports",
|
title = "Send crash reports",
|
||||||
enabledDescription = "Error reports will be sent",
|
enabledDescription = "Error reports will be sent",
|
||||||
disabledDescription = "Error reports will not be sent",
|
disabledDescription = "Error reports will not be sent",
|
||||||
preferenceKey = PREF_KEY_ERROR_REPORTS,
|
preference = Preference.ERROR_REPORTS_ENABLED,
|
||||||
sharedPreferences = sharedPreferences
|
preferenceHelper = preferenceHelper
|
||||||
)
|
)
|
||||||
BooleanPreference(
|
BooleanPreference(
|
||||||
title = "Send analytics",
|
title = "Send analytics",
|
||||||
enabledDescription = "Analytics events will be sent",
|
enabledDescription = "Analytics events will be sent",
|
||||||
disabledDescription = "Analytics events will not be sent",
|
disabledDescription = "Analytics events will not be sent",
|
||||||
preferenceKey = PREF_KEY_ANALYTICS,
|
preference = Preference.ANALYTICS_ENABLED,
|
||||||
sharedPreferences = sharedPreferences
|
preferenceHelper = preferenceHelper
|
||||||
)
|
)
|
||||||
BooleanPreference(
|
BooleanPreference(
|
||||||
title = "Readability highlighting",
|
title = "Readability highlighting",
|
||||||
enabledDescription = "Readability highlighting is on",
|
enabledDescription = "Readability highlighting is on",
|
||||||
disabledDescription = "Readability highlighting is off",
|
disabledDescription = "Readability highlighting is off",
|
||||||
preferenceKey = PREF_KEY_READABILITY,
|
preference = Preference.READABILITY_ENABLED,
|
||||||
sharedPreferences = sharedPreferences,
|
preferenceHelper = preferenceHelper
|
||||||
defaultValue = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,16 +86,11 @@ fun BooleanPreference(
|
||||||
title: String,
|
title: String,
|
||||||
enabledDescription: String,
|
enabledDescription: String,
|
||||||
disabledDescription: String,
|
disabledDescription: String,
|
||||||
preferenceKey: String,
|
preference: Preference,
|
||||||
sharedPreferences: SharedPreferences,
|
preferenceHelper: PreferenceHelper
|
||||||
defaultValue: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
var enabled by remember {
|
var enabled by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(preferenceHelper[preference] as Boolean)
|
||||||
sharedPreferences.getBoolean(
|
|
||||||
preferenceKey, defaultValue
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BooleanPreference(title = title,
|
BooleanPreference(title = title,
|
||||||
enabledDescription = enabledDescription,
|
enabledDescription = enabledDescription,
|
||||||
|
@ -118,9 +98,7 @@ fun BooleanPreference(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
setEnabled = {
|
setEnabled = {
|
||||||
enabled = it
|
enabled = it
|
||||||
sharedPreferences.edit {
|
preferenceHelper[preference] = it
|
||||||
putBoolean(preferenceKey, it)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,27 +134,19 @@ fun BooleanPreference(
|
||||||
fun ListPreference(
|
fun ListPreference(
|
||||||
title: String,
|
title: String,
|
||||||
options: List<String>,
|
options: List<String>,
|
||||||
defaultValue: String,
|
preference: Preference,
|
||||||
preferenceKey: String,
|
preferenceHelper: PreferenceHelper
|
||||||
sharedPreferences: SharedPreferences
|
|
||||||
) {
|
) {
|
||||||
var selected by remember {
|
var selected by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(preferenceHelper[preference] as String)
|
||||||
sharedPreferences.getString(
|
|
||||||
preferenceKey, defaultValue
|
|
||||||
) ?: defaultValue
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListPreference(title = title, options = options, selected = selected, setSelected = {
|
ListPreference(title = title, options = options, selected = selected, setSelected = {
|
||||||
selected = it
|
selected = it
|
||||||
sharedPreferences.edit {
|
preferenceHelper[preference] = it
|
||||||
putString(preferenceKey, it)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListPreference(
|
fun ListPreference(
|
||||||
title: String, options: List<String>, selected: String, setSelected: (String) -> Unit
|
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 androidx.preference.PreferenceManager
|
||||||
import com.google.android.play.core.review.ReviewManager
|
import com.google.android.play.core.review.ReviewManager
|
||||||
import com.google.android.play.core.review.ReviewManagerFactory
|
import com.google.android.play.core.review.ReviewManagerFactory
|
||||||
import com.wbrawner.simplemarkdown.view.activity.MainActivity
|
import com.wbrawner.simplemarkdown.MainActivity
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val KEY_TIME_IN_APP = "timeInApp"
|
private const val KEY_TIME_IN_APP = "timeInApp"
|
||||||
|
|
Loading…
Reference in a new issue