WIP: Migrate to ViewModel architecture
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
parent
f03a91c1d3
commit
04954b96f7
16 changed files with 427 additions and 489 deletions
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
@ -10,4 +17,46 @@ 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"
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
launch {
|
||||||
.getBoolean(KEY_AUTOSAVE, true)
|
withContext(Dispatchers.IO) {
|
||||||
if (shouldAutoSave && presenter.markdown.isNotEmpty() && isAutoSaveEnabled) {
|
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this@MainActivity)
|
||||||
|
val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
|
||||||
|
if (!shouldAutoSave || !isAutoSaveEnabled) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
launch {
|
val uri = if (viewModel.save(this@MainActivity)) {
|
||||||
presenter.saveMarkdown("autosave.md", File(filesDir, "autosave.md").outputStream())
|
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)
|
launch {
|
||||||
?.use { cursor ->
|
val fileLoaded = viewModel.load(this@MainActivity, data.data)
|
||||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
if (!fileLoaded) {
|
||||||
cursor.moveToFirst()
|
Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT)
|
||||||
cursor.getString(nameIndex)
|
.show()
|
||||||
} ?: "Untitled.md"
|
|
||||||
|
|
||||||
contentResolver.openFileDescriptor(data.data!!, "r")?.let {
|
|
||||||
val fileInput = FileInputStream(it.fileDescriptor)
|
|
||||||
launch {
|
|
||||||
presenter.loadMarkdown(fileName, fileInput)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -1,85 +1,73 @@
|
||||||
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 {
|
||||||
getString(R.string.pref_key_dark_mode),
|
val darkMode = withContext(Dispatchers.IO) {
|
||||||
getString(R.string.pref_value_auto)
|
val darkModeValue = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
|
||||||
)
|
.getString(
|
||||||
|
getString(R.string.pref_key_dark_mode),
|
||||||
var darkMode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
getString(R.string.pref_value_auto)
|
||||||
AppCompatDelegate.MODE_NIGHT_AUTO
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
|
|
||||||
if (intent?.data != null) {
|
|
||||||
launch {
|
|
||||||
presenter.loadFromUri(applicationContext, intent.data!!)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
presenter.fileName = "Untitled.md"
|
|
||||||
val autosave = File(filesDir, "autosave.md")
|
|
||||||
if (autosave.exists()) {
|
|
||||||
try {
|
|
||||||
launch {
|
|
||||||
presenter.loadMarkdown(
|
|
||||||
"Untitled.md",
|
|
||||||
autosave.inputStream(),
|
|
||||||
true
|
|
||||||
)
|
)
|
||||||
autosave.delete()
|
|
||||||
}
|
|
||||||
} catch (ignored: FileNotFoundException) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val startIntent = Intent(this, MainActivity::class.java)
|
return@withContext when {
|
||||||
startActivity(startIntent)
|
darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
finish()
|
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
|
||||||
|
} else {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var uri = intent?.data
|
||||||
|
if (uri == null) {
|
||||||
|
uri = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
|
||||||
|
.getString(
|
||||||
|
getString(R.string.pref_key_autosave_uri),
|
||||||
|
null
|
||||||
|
)?.let {
|
||||||
|
Uri.parse(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.load(this@SplashActivity, uri)
|
||||||
|
}
|
||||||
|
val startIntent = Intent(this@SplashActivity, MainActivity::class.java)
|
||||||
|
startActivity(startIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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,46 +32,48 @@ 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?) {
|
||||||
markdownPreview?.post {
|
super.onActivityCreated(savedInstanceState)
|
||||||
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
|
viewModel = ViewModelProviders.of(
|
||||||
AppCompatDelegate.MODE_NIGHT_YES
|
this,
|
||||||
|| context!!.resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
|
(requireActivity().application as MarkdownApplication).viewModelFactory
|
||||||
val defaultCssId = if (isNightMode) {
|
).get(MarkdownViewModel::class.java)
|
||||||
R.string.pref_custom_css_default_dark
|
viewModel.markdownUpdates.observe(this, Observer<String> {
|
||||||
} else {
|
markdownPreview?.post {
|
||||||
R.string.pref_custom_css_default
|
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
|
||||||
|
AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
|| context!!.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
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
val css = withContext(Dispatchers.IO) {
|
||||||
|
@Suppress("ConstantConditionIf")
|
||||||
|
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
||||||
|
requireActivity().getString(defaultCssId)
|
||||||
|
} else {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||||
|
.getString(
|
||||||
|
getString(R.string.pref_custom_css),
|
||||||
|
getString(defaultCssId)
|
||||||
|
)?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val style = String.format(FORMAT_CSS, css)
|
||||||
|
markdownPreview?.loadDataWithBaseURL(null,
|
||||||
|
style + it.toHtml(),
|
||||||
|
"text/html",
|
||||||
|
"UTF-8", null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@Suppress("ConstantConditionIf")
|
})
|
||||||
val css: String? = if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
|
||||||
context?.getString(defaultCssId)
|
|
||||||
} else {
|
|
||||||
sharedPreferences!!.getString(
|
|
||||||
getString(R.string.pref_custom_css),
|
|
||||||
getString(defaultCssId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val style = String.format(FORMAT_CSS, css)
|
|
||||||
|
|
||||||
markdownPreview?.loadDataWithBaseURL(null,
|
|
||||||
style + html,
|
|
||||||
"text/html",
|
|
||||||
"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 {
|
||||||
|
|
|
@ -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,19 +14,39 @@ 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,
|
||||||
addPreferencesFromResource(R.xml.pref_general)
|
CoroutineScope {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
setListPreferenceSummary(
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
sharedPreferences,
|
super.onActivityCreated(savedInstanceState)
|
||||||
findPreference(getString(R.string.pref_key_dark_mode))
|
launch {
|
||||||
)
|
withContext(Dispatchers.IO) {
|
||||||
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
try {
|
||||||
preferenceScreen.removePreference(findPreference(getString(R.string.pref_custom_css)))
|
// This can be thrown when recreating the activity for theme changes
|
||||||
|
addPreferencesFromResource(R.xml.pref_general)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
sharedPreferences.registerOnSharedPreferenceChangeListener(this@SettingsFragment)
|
||||||
|
(findPreference(getString(R.string.pref_key_dark_mode)) as? ListPreference)?.let {
|
||||||
|
setListPreferenceSummary(sharedPreferences, it)
|
||||||
|
}
|
||||||
|
@Suppress("ConstantConditionIf")
|
||||||
|
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
||||||
|
preferenceScreen.removePreference(findPreference(getString(R.string.pref_custom_css)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,34 +60,35 @@ 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)
|
|
||||||
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)
|
|
||||||
activity?.recreate()
|
|
||||||
}
|
}
|
||||||
|
val darkModeValue = sharedPreferences.getString(preference.key, null)
|
||||||
|
if (darkModeValue != null && darkModeValue.isNotEmpty()) {
|
||||||
|
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)
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue