WIP: Migrate to ViewModel architecture

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-08-18 18:56:21 -07:00 committed by William Brawner
parent f03a91c1d3
commit 04954b96f7
16 changed files with 427 additions and 489 deletions

View file

@ -101,7 +101,10 @@ dependencies {
def coroutines_version = "1.3.0-RC2" def coroutines_version = "1.3.0-RC2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
def lifecycle_version = "2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
implementation 'eu.crydee:syllable-counter:4.0.2' implementation 'eu.crydee:syllable-counter:4.0.2'
} }

View file

@ -1,28 +0,0 @@
package com.wbrawner.simplemarkdown
import android.content.Context
import com.wbrawner.simplemarkdown.view.activity.MainActivity
import com.wbrawner.simplemarkdown.view.activity.SplashActivity
import com.wbrawner.simplemarkdown.view.fragment.EditFragment
import com.wbrawner.simplemarkdown.view.fragment.PreviewFragment
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(application: MarkdownApplication)
fun inject(activity: MainActivity)
fun inject(activity: SplashActivity)
fun inject(fragment: EditFragment)
fun inject(fragment: PreviewFragment)
@Component.Builder
abstract class Builder {
@BindsInstance
internal abstract fun context(context: Context): Builder
internal abstract fun build(): AppComponent
}
}

View file

@ -1,24 +0,0 @@
package com.wbrawner.simplemarkdown
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenterImpl
import com.wbrawner.simplemarkdown.utility.CrashlyticsErrorHandler
import com.wbrawner.simplemarkdown.utility.ErrorHandler
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class AppModule {
@Provides
@Singleton
fun provideMarkdownPresenter(errorHandler: ErrorHandler): MarkdownPresenter {
return MarkdownPresenterImpl(errorHandler)
}
@Provides
@Singleton
internal fun provideErrorHandler(): ErrorHandler {
return CrashlyticsErrorHandler()
}
}

View file

@ -1,16 +1,26 @@
package com.wbrawner.simplemarkdown package com.wbrawner.simplemarkdown
import android.app.Application import android.app.Application
import android.os.StrictMode
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModelFactory
class MarkdownApplication : Application() { class MarkdownApplication : Application() {
val viewModelFactory: MarkdownViewModelFactory by lazy {
lateinit var component: AppComponent MarkdownViewModelFactory()
private set }
override fun onCreate() { override fun onCreate() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
// StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
// .detectAll()
// .penaltyLog()
// .penaltyDeath()
// .build())
}
super.onCreate() super.onCreate()
component = DaggerAppComponent.builder()
.context(this)
.build()
} }
} }

View file

@ -10,25 +10,4 @@ import java.io.Reader
*/ */
class MarkdownFile(var name: String = "Untitled.md", var content: String = "") { class MarkdownFile(var name: String = "Untitled.md", var content: String = "") {
fun load(name: String, inputStream: InputStream): Boolean {
this.name = name
return try {
this.content = inputStream.reader().use(Reader::readText)
true
} catch (e: Throwable) {
false
}
}
fun save(name: String, outputStream: OutputStream): Boolean {
this.name = name
return try {
outputStream.writer().use {
it.write(this.content)
}
true
} catch (e: Throwable) {
false
}
}
} }

View file

@ -1,39 +0,0 @@
package com.wbrawner.simplemarkdown.presentation
import android.content.Context
import android.net.Uri
import java.io.InputStream
import java.io.OutputStream
interface MarkdownPresenter {
var fileName: String
var markdown: String
var editView: MarkdownEditView?
var previewView: MarkdownPreviewView?
suspend fun loadFromUri(context: Context, fileUri: Uri): String?
suspend fun loadMarkdown(
fileName: String,
`in`: InputStream,
replaceCurrentFile: Boolean = true
): String?
fun newFile(newName: String)
suspend fun saveMarkdown(name: String, outputStream: OutputStream): Boolean
fun onMarkdownEdited(markdown: String? = null)
fun generateHTML(markdown: String = ""): String
}
interface MarkdownEditView {
var markdown: String
fun setTitle(title: String)
fun onFileSaved(success: Boolean)
fun onFileLoaded(success: Boolean)
}
interface MarkdownPreviewView {
fun updatePreview(html: String)
}

View file

@ -1,127 +0,0 @@
package com.wbrawner.simplemarkdown.presentation
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import com.commonsware.cwac.anddown.AndDown
import com.wbrawner.simplemarkdown.model.MarkdownFile
import com.wbrawner.simplemarkdown.utility.ErrorHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MarkdownPresenterImpl @Inject constructor(private val errorHandler: ErrorHandler) : MarkdownPresenter {
@Volatile
private var file: MarkdownFile = MarkdownFile()
@Volatile
override var editView: MarkdownEditView? = null
set(value) {
field = value
onMarkdownEdited(null)
}
@Volatile
override var previewView: MarkdownPreviewView? = null
override var fileName: String
get() = file.name
set(name) {
file.name = name
}
override var markdown: String
get() = file.content
set(markdown) {
file.content = markdown
}
override suspend fun loadMarkdown(
fileName: String,
`in`: InputStream,
replaceCurrentFile: Boolean
): String? {
val tmpFile = MarkdownFile()
withContext(Dispatchers.IO) {
if (!tmpFile.load(fileName, `in`)) {
throw RuntimeException("Failed to load markdown")
}
}
if (replaceCurrentFile) {
this.file = tmpFile
editView?.let {
it.onFileLoaded(true)
it.setTitle(fileName)
it.markdown = file.content
onMarkdownEdited(file.content)
}
}
return generateHTML(tmpFile.content)
}
override fun newFile(newName: String) {
editView?.let {
file.content = it.markdown
it.setTitle(newName)
it.markdown = ""
}
file = MarkdownFile(newName, "")
}
override suspend fun saveMarkdown(name: String, outputStream: OutputStream): Boolean {
val result = withContext(Dispatchers.IO) {
file.save(name, outputStream)
}
editView?.let {
it.setTitle(file.name)
it.onFileSaved(result)
}
return result
}
override fun onMarkdownEdited(markdown: String?) {
this.markdown = markdown ?: file.content
previewView?.updatePreview(generateHTML(this.markdown))
}
override fun generateHTML(markdown: String): String {
return AndDown().markdownToHtml(markdown, HOEDOWN_FLAGS, 0)
}
override suspend fun loadFromUri(context: Context, fileUri: Uri): String? {
return try {
var fileName: String? = null
if ("content" == fileUri.scheme) {
context.contentResolver
.query(
fileUri,
null,
null,
null,
null
)
?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
fileName = it.getString(nameIndex)
}
} else if ("file" == fileUri.scheme) {
fileName = fileUri.lastPathSegment
}
val inputStream = context.contentResolver.openInputStream(fileUri) ?: return null
loadMarkdown(fileName ?: "Untitled.md", inputStream, true)
} catch (e: Exception) {
errorHandler.reportException(e)
editView?.onFileLoaded(false)
null
}
}
companion object {
const val HOEDOWN_FLAGS = AndDown.HOEDOWN_EXT_STRIKETHROUGH or AndDown.HOEDOWN_EXT_TABLES or
AndDown.HOEDOWN_EXT_UNDERLINE or AndDown.HOEDOWN_EXT_SUPERSCRIPT or
AndDown.HOEDOWN_EXT_FENCED_CODE
}
}

View file

@ -1,8 +1,15 @@
package com.wbrawner.simplemarkdown.utility package com.wbrawner.simplemarkdown.utility
import android.content.Context import android.content.Context
import android.content.res.AssetManager
import android.net.Uri
import android.provider.OpenableColumns
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import com.commonsware.cwac.anddown.AndDown
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.Reader
fun View.showKeyboard() = fun View.showKeyboard() =
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
@ -11,3 +18,45 @@ fun View.showKeyboard() =
fun View.hideKeyboard() = fun View.hideKeyboard() =
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(windowToken, 0) .hideSoftInputFromWindow(windowToken, 0)
suspend fun AssetManager.readAssetToString(asset: String): String? {
return withContext(Dispatchers.IO) {
open(asset).reader().use(Reader::readText)
}
}
const val HOEDOWN_FLAGS = AndDown.HOEDOWN_EXT_STRIKETHROUGH or AndDown.HOEDOWN_EXT_TABLES or
AndDown.HOEDOWN_EXT_UNDERLINE or AndDown.HOEDOWN_EXT_SUPERSCRIPT or
AndDown.HOEDOWN_EXT_FENCED_CODE
suspend fun String.toHtml(): String {
return withContext(Dispatchers.IO) {
AndDown().markdownToHtml(this@toHtml, HOEDOWN_FLAGS, 0)
}
}
suspend fun Uri.getName(context: Context): String {
var fileName: String? = null
try {
if ("content" == scheme) {
withContext(Dispatchers.IO) {
context.contentResolver.query(
this@getName,
null,
null,
null,
null
)?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
fileName = it.getString(nameIndex)
}
}
} else if ("file" == scheme) {
fileName = lastPathSegment
}
} catch (ignored: Exception) {
ignored.printStackTrace()
}
return fileName ?: "Untitled.md"
}

