Clean up much of the Kotlin usage and remove RxJava

This commit is contained in:
Billy Brawner 2019-08-17 13:23:26 -05:00 committed by William Brawner
parent e00d43f93c
commit 829dc11c12
11 changed files with 277 additions and 381 deletions

View file

@ -39,7 +39,6 @@ android {
applicationId "com.wbrawner.simplemarkdown" applicationId "com.wbrawner.simplemarkdown"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 28
multiDexEnabled true
versionCode 20 versionCode 20
versionName "0.7.0" versionName "0.7.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -75,7 +74,6 @@ android {
} }
dependencies { dependencies {
def lifecycle_version = "2.0.0"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.2' testImplementation 'org.robolectric:robolectric:4.2'
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
@ -95,18 +93,14 @@ dependencies {
annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1' annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1'
kapt 'com.google.dagger:dagger-android-processor:2.22.1' kapt 'com.google.dagger:dagger-android-processor:2.22.1'
kapt 'com.google.dagger:dagger-compiler:2.22.1' kapt 'com.google.dagger:dagger-compiler:2.22.1'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.7'
implementation 'com.google.firebase:firebase-core:17.0.1' implementation 'com.google.firebase:firebase-core:17.0.1'
implementation 'com.android.billingclient:billing:1.2' implementation 'com.android.billingclient:billing:1.2'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.core:core-ktx:1.0.2" implementation "androidx.core:core-ktx:1.0.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
def coroutines_version = "1.3.0-RC2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'eu.crydee:syllable-counter:4.0.2' implementation 'eu.crydee:syllable-counter:4.0.2'
} }

View file

@ -3,37 +3,37 @@ package com.wbrawner.simplemarkdown.presentation
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.wbrawner.simplemarkdown.view.MarkdownEditView
import com.wbrawner.simplemarkdown.view.MarkdownPreviewView
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
interface MarkdownPresenter { interface MarkdownPresenter {
var fileName: String var fileName: String
var markdown: String var markdown: String
fun loadFromUri(context: Context, fileUri: Uri) var editView: MarkdownEditView?
var previewView: MarkdownPreviewView?
fun loadMarkdown( suspend fun loadFromUri(context: Context, fileUri: Uri): String?
suspend fun loadMarkdown(
fileName: String, fileName: String,
`in`: InputStream, `in`: InputStream,
listener: FileLoadedListener? = null,
replaceCurrentFile: Boolean = true replaceCurrentFile: Boolean = true
) ): String?
fun newFile(newName: String) fun newFile(newName: String)
fun setEditView(editView: MarkdownEditView) suspend fun saveMarkdown(name: String, outputStream: OutputStream): Boolean
fun setPreviewView(previewView: MarkdownPreviewView)
fun saveMarkdown(listener: MarkdownSavedListener, name: String, outputStream: OutputStream)
fun onMarkdownEdited(markdown: String? = null) fun onMarkdownEdited(markdown: String? = null)
fun generateHTML(markdown: String? = null): String fun generateHTML(markdown: String = ""): String
interface FileLoadedListener {
fun onSuccess(markdown: String)
fun onError()
}
interface MarkdownSavedListener {
fun saveComplete(success: Boolean)
}
} }
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

@ -2,163 +2,126 @@ package com.wbrawner.simplemarkdown.presentation
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler
import android.provider.OpenableColumns import android.provider.OpenableColumns
import com.commonsware.cwac.anddown.AndDown import com.commonsware.cwac.anddown.AndDown
import com.wbrawner.simplemarkdown.model.MarkdownFile import com.wbrawner.simplemarkdown.model.MarkdownFile
import com.wbrawner.simplemarkdown.utility.ErrorHandler import com.wbrawner.simplemarkdown.utility.ErrorHandler
import com.wbrawner.simplemarkdown.view.MarkdownEditView import kotlinx.coroutines.Dispatchers
import com.wbrawner.simplemarkdown.view.MarkdownPreviewView import kotlinx.coroutines.withContext
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class MarkdownPresenterImpl @Inject class MarkdownPresenterImpl @Inject constructor(private val errorHandler: ErrorHandler) : MarkdownPresenter {
constructor(private val errorHandler: ErrorHandler) : MarkdownPresenter {
private val fileLock = Any()
private var file: MarkdownFile? = null
@Volatile @Volatile
private var editView: MarkdownEditView? = null private var file: MarkdownFile = MarkdownFile()
@Volatile @Volatile
private var previewView: MarkdownPreviewView? = null override var editView: MarkdownEditView? = null
private val fileHandler = Handler() set(value) {
field = value
onMarkdownEdited(null)
}
@Volatile
override var previewView: MarkdownPreviewView? = null
override var fileName: String override var fileName: String
get() = synchronized(fileLock) { get() = file.name
return file!!.name set(name) {
} file.name = name
set(name) = synchronized(fileLock) {
file!!.name = name
} }
override var markdown: String override var markdown: String
get() = synchronized(fileLock) { get() = file.content
return file!!.content set(markdown) {
} file.content = markdown
set(markdown) = synchronized(fileLock) {
file!!.content = markdown
} }
init { override suspend fun loadMarkdown(
synchronized(fileLock) {
this.file = MarkdownFile()
}
}
override fun loadMarkdown(
fileName: String, fileName: String,
`in`: InputStream, `in`: InputStream,
listener: MarkdownPresenter.FileLoadedListener?,
replaceCurrentFile: Boolean replaceCurrentFile: Boolean
) { ): String? {
fileHandler.post { val tmpFile = MarkdownFile()
val tmpFile = MarkdownFile() withContext(Dispatchers.IO) {
if (tmpFile.load(fileName, `in`)) { if (!tmpFile.load(fileName, `in`)) {
if (listener != null) { throw RuntimeException("Failed to load markdown")
val html = generateHTML(tmpFile.content)
listener.onSuccess(html)
}
if (replaceCurrentFile) {
synchronized(fileLock) {
this.file = tmpFile
val currentEditView = editView
if (currentEditView != null) {
currentEditView.onFileLoaded(true)
currentEditView.setTitle(fileName)
currentEditView.markdown = this.file!!.content
onMarkdownEdited(null)
}
}
}
} else {
listener?.onError()
} }
} }
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) { override fun newFile(newName: String) {
synchronized(fileLock) { editView?.let {
val currentEditView = editView file.content = it.markdown
if (currentEditView != null) { it.setTitle(newName)
file!!.content = currentEditView.markdown it.markdown = ""
currentEditView.setTitle(newName)
currentEditView.markdown = ""
}
file = MarkdownFile(newName, "")
} }
file = MarkdownFile(newName, "")
} }
override fun setEditView(editView: MarkdownEditView) { override suspend fun saveMarkdown(name: String, outputStream: OutputStream): Boolean {
this.editView = editView val result = withContext(Dispatchers.IO) {
onMarkdownEdited(null) file.save(name, outputStream)
}
override fun setPreviewView(previewView: MarkdownPreviewView) {
this.previewView = previewView
}
override fun saveMarkdown(listener: MarkdownPresenter.MarkdownSavedListener, name: String, outputStream: OutputStream) {
val fileSaver = {
val result: Boolean
synchronized(fileLock) {
result = file!!.save(name, outputStream)
}
listener?.saveComplete(result)
val currentEditView = editView
if (currentEditView != null) {
synchronized(fileLock) {
currentEditView.setTitle(file!!.name)
}
currentEditView.onFileSaved(result)
}
} }
fileHandler.post(fileSaver) editView?.let {
it.setTitle(file.name)
it.onFileSaved(result)
}
return result
} }
override fun onMarkdownEdited(markdown: String?) { override fun onMarkdownEdited(markdown: String?) {
this.markdown = markdown ?: file?.content ?: "" this.markdown = markdown ?: file.content
fileHandler.post { previewView?.updatePreview(generateHTML(this.markdown))
val currentPreviewView = previewView
currentPreviewView?.updatePreview(generateHTML(null))
}
} }
override fun generateHTML(markdown: String?): String { override fun generateHTML(markdown: String): String {
val andDown = AndDown() return AndDown().markdownToHtml(markdown, HOEDOWN_FLAGS, 0)
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
return andDown.markdownToHtml(markdown, HOEDOWN_FLAGS, 0)
} }
override fun loadFromUri(context: Context, fileUri: Uri) { override suspend fun loadFromUri(context: Context, fileUri: Uri): String? {
try { return try {
val `in` = context.contentResolver.openInputStream(fileUri)
var fileName: String? = null var fileName: String? = null
if ("content" == fileUri.scheme) { if ("content" == fileUri.scheme) {
val retCur = context.contentResolver context.contentResolver
.query(fileUri, null, null, null, null) .query(
if (retCur != null) { fileUri,
val nameIndex = retCur null,
.getColumnIndex(OpenableColumns.DISPLAY_NAME) null,
retCur.moveToFirst() null,
fileName = retCur.getString(nameIndex) null
retCur.close() )
} ?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
fileName = it.getString(nameIndex)
}
} else if ("file" == fileUri.scheme) { } else if ("file" == fileUri.scheme) {
fileName = fileUri.lastPathSegment fileName = fileUri.lastPathSegment
} }
if (fileName == null) { val inputStream = context.contentResolver.openInputStream(fileUri) ?: return null
fileName = "Untitled.md" loadMarkdown(fileName ?: "Untitled.md", inputStream, true)
}
loadMarkdown(fileName, `in`!!, null, true)
} catch (e: Exception) { } catch (e: Exception) {
errorHandler.reportException(e) errorHandler.reportException(e)
val currentEditView = editView editView?.onFileLoaded(false)
currentEditView?.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,40 +0,0 @@
package com.wbrawner.simplemarkdown.utility;
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public class MarkdownObserver implements Observer<String> {
private MarkdownPresenter presenter;
private Observable<String> obs;
public MarkdownObserver(MarkdownPresenter presenter, Observable<String> obs) {
this.presenter = presenter;
this.obs = obs;
}
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(String markdown) {
presenter.onMarkdownEdited(markdown);
}
@Override
public void onError(Throwable e) {
System.err.println("An error occurred while handling the markdown");
e.printStackTrace();
// TODO: report this?
obs.subscribe(this);
}
@Override
public void onComplete() {
}
}

View file

@ -1,59 +0,0 @@
package com.wbrawner.simplemarkdown.utility;
import android.graphics.Color;
import android.text.SpannableString;
import android.text.style.BackgroundColorSpan;
import android.util.Log;
import android.widget.EditText;
import android.widget.TextView;
import com.wbrawner.simplemarkdown.model.Readability;
import com.wbrawner.simplemarkdown.model.Sentence;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public class ReadabilityObserver implements Observer<String> {
private EditText text;
private String previousValue = "";
public ReadabilityObserver(EditText text) {
this.text = text;
}
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(String markdown) {
long start = System.currentTimeMillis();
if (markdown.length() < 1) return;
if (previousValue.equals(markdown)) return;
Readability readability = new Readability(markdown);
SpannableString span = new SpannableString(markdown);
for (Sentence sentence : readability.sentences()) {
int color = Color.TRANSPARENT;
if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42);
if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66);
span.setSpan(new BackgroundColorSpan(color), sentence.start(), sentence.end(), 0);
}
text.setTextKeepState(span, TextView.BufferType.SPANNABLE);
previousValue = markdown;
long timeTakenMs = System.currentTimeMillis() - start;
Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms");
}
@Override
public void onError(Throwable e) {
System.err.println("An error occurred while handling the markdown");
e.printStackTrace();
// TODO: report this?
}
@Override
public void onComplete() {
}
}

View file

@ -1,10 +0,0 @@
package com.wbrawner.simplemarkdown.view
interface MarkdownEditView {
var markdown: String
fun setTitle(title: String)
fun onFileSaved(success: Boolean)
fun onFileLoaded(success: Boolean)
}

View file

@ -1,5 +0,0 @@
package com.wbrawner.simplemarkdown.view
interface MarkdownPreviewView {
fun updatePreview(html: String)
}

View file

@ -24,19 +24,20 @@ import com.wbrawner.simplemarkdown.utility.ErrorHandler
import com.wbrawner.simplemarkdown.utility.Utils import com.wbrawner.simplemarkdown.utility.Utils
import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope {
@Inject @Inject
lateinit var presenter: MarkdownPresenter lateinit var presenter: MarkdownPresenter
@Inject @Inject
lateinit var errorHandler: ErrorHandler lateinit var errorHandler: ErrorHandler
private var shouldAutoSave = true private var shouldAutoSave = true
private var newFileHandler: NewFileHandler? = null override val coroutineContext: CoroutineContext = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -65,7 +66,9 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
super.onUserLeaveHint() super.onUserLeaveHint()
if (shouldAutoSave && presenter.markdown.isNotEmpty() && Utils.isAutosaveEnabled(this)) { if (shouldAutoSave && presenter.markdown.isNotEmpty() && Utils.isAutosaveEnabled(this)) {
presenter.saveMarkdown(null, "autosave.md", File(filesDir, "autosave.md").outputStream()) launch {
presenter.saveMarkdown("autosave.md", File(filesDir, "autosave.md").outputStream())
}
} }
} }
@ -130,28 +133,18 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
} }
} }
infoIntent.putExtra("title", title) infoIntent.putExtra("title", title)
var `in`: InputStream? = null launch {
try { try {
val assetManager = assets val inputStream = assets?.open(fileName)
if (assetManager != null) { ?: throw RuntimeException("Unable to open stream to $fileName")
`in` = assetManager.open(fileName) val html = presenter.loadMarkdown(fileName, inputStream, false)
infoIntent.putExtra("html", html)
startActivity(infoIntent)
} catch (e: Exception) {
errorHandler.reportException(e)
Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT).show()
} }
presenter.loadMarkdown(fileName, `in`, object : MarkdownPresenter.FileLoadedListener {
override fun onSuccess(html: String) {
infoIntent.putExtra("html", html)
startActivity(infoIntent)
}
override fun onError() {
Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT)
.show()
}
}, false)
} catch (e: Exception) {
errorHandler.reportException(e)
Toast.makeText(this@MainActivity, R.string.file_load_error, Toast.LENGTH_SHORT).show()
} }
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
@ -197,12 +190,13 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
contentResolver.openFileDescriptor(data.data!!, "r")?.let { contentResolver.openFileDescriptor(data.data!!, "r")?.let {
val fileInput = FileInputStream(it.fileDescriptor) val fileInput = FileInputStream(it.fileDescriptor)
presenter.loadMarkdown(fileName, fileInput) launch {
presenter.loadMarkdown(fileName, fileInput)
}
} }
} }
REQUEST_SAVE_FILE -> { REQUEST_SAVE_FILE -> {
if (resultCode != Activity.RESULT_OK if (resultCode != Activity.RESULT_OK || data?.data == null) {
|| data?.data == null) {
return return
} }
@ -213,11 +207,14 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
cursor.getString(nameIndex) cursor.getString(nameIndex)
} ?: "Untitled.md" } ?: "Untitled.md"
presenter.saveMarkdown( launch {
newFileHandler, val outputStream = contentResolver.openOutputStream(data.data!!)
fileName, ?: throw RuntimeException("Unable to open output stream to save file")
contentResolver.openOutputStream(data.data!!) presenter.saveMarkdown(
) fileName,
outputStream
)
}
} }
REQUEST_DARK_MODE -> recreate() REQUEST_DARK_MODE -> recreate()
} }
@ -228,11 +225,10 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
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) { d, _ -> .setNegativeButton(R.string.action_discard) { _, _ ->
presenter.newFile("Untitled.md") presenter.newFile("Untitled.md")
} }
.setPositiveButton(R.string.action_save) { d, _ -> .setPositiveButton(R.string.action_save) { _, _ ->
newFileHandler = NewFileHandler()
requestFileOp(REQUEST_SAVE_FILE) requestFileOp(REQUEST_SAVE_FILE)
} }
.create() .create()
@ -283,17 +279,11 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
shouldAutoSave = true shouldAutoSave = true
} }
private inner class NewFileHandler : MarkdownPresenter.MarkdownSavedListener {
override fun saveComplete(success: Boolean) { override fun onDestroy() {
if (success) { super.onDestroy()
presenter.newFile("Untitled.md") coroutineContext[Job]?.let {
} else { cancel()
Toast.makeText(
this@MainActivity,
R.string.file_save_error,
Toast.LENGTH_SHORT
).show()
}
} }
} }
} }

View file

@ -10,25 +10,28 @@ 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.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.utility.ErrorHandler import com.wbrawner.simplemarkdown.utility.ErrorHandler
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
class SplashActivity : AppCompatActivity() { class SplashActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
@Inject @Inject
internal var presenter: MarkdownPresenter? = null lateinit var presenter: MarkdownPresenter
@Inject @Inject
internal var errorHandler: ErrorHandler? = null 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) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
(application as MarkdownApplication).component.inject(this) (application as MarkdownApplication).component.inject(this)
if (sharedPreferences.getBoolean(getString(R.string.error_reports_enabled), true)) { if (sharedPreferences.getBoolean(getString(R.string.error_reports_enabled), true)) {
errorHandler!!.init(this) errorHandler.init(this)
} }
val darkModeValue = sharedPreferences.getString( val darkModeValue = sharedPreferences.getString(
@ -36,11 +39,10 @@ class SplashActivity : AppCompatActivity() {
getString(R.string.pref_value_auto) getString(R.string.pref_value_auto)
) )
var darkMode: Int var darkMode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { AppCompatDelegate.MODE_NIGHT_AUTO
darkMode = AppCompatDelegate.MODE_NIGHT_AUTO
} else { } else {
darkMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
if (darkModeValue != null && !darkModeValue.isEmpty()) { if (darkModeValue != null && !darkModeValue.isEmpty()) {
@ -52,33 +54,26 @@ class SplashActivity : AppCompatActivity() {
} }
AppCompatDelegate.setDefaultNightMode(darkMode) AppCompatDelegate.setDefaultNightMode(darkMode)
val intent = intent if (intent?.data != null) {
if (intent != null && intent.data != null) { launch {
presenter!!.loadFromUri(applicationContext, intent.data) presenter.loadFromUri(applicationContext, intent.data!!)
}
} else { } else {
presenter!!.fileName = "Untitled.md" presenter.fileName = "Untitled.md"
val autosave = File(filesDir, "autosave.md") val autosave = File(filesDir, "autosave.md")
if (autosave.exists()) { if (autosave.exists()) {
try { try {
val fileInputStream = FileInputStream(autosave) launch {
presenter!!.loadMarkdown( presenter.loadMarkdown(
"Untitled.md", "Untitled.md",
fileInputStream, autosave.inputStream(),
object : MarkdownPresenter.FileLoadedListener { true
override fun onSuccess(markdown: String) { )
autosave.delete() autosave.delete()
} }
override fun onError() {
autosave.delete()
}
},
true
)
} catch (ignored: FileNotFoundException) { } catch (ignored: FileNotFoundException) {
return return
} }
} }
} }
@ -86,4 +81,11 @@ class SplashActivity : AppCompatActivity() {
startActivity(startIntent) startActivity(startIntent)
finish() finish()
} }
override fun onDestroy() {
super.onDestroy()
coroutineContext[Job]?.let {
cancel()
}
}
} }