View file

@ -5,10 +5,10 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.provider.OpenableColumns
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -18,11 +18,16 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.utility.ErrorHandler import com.wbrawner.simplemarkdown.utility.ErrorHandler
import com.wbrawner.simplemarkdown.utility.getName
import com.wbrawner.simplemarkdown.utility.readAssetToString
import com.wbrawner.simplemarkdown.utility.toHtml
import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
@ -32,12 +37,11 @@ import kotlin.coroutines.CoroutineContext
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope { class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope {
@Inject
lateinit var presenter: MarkdownPresenter
@Inject @Inject
lateinit var errorHandler: ErrorHandler lateinit var errorHandler: ErrorHandler
private var shouldAutoSave = true private var shouldAutoSave = true
override val coroutineContext: CoroutineContext = Dispatchers.Main override val coroutineContext: CoroutineContext = Dispatchers.Main
private lateinit var viewModel: MarkdownViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -50,7 +54,10 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
) )
} }
(application as MarkdownApplication).component.inject(this) viewModel = ViewModelProviders.of(
this,
(application as MarkdownApplication).viewModelFactory
).get(MarkdownViewModel::class.java)
val adapter = EditPagerAdapter(supportFragmentManager, this@MainActivity) val adapter = EditPagerAdapter(supportFragmentManager, this@MainActivity)
pager.adapter = adapter pager.adapter = adapter
pager.addOnPageChangeListener(adapter) pager.addOnPageChangeListener(adapter)
@ -60,16 +67,38 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
tabLayout!!.visibility = View.GONE tabLayout!!.visibility = View.GONE
} }
@Suppress("CAST_NEVER_SUCCEEDS")
viewModel.fileName.observe(this, Observer<String> {
title = it
})
} }
override fun onUserLeaveHint() { override fun onUserLeaveHint() {
super.onUserLeaveHint() super.onUserLeaveHint()
val isAutoSaveEnabled = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(KEY_AUTOSAVE, true)
if (shouldAutoSave && presenter.markdown.isNotEmpty() && isAutoSaveEnabled) {
launch { launch {
presenter.saveMarkdown("autosave.md", File(filesDir, "autosave.md").outputStream()) withContext(Dispatchers.IO) {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this@MainActivity)
val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
if (!shouldAutoSave || !isAutoSaveEnabled) {
return@withContext
}
val uri = if (viewModel.save(this@MainActivity)) {
viewModel.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(filesDir, viewModel.fileName.value))
if (viewModel.save(this@MainActivity, fileUri)) {
fileUri
} else {
null
}
}?: return@withContext
sharedPrefs.edit()
.putString(getString(R.string.pref_key_autosave_uri), uri.toString())
.apply()
} }
} }
} }
@ -89,10 +118,22 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_save -> requestFileOp(REQUEST_SAVE_FILE) R.id.action_save -> {
launch {
if (!viewModel.save(this@MainActivity)) {
requestFileOp(REQUEST_SAVE_FILE)
} else {
Toast.makeText(
this@MainActivity,
getString(R.string.file_saved, viewModel.fileName.value),
Toast.LENGTH_SHORT
).show()
}
}
}
R.id.action_share -> { R.id.action_share -> {
val shareIntent = Intent(Intent.ACTION_SEND) val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, presenter.markdown) shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
shareIntent.type = "text/plain" shareIntent.type = "text/plain"
startActivity(Intent.createChooser( startActivity(Intent.createChooser(
shareIntent, shareIntent,
@ -137,9 +178,9 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
infoIntent.putExtra("title", title) infoIntent.putExtra("title", title)
launch { launch {
try { try {
val inputStream = assets?.open(fileName) val html = assets?.readAssetToString(fileName)
?.toHtml()
?: throw RuntimeException("Unable to open stream to $fileName") ?: throw RuntimeException("Unable to open stream to $fileName")
val html = presenter.loadMarkdown(fileName, inputStream, false)
infoIntent.putExtra("html", html) infoIntent.putExtra("html", html)
startActivity(infoIntent) startActivity(infoIntent)
} catch (e: Exception) { } catch (e: Exception) {
@ -183,17 +224,11 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
return return
} }
val fileName = contentResolver.query(data.data!!, null, null, null, null)
?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
} ?: "Untitled.md"
contentResolver.openFileDescriptor(data.data!!, "r")?.let {
val fileInput = FileInputStream(it.fileDescriptor)
launch { launch {
presenter.loadMarkdown(fileName, fileInput) val fileLoaded = viewModel.load(this@MainActivity, data.data)
if (!fileLoaded) {
Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT)
.show()
} }
} }
} }
@ -202,20 +237,8 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
return return
} }
val fileName = contentResolver.query(data.data!!, null, null, null, null)
?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
} ?: "Untitled.md"
launch { launch {
val outputStream = contentResolver.openOutputStream(data.data!!) viewModel.save(this@MainActivity, data.data)
?: throw RuntimeException("Unable to open output stream to save file")
presenter.saveMarkdown(
fileName,
outputStream
)
} }
} }
REQUEST_DARK_MODE -> recreate() REQUEST_DARK_MODE -> recreate()
@ -224,11 +247,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
} }
private fun promptSaveOrDiscardChanges() { private fun promptSaveOrDiscardChanges() {
if (viewModel.originalMarkdown.value == viewModel.markdownUpdates.value) {
viewModel.reset("Untitled.md")
return
}
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.save_changes) .setTitle(R.string.save_changes)
.setMessage(R.string.prompt_save_changes) .setMessage(R.string.prompt_save_changes)
.setNegativeButton(R.string.action_discard) { _, _ -> .setNegativeButton(R.string.action_discard) { _, _ ->
presenter.newFile("Untitled.md") viewModel.reset("Untitled.md")
} }
.setPositiveButton(R.string.action_save) { _, _ -> .setPositiveButton(R.string.action_save) { _, _ ->
requestFileOp(REQUEST_SAVE_FILE) requestFileOp(REQUEST_SAVE_FILE)
@ -252,7 +279,7 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
REQUEST_SAVE_FILE -> { REQUEST_SAVE_FILE -> {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply { Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/markdown" type = "text/markdown"
putExtra(Intent.EXTRA_TITLE, presenter.fileName) putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value)
} }
} }
REQUEST_OPEN_FILE -> { REQUEST_OPEN_FILE -> {
@ -274,14 +301,11 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
) )
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
title = presenter.fileName
shouldAutoSave = true shouldAutoSave = true
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
coroutineContext[Job]?.let { coroutineContext[Job]?.let {

View file

@ -1,86 +1,74 @@
package com.wbrawner.simplemarkdown.view.activity package com.wbrawner.simplemarkdown.view.activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ViewModelProviders
import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import com.wbrawner.simplemarkdown.utility.ErrorHandler
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File
import java.io.FileNotFoundException
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class SplashActivity : AppCompatActivity(), CoroutineScope { class SplashActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main override val coroutineContext: CoroutineContext = Dispatchers.Main
@Inject lateinit var viewModel: MarkdownViewModel
lateinit var presenter: MarkdownPresenter
@Inject
lateinit var errorHandler: ErrorHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) viewModel = ViewModelProviders.of(
(application as MarkdownApplication).component.inject(this) this,
if (sharedPreferences.getBoolean(getString(R.string.error_reports_enabled), true)) { (application as MarkdownApplication).viewModelFactory
errorHandler.init(this) ).get(MarkdownViewModel::class.java)
}
val darkModeValue = sharedPreferences.getString( launch {
val darkMode = withContext(Dispatchers.IO) {
val darkModeValue = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
.getString(
getString(R.string.pref_key_dark_mode), getString(R.string.pref_key_dark_mode),
getString(R.string.pref_value_auto) getString(R.string.pref_value_auto)
) )
var darkMode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { return@withContext when {
darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_NO
darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO AppCompatDelegate.MODE_NIGHT_AUTO
} else { } else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
}
}
if (darkModeValue != null && !darkModeValue.isEmpty()) {
if (darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_NO
} else if (darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_YES
}
} }
AppCompatDelegate.setDefaultNightMode(darkMode) AppCompatDelegate.setDefaultNightMode(darkMode)
withContext(Dispatchers.IO) {
if (intent?.data != null) { var uri = intent?.data
launch { if (uri == null) {
presenter.loadFromUri(applicationContext, intent.data!!) uri = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
} .getString(
} else { getString(R.string.pref_key_autosave_uri),
presenter.fileName = "Untitled.md" null
val autosave = File(filesDir, "autosave.md") )?.let {
if (autosave.exists()) { Uri.parse(it)
try {
launch {
presenter.loadMarkdown(
"Untitled.md",
autosave.inputStream(),
true
)
autosave.delete()
}
} catch (ignored: FileNotFoundException) {
return
}
} }
} }
val startIntent = Intent(this, MainActivity::class.java) viewModel.load(this@SplashActivity, uri)
}
val startIntent = Intent(this@SplashActivity, MainActivity::class.java)
startActivity(startIntent) startActivity(startIntent)
finish() finish()
} }
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

View file

@ -16,31 +16,24 @@ import android.view.ViewGroup
import android.widget.EditText import android.widget.EditText
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.model.Readability import com.wbrawner.simplemarkdown.model.Readability
import com.wbrawner.simplemarkdown.presentation.MarkdownEditView
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.utility.hideKeyboard import com.wbrawner.simplemarkdown.utility.hideKeyboard
import com.wbrawner.simplemarkdown.utility.showKeyboard import com.wbrawner.simplemarkdown.utility.showKeyboard
import com.wbrawner.simplemarkdown.view.ViewPagerPage import com.wbrawner.simplemarkdown.view.ViewPagerPage
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.abs import kotlin.math.abs
class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope { class EditFragment : Fragment(), ViewPagerPage, CoroutineScope {
@Inject
lateinit var presenter: MarkdownPresenter
private var markdownEditor: EditText? = null private var markdownEditor: EditText? = null
private var markdownEditorScroller: ScrollView? = null private var markdownEditorScroller: ScrollView? = null
override var markdown: String private lateinit var viewModel: MarkdownViewModel
get() = markdownEditor?.text?.toString() ?: ""
set(value) {
markdownEditor?.setText(value)
}
override val coroutineContext: CoroutineContext = Dispatchers.Main override val coroutineContext: CoroutineContext = Dispatchers.Main
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -67,7 +60,7 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
delay(50) delay(50)
if (searchText != searchFor) if (searchText != searchFor)
return@launch return@launch
presenter.onMarkdownEdited(searchText) viewModel.updateMarkdown(searchText)
} }
} }
@ -78,8 +71,15 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
} }
}) })
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) var enableReadability = false
val enableReadability = sharedPrefs.getBoolean(getString(R.string.readability_enabled), false) launch {
enableReadability = withContext(Dispatchers.IO) {
context?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.getBoolean(getString(R.string.readability_enabled), false)
}?: false
}
}
if (enableReadability) { if (enableReadability) {
markdownEditor?.addTextChangedListener(object : TextWatcher { markdownEditor?.addTextChangedListener(object : TextWatcher {
private var previousValue = "" private var previousValue = ""
@ -97,10 +97,10 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
if (searchText != searchFor) if (searchText != searchFor)
return@launch return@launch
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
if (markdown.isEmpty()) return@launch if (searchFor.isEmpty()) return@launch
if (previousValue == markdown) return@launch if (previousValue == searchFor) return@launch
val readability = Readability(markdown) val readability = Readability(searchFor)
val span = SpannableString(markdown) val span = SpannableString(searchFor)
for (sentence in readability.sentences()) { for (sentence in readability.sentences()) {
var color = Color.TRANSPARENT var color = Color.TRANSPARENT
if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42) if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42)
@ -108,7 +108,7 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0) span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0)
} }
markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE) markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE)
previousValue = markdown previousValue = searchFor
val timeTakenMs = System.currentTimeMillis() - start val timeTakenMs = System.currentTimeMillis() - start
Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms") Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms")
} }
@ -148,21 +148,13 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
@Suppress("CAST_NEVER_SUCCEEDS") viewModel = ViewModelProviders.of(
(activity?.application as? MarkdownApplication)?.component?.inject(this) this,
presenter.editView = this@EditFragment (requireActivity().application as MarkdownApplication).viewModelFactory
} ).get(MarkdownViewModel::class.java)
viewModel.originalMarkdown.observe(this, Observer<String> {
override fun onResume() { markdownEditor?.setText(it)
super.onResume() })
presenter.editView = this
markdown = presenter.markdown
}
override fun onPause() {
super.onPause()
presenter.editView = null
markdownEditor?.hideKeyboard()
} }
override fun onDestroy() { override fun onDestroy() {
@ -179,26 +171,4 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope
override fun onDeselected() { override fun onDeselected() {
markdownEditor?.hideKeyboard() markdownEditor?.hideKeyboard()
} }
}
override fun setTitle(title: String) {
val activity = activity
if (activity != null) {
activity.title = title
}
}
override fun onFileSaved(success: Boolean) {
val message: String = if (success) {
getString(R.string.file_saved, presenter.fileName)
} else {
getString(R.string.file_save_error)
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
override fun onFileLoaded(success: Boolean) {
// TODO: Investigate why this fires off so often
// int message = success ? R.string.file_loaded : R.string.file_load_error;
// Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show();
}
}// Required empty public constructor

View file

@ -1,6 +1,5 @@
package com.wbrawner.simplemarkdown.view.fragment package com.wbrawner.simplemarkdown.view.fragment
import android.content.SharedPreferences
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle import android.os.Bundle
@ -11,19 +10,20 @@ import android.view.ViewGroup
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.wbrawner.simplemarkdown.BuildConfig import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter import com.wbrawner.simplemarkdown.utility.toHtml
import com.wbrawner.simplemarkdown.presentation.MarkdownPreviewView import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import javax.inject.Inject import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class PreviewFragment : Fragment(), CoroutineScope {
class PreviewFragment : Fragment(), MarkdownPreviewView { override val coroutineContext: CoroutineContext = Dispatchers.Main
@Inject lateinit var viewModel: MarkdownViewModel
lateinit var presenter: MarkdownPresenter
private var markdownPreview: WebView? = null private var markdownPreview: WebView? = null
private var sharedPreferences: SharedPreferences? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -32,13 +32,17 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
): View? = inflater.inflate(R.layout.fragment_preview, container, false) ): View? = inflater.inflate(R.layout.fragment_preview, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(view.context)
markdownPreview = view.findViewById(R.id.markdown_view) markdownPreview = view.findViewById(R.id.markdown_view)
(activity?.application as? MarkdownApplication)?.component?.inject(this)
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
} }
override fun updatePreview(html: String) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(
this,
(requireActivity().application as MarkdownApplication).viewModelFactory
).get(MarkdownViewModel::class.java)
viewModel.markdownUpdates.observe(this, Observer<String> {
markdownPreview?.post { markdownPreview?.post {
val isNightMode = AppCompatDelegate.getDefaultNightMode() == val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
AppCompatDelegate.MODE_NIGHT_YES AppCompatDelegate.MODE_NIGHT_YES
@ -48,30 +52,28 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
} else { } else {
R.string.pref_custom_css_default R.string.pref_custom_css_default
} }
launch {
val css = withContext(Dispatchers.IO) {
@Suppress("ConstantConditionIf") @Suppress("ConstantConditionIf")
val css: String? = if (!BuildConfig.ENABLE_CUSTOM_CSS) { if (!BuildConfig.ENABLE_CUSTOM_CSS) {
context?.getString(defaultCssId) requireActivity().getString(defaultCssId)
} else { } else {
sharedPreferences!!.getString( PreferenceManager.getDefaultSharedPreferences(requireActivity())
.getString(
getString(R.string.pref_custom_css), getString(R.string.pref_custom_css),
getString(defaultCssId) getString(defaultCssId)
) )?: ""
}
} }
val style = String.format(FORMAT_CSS, css) val style = String.format(FORMAT_CSS, css)
markdownPreview?.loadDataWithBaseURL(null, markdownPreview?.loadDataWithBaseURL(null,
style + html, style + it.toHtml(),
"text/html", "text/html",
"UTF-8", null "UTF-8", null
) )
} }
} }
})
override fun onResume() {
super.onResume()
presenter.previewView = this
presenter.onMarkdownEdited()
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -84,8 +86,10 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
} }
override fun onDestroy() { override fun onDestroy() {
coroutineContext[Job]?.let {
cancel()
}
super.onDestroy() super.onDestroy()
presenter.previewView = null
} }
companion object { companion object {

View file

@ -3,6 +3,7 @@ package com.wbrawner.simplemarkdown.view.fragment
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode
import android.preference.ListPreference import android.preference.ListPreference
import android.preference.Preference import android.preference.Preference
import android.preference.PreferenceFragment import android.preference.PreferenceFragment
@ -13,21 +14,41 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.wbrawner.simplemarkdown.BuildConfig import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Exception
import kotlin.coroutines.CoroutineContext
class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsFragment
override fun onCreate(savedInstanceState: Bundle?) { : PreferenceFragment(),
super.onCreate(savedInstanceState) SharedPreferences.OnSharedPreferenceChangeListener,
CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
launch {
withContext(Dispatchers.IO) {
try {
// This can be thrown when recreating the activity for theme changes
addPreferencesFromResource(R.xml.pref_general) addPreferencesFromResource(R.xml.pref_general)
} catch (ignored: Exception) {
return@withContext
}
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
sharedPreferences.registerOnSharedPreferenceChangeListener(this) sharedPreferences.registerOnSharedPreferenceChangeListener(this@SettingsFragment)
setListPreferenceSummary( (findPreference(getString(R.string.pref_key_dark_mode)) as? ListPreference)?.let {
sharedPreferences, setListPreferenceSummary(sharedPreferences, it)
findPreference(getString(R.string.pref_key_dark_mode)) }
) @Suppress("ConstantConditionIf")
if (!BuildConfig.ENABLE_CUSTOM_CSS) { if (!BuildConfig.ENABLE_CUSTOM_CSS) {
preferenceScreen.removePreference(findPreference(getString(R.string.pref_custom_css))) preferenceScreen.removePreference(findPreference(getString(R.string.pref_custom_css)))
} }
} }
}
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -39,18 +60,18 @@ class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPrefere
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (!isAdded) return if (!isAdded) return
val preference = findPreference(key) val preference = findPreference(key) as? ListPreference ?: return
if (preference is ListPreference) {
setListPreferenceSummary(sharedPreferences, preference) setListPreferenceSummary(sharedPreferences, preference)
if (preference.key != getString(R.string.pref_key_dark_mode)) {
return
} }
if (preference.key == getString(R.string.pref_key_dark_mode)) {
var darkMode: Int = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { var darkMode: Int = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO AppCompatDelegate.MODE_NIGHT_AUTO
} else { } else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
val darkModeValue = sharedPreferences.getString(preference.key, null) val darkModeValue = sharedPreferences.getString(preference.key, null)
if (darkModeValue != null && !darkModeValue.isEmpty()) { if (darkModeValue != null && darkModeValue.isNotEmpty()) {
if (darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true)) { if (darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_NO darkMode = AppCompatDelegate.MODE_NIGHT_NO
} else if (darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true)) { } else if (darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true)) {
@ -60,13 +81,14 @@ class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPrefere
AppCompatDelegate.setDefaultNightMode(darkMode) AppCompatDelegate.setDefaultNightMode(darkMode)
activity?.recreate() activity?.recreate()
} }
}
private fun setListPreferenceSummary(sharedPreferences: SharedPreferences, preference: Preference) { private fun setListPreferenceSummary(sharedPreferences: SharedPreferences, preference: ListPreference) {
val listPreference = preference as ListPreference val storedValue = sharedPreferences.getString(
val storedValue = sharedPreferences.getString(preference.getKey(), null) ?: return preference.key,
val index = listPreference.findIndexOfValue(storedValue) null
) ?: return
val index = preference.findIndexOfValue(storedValue)
if (index < 0) return if (index < 0) return
preference.setSummary(listPreference.entries[index].toString()) preference.summary = preference.entries[index].toString()
} }
} }

View file

@ -0,0 +1,106 @@
package com.wbrawner.simplemarkdown.viewmodel
import android.content.Context
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.wbrawner.simplemarkdown.utility.getName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.io.Reader
import kotlin.coroutines.CoroutineContext
class MarkdownViewModel : ViewModel() {
private val coroutineContext: CoroutineContext = Dispatchers.IO
val fileName = MutableLiveData<String>().apply {
postValue("Untitled.md")
}
val markdownUpdates = MutableLiveData<String>()
val originalMarkdown = MutableLiveData<String>()
val uri = MutableLiveData<Uri>()
fun updateMarkdown(markdown: String?) {
this.markdownUpdates.postValue(markdown ?: "")
}
suspend fun load(context: Context, uri: Uri?): Boolean {
if (uri == null) return false
return withContext(Dispatchers.IO) {
context.contentResolver.openFileDescriptor(uri, "r")?.use {
val fileInput = FileInputStream(it.fileDescriptor)
val fileName = uri.getName(context)
return@withContext if (load(fileInput)) {
this@MarkdownViewModel.fileName.postValue(fileName)
this@MarkdownViewModel.uri.postValue(uri)
true
} else {
false
}
} ?: false
}
}
suspend fun load(inputStream: InputStream): Boolean {
return try {
withContext(coroutineContext) {
val content = inputStream.reader().use(Reader::readText)
originalMarkdown.postValue(content)
markdownUpdates.postValue(content)
}
true
} catch (e: Throwable) {
e.printStackTrace()
false
}
}
suspend fun save(context: Context, givenUri: Uri? = this.uri.value): Boolean {
val uri = givenUri ?: this.uri.value ?: return false
return withContext(Dispatchers.IO) {
val fileName = uri.getName(context)
val outputStream = context.contentResolver.openOutputStream(uri)
?: return@withContext false
if (save(outputStream)) {
this@MarkdownViewModel.fileName.postValue(fileName)
this@MarkdownViewModel.uri.postValue(uri)
true
} else {
false
}
}
}
suspend fun save(outputStream: OutputStream): Boolean {
return try {
withContext(coroutineContext) {
outputStream.writer().use {
it.write(markdownUpdates.value)
}
}
true
} catch (e: Throwable) {
false
}
}
fun reset(untitledFileName: String) {
fileName.postValue(untitledFileName)
originalMarkdown.postValue("")
markdownUpdates.postValue("")
}
}
class MarkdownViewModelFactory : ViewModelProvider.Factory {
private val markdownViewModel: MarkdownViewModel by lazy {
MarkdownViewModel()
}
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return markdownViewModel as T
}
}