View file

@ -1,72 +1,126 @@
package com.wbrawner.simplemarkdown.view.fragment package com.wbrawner.simplemarkdown.view.fragment
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.text.Editable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.style.BackgroundColorSpan
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup 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.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding2.widget.RxTextView
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.presentation.MarkdownEditView
import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.utility.MarkdownObserver
import com.wbrawner.simplemarkdown.utility.ReadabilityObserver
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.MarkdownEditView
import com.wbrawner.simplemarkdown.view.ViewPagerPage import com.wbrawner.simplemarkdown.view.ViewPagerPage
import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.*
import io.reactivex.schedulers.Schedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.math.abs import kotlin.math.abs
class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage { class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage, CoroutineScope {
@Inject @Inject
lateinit var presenter: MarkdownPresenter 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
get() = markdownEditor?.text?.toString() ?: ""
set(value) {
markdownEditor?.setText(value)
}
override val coroutineContext: CoroutineContext = Dispatchers.Main
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? =
// Inflate the layout for this fragment inflater.inflate(R.layout.fragment_edit, container, false)
val view = inflater.inflate(R.layout.fragment_edit, container, false)
markdownEditor = view.findViewById(R.id.markdown_edit)
markdownEditorScroller = view.findViewById(R.id.markdown_edit_container)
val activity = activity
if (activity != null) {
(activity.application as MarkdownApplication).component.inject(this)
}
val obs = RxTextView.textChanges(markdownEditor!!)
.debounce(50, TimeUnit.MILLISECONDS)
.map { it.toString() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
obs.subscribe(MarkdownObserver(presenter, obs))
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val enableReadability = sharedPrefs.getBoolean(getString(R.string.readability_enabled), false)
if (enableReadability) {
val readabilityObserver = RxTextView.textChanges(markdownEditor!!)
.debounce(250, TimeUnit.MILLISECONDS)
.map { it.toString() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
readabilityObserver.subscribe(ReadabilityObserver(markdownEditor))
}
return view
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.setEditView(this@EditFragment) markdownEditor = view.findViewById(R.id.markdown_edit)
markdownEditorScroller = view.findViewById(R.id.markdown_edit_container)
markdownEditor?.addTextChangedListener(object : TextWatcher {
private var searchFor = ""
override fun afterTextChanged(s: Editable?) {
val searchText = s.toString().trim()
if (searchText == searchFor)
return
searchFor = searchText
launch {
delay(50)
if (searchText != searchFor)
return@launch
presenter.onMarkdownEdited(searchText)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val enableReadability = sharedPrefs.getBoolean(getString(R.string.readability_enabled), false)
if (enableReadability) {
markdownEditor?.addTextChangedListener(object : TextWatcher {
private var previousValue = ""
private var searchFor = ""
override fun afterTextChanged(s: Editable?) {
val searchText = s.toString().trim()
if (searchText == searchFor)
return
searchFor = searchText
launch {
delay(250)
if (searchText != searchFor)
return@launch
val start = System.currentTimeMillis()
if (markdown.isEmpty()) return@launch
if (previousValue == markdown) return@launch
val readability = Readability(markdown)
val span = SpannableString(markdown)
for (sentence in readability.sentences()) {
var color = Color.TRANSPARENT
if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42)
if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66)
span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0)
}
markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE)
previousValue = markdown
val timeTakenMs = System.currentTimeMillis() - start
Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms")
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
}
var touchDown = 0L var touchDown = 0L
var oldX = 0f var oldX = 0f
@ -92,18 +146,32 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage {
} }
} }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@Suppress("CAST_NEVER_SUCCEEDS")
(activity?.application as? MarkdownApplication)?.component?.inject(this)
presenter.editView = this@EditFragment
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
presenter.setEditView(this) presenter.editView = this
markdown = presenter.markdown markdown = presenter.markdown
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
presenter.setEditView(null) presenter.editView = null
markdownEditor?.hideKeyboard() markdownEditor?.hideKeyboard()
} }
override fun onDestroy() {
coroutineContext[Job]?.let {
cancel()
}
super.onDestroy()
}
override fun onSelected() { override fun onSelected() {
markdownEditor?.showKeyboard() markdownEditor?.showKeyboard()
} }
@ -112,14 +180,6 @@ class EditFragment : Fragment(), MarkdownEditView, ViewPagerPage {
markdownEditor?.hideKeyboard() markdownEditor?.hideKeyboard()
} }
override fun getMarkdown(): String {
return markdownEditor!!.text.toString()
}
override fun setMarkdown(markdown: String) {
markdownEditor?.setText(markdown)
}
override fun setTitle(title: String) { override fun setTitle(title: String) {
val activity = activity val activity = activity
if (activity != null) { if (activity != null) {

View file

@ -15,7 +15,7 @@ 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.presentation.MarkdownPresenter
import com.wbrawner.simplemarkdown.view.MarkdownPreviewView import com.wbrawner.simplemarkdown.presentation.MarkdownPreviewView
import javax.inject.Inject import javax.inject.Inject
@ -50,7 +50,7 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
} }
@Suppress("ConstantConditionIf") @Suppress("ConstantConditionIf")
val css: String? = if (!BuildConfig.ENABLE_CUSTOM_CSS) { val css: String? = if (!BuildConfig.ENABLE_CUSTOM_CSS) {
getString(defaultCssId) context?.getString(defaultCssId)
} else { } else {
sharedPreferences!!.getString( sharedPreferences!!.getString(
getString(R.string.pref_custom_css), getString(R.string.pref_custom_css),
@ -70,7 +70,7 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
presenter.setPreviewView(this) presenter.previewView = this
presenter.onMarkdownEdited() presenter.onMarkdownEdited()
} }
@ -78,13 +78,14 @@ class PreviewFragment : Fragment(), MarkdownPreviewView {
markdownPreview?.let { markdownPreview?.let {
(it.parent as ViewGroup).removeView(it) (it.parent as ViewGroup).removeView(it)
it.destroy() it.destroy()
markdownPreview = null
} }
super.onDestroyView() super.onDestroyView()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
presenter.setPreviewView(null) presenter.previewView = null
} }
companion object { companion object {