View file

@ -60,6 +60,7 @@
<string name="pref_key_dark_mode_light" translatable="false">light</string> <string name="pref_key_dark_mode_light" translatable="false">light</string>
<string name="pref_key_dark_mode_dark" translatable="false">dark</string> <string name="pref_key_dark_mode_dark" translatable="false">dark</string>
<string name="pref_key_dark_mode_auto" translatable="false">auto</string> <string name="pref_key_dark_mode_auto" translatable="false">auto</string>
<string name="pref_key_autosave_uri" translatable="false">autosave.uri</string>
<string name="save_changes">Save Changes</string> <string name="save_changes">Save Changes</string>
<string name="prompt_save_changes">Would you like to save your changes?</string> <string name="prompt_save_changes">Would you like to save your changes?</string>
<string name="action_discard">Discard</string> <string name="action_discard">Discard</string>

View file

@ -14,7 +14,7 @@
<ListPreference <ListPreference
android:entries="@array/pref_entries_dark_mode" android:entries="@array/pref_entries_dark_mode"
android:entryValues="@array/pref_values_dark_mode" android:entryValues="@array/pref_values_dark_mode"
android:defaultValue="@string/pref_value_auto" android:defaultValue="@string/pref_key_dark_mode_auto"
android:key="@string/pref_key_dark_mode" android:key="@string/pref_key_dark_mode"
android:title="@string/title_dark_mode" /> android:title="@string/title_dark_mode" />
<SwitchPreference <SwitchPreference