You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1201 lines
44 KiB
1201 lines
44 KiB
package org.wntr.mdeditor
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.Activity
|
|
import android.app.AlertDialog
|
|
import android.content.ActivityNotFoundException
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.net.Uri.parse
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.ParcelFileDescriptor
|
|
import android.provider.DocumentsContract
|
|
import android.provider.OpenableColumns
|
|
import android.util.DisplayMetrics
|
|
import android.util.Log
|
|
import android.view.Menu
|
|
import android.view.Menu.NONE
|
|
import android.view.MenuItem
|
|
import android.view.WindowInsets
|
|
import android.view.WindowManager
|
|
import android.webkit.ConsoleMessage
|
|
import android.webkit.JavascriptInterface
|
|
import android.webkit.ValueCallback
|
|
import android.webkit.WebChromeClient
|
|
import android.webkit.WebView
|
|
import android.widget.Toast
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
import androidx.activity.result.PickVisualMediaRequest
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.content.FileProvider
|
|
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
import okhttp3.MultipartBody
|
|
import okhttp3.RequestBody.Companion.asRequestBody
|
|
import okhttp3.ResponseBody
|
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
import org.json.JSONObject
|
|
import org.wntr.mdeditor.databinding.ActivityMainBinding
|
|
import java.io.BufferedReader
|
|
import java.io.File
|
|
import java.io.FileOutputStream
|
|
import java.io.IOException
|
|
import java.io.InputStream
|
|
import java.io.InputStreamReader
|
|
import java.lang.Thread.sleep
|
|
import java.net.URLDecoder
|
|
import java.time.Instant
|
|
import kotlin.concurrent.fixedRateTimer
|
|
|
|
|
|
class MainActivity : AppCompatActivity() {
|
|
private lateinit var binding: ActivityMainBinding
|
|
private val easyMDEscript = """
|
|
const easyMDE = new EasyMDE({
|
|
spellChecker: false,
|
|
nativeSpellcheck: false,
|
|
maxHeight: String(windowHeight-120)+"px",
|
|
inputStyle: "textarea",
|
|
autoDownloadFontAwesome: false,
|
|
theme: "solarized",
|
|
status: [
|
|
{
|
|
className: "editor-statusbar-left",
|
|
onUpdate: (el) => {
|
|
el.innerHTML = "<i class=\"fa fa-circle\"></i>"
|
|
}
|
|
},
|
|
{
|
|
className: "displayName",
|
|
defaultValue: "None",
|
|
onUpdate: (el) => {
|
|
el.innerHTML = displayName()
|
|
},
|
|
}, "lines", "words", "cursor",
|
|
{
|
|
className: "editor-statusbar-right",
|
|
onUpdate: (el) => {
|
|
el.innerHTML = "<i class=\"fa fa-square\"></i>"
|
|
}
|
|
}
|
|
],
|
|
toolbar: [
|
|
{
|
|
name: "toggleTheme",
|
|
action: toggleTheme,
|
|
className: "fa fa-moon",
|
|
title: "Toggle Theme"
|
|
},
|
|
{
|
|
name: "share",
|
|
action: shareText,
|
|
className: "fa fa-share-nodes",
|
|
title: "Share"
|
|
},"strikethrough", "horizontal-rule","undo",
|
|
{
|
|
name: "preview",
|
|
action: myPreview,
|
|
className: "fa fa-eye",
|
|
title: "Preview",
|
|
noDisable: true
|
|
},"redo",
|
|
"bold", "italic","link","code",
|
|
{
|
|
name: "toggle",
|
|
action: toggleBar,
|
|
className: "fa fa-expand",
|
|
title: "Toggle Bar",
|
|
}
|
|
]
|
|
});
|
|
"""
|
|
|
|
|
|
companion object {
|
|
const val CREATE_FILE = 1
|
|
const val OPEN_FILE = 2
|
|
const val DELETE_GHOST = 4
|
|
var deleteVisible = false
|
|
var mdeValue: String = ""
|
|
var metaData = mdMeta()
|
|
var mdToAppend: String = ""
|
|
var thisFileUri: Uri? = null
|
|
var truncate = false
|
|
lateinit var tempFile: File
|
|
lateinit var pickMultipleVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
|
|
lateinit var ghostSettings: ActivityResultLauncher<Intent>
|
|
lateinit var ghostMetaData: ActivityResultLauncher<Intent>
|
|
lateinit var webView: WebView
|
|
lateinit var api: ghostAPI
|
|
var ghostConnection = false
|
|
lateinit var credManager: CredentialManager
|
|
var intentScheme = "none"
|
|
var easyMDELoaded = false
|
|
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
setContentView(R.layout.activity_main)
|
|
|
|
setSupportActionBar(findViewById(R.id.toolbar)) // deprecated source
|
|
supportActionBar!!.setDisplayShowTitleEnabled(false)
|
|
|
|
deleteCache()
|
|
|
|
Log.i(javaClass.simpleName, "intent data on start: ${intent.data.toString()}\nIntent action: ${intent.action}")
|
|
|
|
credManager = CredentialManager(applicationContext)
|
|
webView = findViewById<WebView>(R.id.mde_webview)
|
|
webView.settings.javaScriptEnabled = true
|
|
webView.setLongClickable(true);
|
|
|
|
webView.webChromeClient = object : WebChromeClient() {
|
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
|
Log.d("WebView", consoleMessage.message())
|
|
return true
|
|
}
|
|
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
|
super.onProgressChanged(view, newProgress)
|
|
Log.d(javaClass.simpleName, "new progress: ${newProgress}")
|
|
if (newProgress == 100) {
|
|
webView.evaluateJavascript(easyMDEscript, {
|
|
easyMDELoaded = true
|
|
Log.d(javaClass.simpleName, "easyMDE loaded")
|
|
})
|
|
}
|
|
}
|
|
}
|
|
webView.loadUrl("file:///android_res/raw/index.html")
|
|
|
|
val jsi = object {
|
|
@JavascriptInterface
|
|
fun getValue(): String {
|
|
return mdeValue
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun reportChange(value: String) {
|
|
Log.i(javaClass.simpleName, value)
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun triggerSaveFile(value: String) {
|
|
mdeValue = value
|
|
saveFile()
|
|
}
|
|
|
|
/*@JavascriptInterface
|
|
fun getHeight(): Int {
|
|
val displayMetrics = DisplayMetrics()
|
|
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
val height = displayMetrics.heightPixels
|
|
Log.i(javaClass.simpleName, "display is $height pixels high.")
|
|
return height
|
|
}*/
|
|
|
|
@JavascriptInterface
|
|
fun refresh() {
|
|
readFile(thisFileUri!!)
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun triggerDisplayName(): String {
|
|
return getDisplayName(thisFileUri)
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun triggerShare(sharedText: String) {
|
|
shareHtml(sharedText)
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun triggerGhost(sharedText: String) {
|
|
shareGhost(sharedText, ::sendPost)
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun getMdToAppend(): String {
|
|
val md = mdToAppend
|
|
mdToAppend = ""
|
|
return md
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun getCursor(): String {
|
|
val cursor = JSONObject(JSONObject(metaData.cursor), arrayOf("ch", "line"))
|
|
Log.i(javaClass.simpleName,"delivering cursor: $cursor")
|
|
return cursor.toString()
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun isFullscreen() : Boolean{
|
|
return supportActionBar!!.isShowing
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun toggleBar() {
|
|
this@MainActivity.runOnUiThread({
|
|
if (supportActionBar!!.isShowing) {
|
|
@Suppress("DEPRECATION")
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
window.insetsController?.hide(WindowInsets.Type.statusBars())
|
|
} else {
|
|
window.setFlags(
|
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
0
|
|
)
|
|
}
|
|
supportActionBar!!.hide()
|
|
}
|
|
else {
|
|
@Suppress("DEPRECATION")
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
window.insetsController?.show(WindowInsets.Type.statusBars())
|
|
} else {
|
|
window.setFlags(
|
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
)
|
|
}
|
|
supportActionBar!!.show()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
webView.addJavascriptInterface(jsi, "Android")
|
|
|
|
pickMultipleVisualMedia = registerForActivityResult(
|
|
ActivityResultContracts.PickMultipleVisualMedia(
|
|
5
|
|
)
|
|
) { uris ->
|
|
// Process URIs
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"Uploading\n$uris",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
|
|
// This check for connection needs to be refined once multiple accounts are supported
|
|
checkGhostConnection()
|
|
|
|
for (uri in uris) pushImage(uri)
|
|
|
|
Log.d(javaClass.simpleName, "Photo Picker URIs count ${uris.size}")
|
|
}
|
|
ghostSettings = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
checkGhostConnection()
|
|
}
|
|
ghostMetaData = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
saveFile()
|
|
}
|
|
|
|
fixedRateTimer("timer",true,0,5000){
|
|
this@MainActivity.runOnUiThread {
|
|
val script = "easyMDE.codemirror.doc.isClean();"
|
|
webView.evaluateJavascript(script , {
|
|
if (it == "false" && thisFileUri != null) {
|
|
saveFile()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
|
|
loadMetaFromSharedPrefs()
|
|
if (intent.data !== null) {
|
|
readFile(intent.data!!)
|
|
Log.d(javaClass.simpleName,"Loading on resume from intent: ${thisFileUri}")
|
|
intent.data = null
|
|
} else if (thisFileUri !== null ) {
|
|
readFile(thisFileUri!!)
|
|
Log.d(javaClass.simpleName,"Loading on resume from thisFileUri: ${thisFileUri}")
|
|
}
|
|
|
|
if (metaData.metaData.get("url") !== null) {
|
|
val url = parse(metaData.metaData.get("url"))
|
|
deleteVisible = true
|
|
invalidateOptionsMenu()
|
|
val apiHost = url.scheme + "://" + url.host
|
|
Log.i(javaClass.simpleName, "Starting api controller for: $apiHost")
|
|
api = ghostAPI(applicationContext, apiHost)
|
|
}
|
|
}
|
|
override fun onNewIntent(intent:Intent) {
|
|
super.onNewIntent(intent)
|
|
|
|
Log.i(javaClass.simpleName, "intent data onNewIntent: ${intent.data.toString()}\nIntent action: ${intent.action}")
|
|
|
|
var uri:Uri? = null
|
|
var mimeType: String? = null
|
|
|
|
if (intent.action == Intent.ACTION_SEND && intent.extras != null) {
|
|
if (intent.extras!!.get(Intent.EXTRA_STREAM) != null){
|
|
uri = intent.extras!!.get(Intent.EXTRA_STREAM) as Uri
|
|
mimeType = "image"
|
|
} else {
|
|
uri = parse(intent.extras!!.getCharSequence(Intent.EXTRA_TEXT).toString())
|
|
}
|
|
} else if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
|
uri = parse(intent.dataString)
|
|
mimeType = contentResolver.getType(uri)
|
|
if (mimeType != null) {
|
|
if (mimeType.split("/")[0] == "text" ) {
|
|
Log.i(javaClass.simpleName, "Wanna open a text file")
|
|
mimeType = "text"
|
|
}
|
|
else if (mimeType.split("/")[0] == "image" ) {
|
|
Log.i(javaClass.simpleName, "Wanna upload a image")
|
|
mimeType = "image"
|
|
}
|
|
}
|
|
}
|
|
Log.i(javaClass.simpleName,"Got a new intent with URI: $uri")
|
|
if (uri != null && uri.toString()!="null") {
|
|
intentScheme = uri.scheme!!
|
|
if (intentScheme == "content") {
|
|
Log.i(javaClass.simpleName, "content intent")
|
|
}
|
|
else if (intentScheme =="http" || intentScheme == "https") {
|
|
intentScheme = "link"
|
|
Log.i(javaClass.simpleName, "link intent")
|
|
}
|
|
else Log.i(javaClass.simpleName, "unknown intent")
|
|
}
|
|
|
|
if (intentScheme == "link" && uri.toString() !="null") {
|
|
mdToAppend += "[](${uri})\n"
|
|
intent.putExtra(Intent.EXTRA_TEXT, null as CharSequence?)
|
|
}
|
|
if (intentScheme == "content") {
|
|
if (mimeType == "image" && uri != null) {
|
|
pushImage(uri)
|
|
intent.putExtra(Intent.EXTRA_STREAM, null as Uri?)
|
|
} else if (mimeType == "text" && intent.data != null ) {
|
|
thisFileUri = uri
|
|
saveMetaToSharedPrefs()
|
|
Log.d(javaClass.simpleName,"wanna start app with new txt")
|
|
}
|
|
}
|
|
}
|
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
menuInflater.inflate(R.menu.ghost_menu, menu)
|
|
return true
|
|
}
|
|
|
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
|
if (deleteVisible) {if (menu.findItem(DELETE_GHOST) == null) menu.add(NONE,DELETE_GHOST, NONE,"Delete" )}
|
|
else menu.removeItem(DELETE_GHOST)
|
|
return true
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
val id = item.itemId
|
|
when (id) {
|
|
R.id.new_file -> {
|
|
truncate = true
|
|
selectFileForSaveAs()
|
|
}
|
|
|
|
R.id.open_file -> {
|
|
openFile()
|
|
}
|
|
|
|
R.id.save_file -> {
|
|
saveFile()
|
|
selectFileForSaveAs()
|
|
}
|
|
|
|
R.id.push_ghost -> {
|
|
webView.evaluateJavascript("getHtml();", {
|
|
val msg = URLDecoder.decode(it.removeSurrounding("\""))
|
|
if (metaData.metaData.get("url") !== null) {
|
|
CoroutineScope(Dispatchers.Main).launch {
|
|
withContext(Dispatchers.IO) {
|
|
if (metaData.getId() == null) {
|
|
this@MainActivity.runOnUiThread({
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"No ID for url in Metadata found. Deleted? Trying repost.",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
sendPosting(html=msg, author = credManager.username)
|
|
} else {
|
|
Log.i(javaClass.simpleName,"posting $msg")
|
|
updatePost(
|
|
title = metaData.get("title") ?: getDisplayName(thisFileUri),
|
|
author = credManager.username,
|
|
html = msg,
|
|
id = metaData.getId()!!
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
shareGhost(msg, ::sendPost)
|
|
})
|
|
}
|
|
R.id.settings -> {
|
|
ghostSettings.launch(Intent(this, LoginActivity::class.java))
|
|
}
|
|
R.id.image -> {
|
|
pickMultipleVisualMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
|
|
}
|
|
R.id.metadata -> {
|
|
ghostMetaData.launch(Intent(this, MetadataActivity::class.java))
|
|
}
|
|
DELETE_GHOST -> {
|
|
checkGhostConnection()
|
|
var response: retrofit2.Response<ResponseBody> = retrofit2.Response.error(
|
|
444,
|
|
"error".toResponseBody("text/plain".toMediaTypeOrNull())
|
|
)
|
|
CoroutineScope(Dispatchers.Main).launch {
|
|
withContext(Dispatchers.IO) {
|
|
if (metaData.ID == null) {
|
|
if (metaData.getId() == null) {
|
|
// No ID for URL found
|
|
saveFile()
|
|
deleteVisible = false
|
|
invalidateOptionsMenu()
|
|
return@withContext false
|
|
}
|
|
}
|
|
try {
|
|
response = api.postApi.deletePost(metaData.ID!!).execute()
|
|
} catch (e:Exception) {
|
|
Log.d(javaClass.simpleName, "Exception during DELETE: $e")
|
|
}
|
|
Log.d(javaClass.simpleName, "result: ${response.code()}")
|
|
}
|
|
if (response.isSuccessful) {
|
|
Log.d(javaClass.simpleName, "Post under ${metaData.metaData.get("url")} got deleted.")
|
|
metaData.metaData.minusAssign("url")
|
|
metaData.ID=null
|
|
deleteVisible = false
|
|
invalidateOptionsMenu()
|
|
saveFile()
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
fun getFile(context: Context, uri: Uri): File? {
|
|
val destinationFilename =
|
|
File(context.filesDir.path + File.separatorChar + queryName(context, uri))
|
|
try {
|
|
context.contentResolver.openInputStream(uri!!).use { ins ->
|
|
createFileFromStream(
|
|
ins!!,
|
|
destinationFilename
|
|
)
|
|
}
|
|
} catch (ex: java.lang.Exception) {
|
|
Log.e("Save File", ex.message!!)
|
|
ex.printStackTrace()
|
|
}
|
|
return destinationFilename
|
|
}
|
|
|
|
fun createFileFromStream(ins: InputStream, destination: File?) {
|
|
try {
|
|
FileOutputStream(destination).use { os ->
|
|
val buffer = ByteArray(4096)
|
|
var length: Int
|
|
while (ins.read(buffer).also { length = it } > 0) {
|
|
os.write(buffer, 0, length)
|
|
}
|
|
os.flush()
|
|
}
|
|
} catch (ex: java.lang.Exception) {
|
|
Log.e("Save File", ex.message!!)
|
|
ex.printStackTrace()
|
|
}
|
|
}
|
|
|
|
private fun queryName(context: Context, uri: Uri): String? {
|
|
val returnCursor = context.contentResolver.query(uri, null, null, null, null)!!
|
|
val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
returnCursor.moveToFirst()
|
|
val name = returnCursor.getString(nameIndex)
|
|
returnCursor.close()
|
|
return name
|
|
}
|
|
|
|
fun pushImage(uri: Uri) {
|
|
// see https://androidfortechs.blogspot.com/2020/12/how-to-convert-uri-to-file-android-10.html
|
|
val file = getFile(applicationContext, uri)!!
|
|
shareGhost(file, ::pushImageFile)
|
|
}
|
|
|
|
fun pushImageFile(username: String?, file: File): retrofit2.Response<Any> {
|
|
var response: retrofit2.Response<imagesObj> = retrofit2.Response.error(
|
|
444,
|
|
"error".toResponseBody("text/plain".toMediaTypeOrNull())
|
|
)
|
|
try {
|
|
response = api.postApi.pushMyImage(
|
|
MultipartBody.Part.createFormData(
|
|
"file",
|
|
file.name,
|
|
file.asRequestBody("image/jpeg".toMediaTypeOrNull())
|
|
)
|
|
).execute()
|
|
} catch (e:Exception) {
|
|
this.runOnUiThread(Runnable() {
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"Exception during image upload: $e",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
})
|
|
Log.i(javaClass.simpleName, "Exception during image upload:\n$e")
|
|
}
|
|
|
|
if (response.isSuccessful) {
|
|
val imgUrl = response.body()!!.images[0].url
|
|
Log.d(javaClass.simpleName, "\"${file.name}\" uploaded to \"$imgUrl\"")
|
|
mdToAppend += "![${file.name}]($imgUrl)\n"
|
|
this.runOnUiThread(Runnable() {
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"\"${file.name}\" uploaded to \"$imgUrl\"",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
webView.evaluateJavascript("pasteText()") {}
|
|
})
|
|
return response as retrofit2.Response<Any>
|
|
} else return response as retrofit2.Response<Any>
|
|
}
|
|
|
|
fun updatePost(title: String, html: String, author: String, id: String) : retrofit2.Response<Any> {
|
|
val post = sendPost(title, updated_at = metaData.updatedAt!!, authors = listOf(author), html, feature_image = metaData.get("feature_image"))
|
|
val postings = sendPostList(listOf(post))
|
|
|
|
var response: retrofit2.Response<posts> = retrofit2.Response.error(
|
|
444,
|
|
"error".toResponseBody("text/plain".toMediaTypeOrNull())
|
|
)
|
|
|
|
try {
|
|
response = api.postApi.updatePost(id, postings).execute()
|
|
} catch (ex: Exception) {
|
|
Log.d(javaClass.simpleName, "Couldn't update posting. Exception: ${ex}")
|
|
}
|
|
|
|
if (response.isSuccessful) {
|
|
Log.d(javaClass.simpleName, "Updated: ${response.body()!!.posts[0].url}")
|
|
metaData.updatedAt= response.body()!!.posts[0].updated_at
|
|
val intent = Intent(Intent.ACTION_VIEW).setData(parse(metaData.metaData.get("url")))
|
|
try {
|
|
startActivity(intent)
|
|
} catch (e: ActivityNotFoundException) {
|
|
this.runOnUiThread({
|
|
Toast.makeText(this, "No eligble app installed.", Toast.LENGTH_LONG).show()
|
|
Log.i(javaClass.simpleName, e.toString())
|
|
})
|
|
}
|
|
} else {
|
|
this.runOnUiThread({
|
|
Toast.makeText(
|
|
this,
|
|
"error while updating post.\n${response.errorBody()}",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
Log.i(javaClass.simpleName, response.code().toString())
|
|
}
|
|
return response as retrofit2.Response<Any>
|
|
|
|
}
|
|
fun sendPosting(html: String, author: String): retrofit2.Response<Any> {
|
|
val title = metaData.get("title") ?: "test"
|
|
val post = sendPost(title, updated_at = Instant.now().toString(), authors = listOf(author), html, feature_image = metaData.get("feature_image"))
|
|
val postings = sendPostList(listOf(post))
|
|
var response: retrofit2.Response<posts> = retrofit2.Response.error(
|
|
444,
|
|
"error".toResponseBody("text/plain".toMediaTypeOrNull())
|
|
)
|
|
|
|
try {
|
|
response = api.postApi.pushPost(postings).execute()
|
|
} catch (ex: Exception) {
|
|
Log.d(javaClass.simpleName, "Couldn't send posting. Exception: ${ex}")
|
|
}
|
|
Log.d(javaClass.simpleName, "result: ${response.body()}")
|
|
if (response.isSuccessful) {
|
|
val post = response.body()!!.posts[0]
|
|
val uri = parse(post.url)
|
|
metaData.ID = post.id
|
|
metaData.updatedAt = post.updated_at
|
|
Log.d(javaClass.simpleName, "Uploaded to: $uri\nID: ${metaData.ID}")
|
|
|
|
metaData.put("url", uri.toString())
|
|
|
|
saveFile()
|
|
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
|
|
try {
|
|
startActivity(intent)
|
|
} catch (e: ActivityNotFoundException) {
|
|
Toast.makeText(this, "No eligble app installed.", Toast.LENGTH_LONG).show()
|
|
Log.i(javaClass.simpleName, e.toString())
|
|
}
|
|
}
|
|
return response as retrofit2.Response<Any>
|
|
}
|
|
|
|
fun sendPost(username: String, text: String): retrofit2.Response<Any> {
|
|
return sendPosting(html = text, author = username)
|
|
}
|
|
|
|
fun shareGhost(
|
|
text: String,
|
|
sendpost: (username: String, text: String) -> retrofit2.Response<Any>
|
|
) {
|
|
shareGhost(
|
|
text = text,
|
|
file = null,
|
|
sendpost = sendpost as (String?, Any?) -> retrofit2.Response<Any>
|
|
)
|
|
}
|
|
|
|
fun shareGhost(
|
|
file: File,
|
|
sendimage: (username: String, file: File) -> retrofit2.Response<Any>
|
|
) {
|
|
shareGhost(
|
|
text = null,
|
|
file,
|
|
sendpost = sendimage as (String?, Any?) -> retrofit2.Response<Any>
|
|
)
|
|
}
|
|
|
|
fun shareGhost(
|
|
text: String?,
|
|
file: File?,
|
|
sendpost: (username: String?, content: Any?) -> retrofit2.Response<Any>
|
|
) {
|
|
val instance = credManager.instance
|
|
if (instance == "nowhere") {
|
|
checkGhostConnection()
|
|
return
|
|
}
|
|
val username = credManager.username
|
|
|
|
api = ghostAPI(applicationContext, instance)
|
|
var result: retrofit2.Response<Any> =
|
|
retrofit2.Response.error(444, "error".toResponseBody("text/plain".toMediaTypeOrNull()))
|
|
|
|
CoroutineScope(Dispatchers.Main).launch {
|
|
withContext(Dispatchers.IO) {
|
|
if (text !== null) result = sendpost(username, text)
|
|
if (file !== null) result = sendpost(null, file)
|
|
}
|
|
if (text !== null && result.isSuccessful) {
|
|
deleteVisible = true
|
|
invalidateOptionsMenu()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun checkGhostConnection(): Boolean {
|
|
if (ghostConnection) return true
|
|
if (SharedPrefsCookiePersistor(applicationContext).loadAll().size == 0) {
|
|
ghostSettings.launch(Intent(this, LoginActivity::class.java))
|
|
return false
|
|
}
|
|
// we have a cookie
|
|
return true
|
|
}
|
|
//
|
|
|
|
fun deleteCache() {
|
|
try {
|
|
val dir = File(cacheDir, "html")
|
|
deleteDir(dir)
|
|
} catch (e: java.lang.Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
fun deleteDir(dir: File?): Boolean {
|
|
return if (dir != null && dir.isDirectory) {
|
|
val children = dir.list()
|
|
for (i in children.indices) {
|
|
val success = deleteDir(File(dir, children[i]))
|
|
if (!success) {
|
|
return false
|
|
}
|
|
}
|
|
dir.delete()
|
|
} else if (dir != null && dir.isFile) {
|
|
dir.delete()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fun shareHtml(htmlString: String): Boolean {
|
|
try {
|
|
createHTMLFile()
|
|
} catch (e: Exception) {
|
|
Log.d(javaClass.simpleName, "Problem with accessing file\n${e.stackTraceToString()}")
|
|
Toast.makeText(
|
|
this,
|
|
"Problem with accessing file\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
return false
|
|
}
|
|
|
|
try {
|
|
FileOutputStream(tempFile).use {
|
|
it.write(htmlString.toByteArray())
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
Toast.makeText(
|
|
this,
|
|
"Error during writing.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
return false
|
|
}
|
|
Log.d(javaClass.simpleName, "File saved: ${tempFile.toURI()}")
|
|
Toast.makeText(
|
|
this,
|
|
"HTML output produced.",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
|
|
val htmlUri = FileProvider.getUriForFile(this, "org.wntr.mdeditor.fileprovider", tempFile)
|
|
|
|
with(Intent(Intent.ACTION_SEND)) {
|
|
tempFile?.also {
|
|
type = "text/plain"
|
|
putExtra(Intent.EXTRA_STREAM, htmlUri)
|
|
startActivity(Intent.createChooser(this, "Share html"))
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
private fun createHTMLFile() {
|
|
// Create an image file name
|
|
val storageDir = File(cacheDir, "html")
|
|
storageDir.mkdir()
|
|
tempFile = File(storageDir.path + "/${getDisplayName(thisFileUri).split(".")[0]}.html")
|
|
if (tempFile.exists()) tempFile.delete()
|
|
tempFile.createNewFile()
|
|
}
|
|
|
|
fun openFile() {
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, thisFileUri)
|
|
|
|
type = "text/*"
|
|
|
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
}
|
|
startActivityForResult(intent, OPEN_FILE)
|
|
}
|
|
|
|
@SuppressLint("Range")
|
|
fun getDisplayName(uri: Uri?): String {
|
|
// via: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content
|
|
var result: String? = null;
|
|
if (uri == null) return "hauntED.md"
|
|
if (uri!!.getScheme().equals("content")) {
|
|
val cursor = getContentResolver().query(uri!!, null, null, null, null);
|
|
try {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
|
}
|
|
} finally {
|
|
cursor?.close();
|
|
}
|
|
}
|
|
if (result == null) {
|
|
result = uri!!.getPath();
|
|
val cut = result!!.lastIndexOf('/');
|
|
if (cut != -1) {
|
|
result = result.substring(cut + 1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fun selectFileForSaveAs() {
|
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "text/*"
|
|
putExtra(Intent.EXTRA_TITLE, getDisplayName(thisFileUri))
|
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
}
|
|
this.runOnUiThread({
|
|
Toast.makeText(
|
|
this,
|
|
"Please select a location for the new file.",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
startActivityForResult(intent, CREATE_FILE)
|
|
Log.i(javaClass.simpleName, "Buffer geschrieben")
|
|
}
|
|
|
|
/* Cut is not possible currently because of the deactivation of the custom text selection and
|
|
context menu mechanisms.
|
|
|
|
override fun onActionModeStarted(mode: ActionMode?) {
|
|
super.onActionModeStarted(mode)
|
|
val myContextMenu = mode!!.menu
|
|
myContextMenu.add(NONE, CONTEXT_MENU_CUT, NONE,"Cut" )
|
|
myContextMenu.getItem(0).setOnMenuItemClickListener {
|
|
Log.i(javaClass.simpleName,"somwhere in between")
|
|
when(it.itemId) {
|
|
CONTEXT_MENU_CUT -> {
|
|
webView.evaluateJavascript("dispatchCut();", ValueCallback<String>() {})
|
|
true
|
|
}
|
|
else -> {false}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
|
|
super.onActivityResult(requestCode, resultCode, resultData)
|
|
|
|
if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
|
|
// The result data contains a URI for the document or directory that
|
|
// the user selected.
|
|
resultData?.data?.also { uri ->
|
|
getContentResolver().takePersistableUriPermission(
|
|
uri,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
)
|
|
Log.d(javaClass.simpleName, "Saving to: ${getDisplayName(uri)}")
|
|
if (getDisplayName(thisFileUri) + " (1)" != getDisplayName(uri)) thisFileUri = uri
|
|
saveAs()
|
|
}
|
|
} else if (requestCode == OPEN_FILE && resultCode == Activity.RESULT_OK) {
|
|
resultData?.data?.also { uri ->
|
|
getContentResolver().takePersistableUriPermission(
|
|
uri,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION and Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
)
|
|
thisFileUri = uri
|
|
saveMetaToSharedPrefs()
|
|
if (metaData.metaData.get("url") == null) {
|
|
deleteVisible = false
|
|
invalidateOptionsMenu()
|
|
} else {
|
|
deleteVisible = true
|
|
invalidateOptionsMenu()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun saveFile() {
|
|
if (thisFileUri == null) return
|
|
lateinit var textFile: ParcelFileDescriptor
|
|
|
|
try {
|
|
textFile = contentResolver.openFileDescriptor(thisFileUri!!, "w")!!
|
|
textFile.checkError()
|
|
} catch (e: java.lang.IllegalArgumentException) {
|
|
Log.d(javaClass.simpleName, "Problem with saving file\nBug in Android 11?\n${e.stackTraceToString()}")
|
|
this.runOnUiThread(
|
|
{
|
|
Toast.makeText(
|
|
this,
|
|
"Problem with saving file\n" +
|
|
"Bug in Android 11?\n" +
|
|
"Workaround: Load file with normal file selection",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
)
|
|
return
|
|
// TODO: implement workaround for this bug. See: https://stackoverflow.com/q/69248596
|
|
} catch (e: java.io.FileNotFoundException) {
|
|
Log.d(javaClass.simpleName, "File not found. Deleted while loaded?\n${e.stackTraceToString()}")
|
|
this.runOnUiThread(
|
|
{
|
|
Toast.makeText(
|
|
this,
|
|
"File not found. Deleted while loaded?",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
)
|
|
selectFileForSaveAs()
|
|
return
|
|
}
|
|
|
|
catch (e: Exception) {
|
|
Log.d(javaClass.simpleName, "Problem with accessing file\n${e.stackTraceToString()}")
|
|
this.runOnUiThread(
|
|
{
|
|
Toast.makeText(
|
|
this,
|
|
"Problem with accessing file\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
this. runOnUiThread({
|
|
webView.evaluateJavascript("getValue();") {
|
|
mdeValue =
|
|
metaData.toString() + URLDecoder.decode(it.removeSurrounding("\""))
|
|
if (mdeValue.length.toLong() == textFile.statSize) {
|
|
Log.d(javaClass.simpleName, "No change on disk, file not saved.")
|
|
return@evaluateJavascript
|
|
}
|
|
contentResolver.openFileDescriptor(thisFileUri!!, "wt")?.use {
|
|
FileOutputStream(it.fileDescriptor).use {
|
|
it.write(mdeValue.toByteArray())
|
|
}
|
|
}
|
|
Log.d(javaClass.simpleName, "File saved: ${thisFileUri}")
|
|
textFile.close()
|
|
this@MainActivity.runOnUiThread {
|
|
webView.evaluateJavascript("easyMDE.codemirror.doc.markClean();", {})
|
|
}
|
|
}
|
|
})
|
|
} catch (e: Exception) {
|
|
Toast.makeText(
|
|
this,
|
|
"Error during writing.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
return
|
|
}
|
|
}
|
|
|
|
private fun saveAs() {
|
|
try {
|
|
lateinit var textFile: ParcelFileDescriptor
|
|
try {
|
|
textFile = contentResolver.openFileDescriptor(thisFileUri!!, "w")!!
|
|
textFile.checkError()
|
|
} catch (e: Exception) {
|
|
Toast.makeText(
|
|
this,
|
|
"Problem with accessing file\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
return
|
|
}
|
|
if (truncate) {
|
|
mdeValue = ""
|
|
truncate = false
|
|
}
|
|
FileOutputStream(textFile.fileDescriptor).use {
|
|
it.write(mdeValue.toByteArray())
|
|
}
|
|
Toast.makeText(
|
|
this,
|
|
"File saved.",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
textFile.close()
|
|
saveMetaToSharedPrefs()
|
|
Log.i(javaClass.simpleName, "file newly written")
|
|
} catch (e: Exception) {
|
|
Toast.makeText(
|
|
this,
|
|
"Error during writing.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
private fun readFile(uri: Uri) {
|
|
//TODO: Which permissions are really needed?
|
|
try {
|
|
contentResolver.takePersistableUriPermission(
|
|
uri,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
)
|
|
} catch (e: java.lang.SecurityException) {
|
|
Log.d(javaClass.simpleName, "Couldn't get persistable URI permissions. Trying to grant 'em.\n${e.stackTraceToString()}")
|
|
}
|
|
try {
|
|
grantUriPermission(
|
|
"org.wntr.mdeditor",
|
|
uri,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
)
|
|
} catch (e: java.lang.SecurityException) {
|
|
Log.d(javaClass.simpleName, e.stackTraceToString())
|
|
this.runOnUiThread({
|
|
Toast.makeText(
|
|
this,
|
|
"Security exception during file load. Try open manually.",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
}
|
|
|
|
CoroutineScope(Dispatchers.Main).launch {
|
|
withContext(Dispatchers.IO) {
|
|
try {
|
|
contentResolver.openInputStream(uri)?.use { inputStream ->
|
|
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
|
var i = ""
|
|
if (reader.ready()) {
|
|
i = reader.readLine()
|
|
var line: String? = ""
|
|
while (line != null) {
|
|
i += line + '\n'
|
|
line = reader.readLine()
|
|
}
|
|
}
|
|
mdeValue = metaData.extractMetadataFromMarkdown(i)
|
|
}
|
|
}
|
|
}
|
|
catch (e: java.io.FileNotFoundException) {
|
|
Log.d(javaClass.simpleName, "File not found during reading:\n${e.stackTraceToString()}")
|
|
this@MainActivity.runOnUiThread({
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"File not found during reading.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
return@withContext
|
|
}
|
|
catch (e: java.lang.NullPointerException) {
|
|
Log.d(javaClass.simpleName, "Nullpointerexception. Maybe file deleted in the meantime?.\n$e")
|
|
this@MainActivity.runOnUiThread({
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"Nullpointerexception. Maybe file deleted in the meantime?.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
return@withContext
|
|
}
|
|
catch (e: Exception) {
|
|
Log.d(javaClass.simpleName, e.stackTraceToString())
|
|
this@MainActivity.runOnUiThread({
|
|
Toast.makeText(
|
|
this@MainActivity,
|
|
"Error during reading.\n$e",
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
})
|
|
return@withContext
|
|
}
|
|
while (!easyMDELoaded) {
|
|
sleep(100)
|
|
Log.d(javaClass.simpleName,"waiting for easyMDE")
|
|
}
|
|
this@MainActivity.runOnUiThread({
|
|
|
|
val script = "if (typeof easyMDE !== 'undefined') {" +
|
|
"easyMDE.codemirror.doc.setValue(`${mdeValue}`);" +
|
|
"easyMDE.codemirror.doc.markClean();" +
|
|
"easyMDE.codemirror.focus();" +
|
|
"easyMDE.codemirror.doc.setCursor(JSON.parse(`${metaData.cursor}`));" +
|
|
"pasteText();" +
|
|
"easyMDE.updateStatusBar(\"displayName\",\"${getDisplayName(uri)}\");}"
|
|
|
|
Log.d(javaClass.simpleName, "executing in webview:\n${script}")
|
|
webView.evaluateJavascript(script, {
|
|
thisFileUri = uri
|
|
Log.d(javaClass.simpleName,"File read: ${thisFileUri}")
|
|
})
|
|
webView.requestFocus()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun saveMetaToSharedPrefs() {
|
|
Log.d(javaClass.simpleName, "saving to shared prefs cursor: ${metaData.cursor} in file: ${thisFileUri}")
|
|
getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
.edit().apply {
|
|
putString("lastFile", thisFileUri.toString())
|
|
putString("cursor", metaData.cursor)
|
|
apply()
|
|
}
|
|
}
|
|
|
|
private fun loadMetaFromSharedPrefs(){
|
|
val prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
val uriString = prefs.getString("lastFile", "noLastFile")
|
|
|
|
if (uriString == "noLastFile") {
|
|
with(AlertDialog.Builder(this)){
|
|
setPositiveButton("Open", { dialog, id ->
|
|
openFile()
|
|
})
|
|
setNeutralButton("New", { dialog, id ->
|
|
selectFileForSaveAs()
|
|
})
|
|
show()
|
|
}
|
|
} else {
|
|
thisFileUri = parse(uriString)
|
|
metaData.cursor = prefs.getString("cursor", "nocursor") ?: "{ line: 0, ch: 0, sticky: null }"
|
|
Log.i(javaClass.simpleName,"Loaded cursor: ${metaData.cursor}")
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
/*outState.putString("test", "onSaveInstanceState-String")*/
|
|
saveFile()
|
|
webView.evaluateJavascript("easyMDE.codemirror.doc.getCursor();") {
|
|
metaData.cursor=it
|
|
Log.i(javaClass.simpleName,"Cursor: $it")
|
|
saveMetaToSharedPrefs()
|
|
}
|
|
Log.i(javaClass.simpleName, "\"onPause\" durchlaufen")
|
|
}
|
|
|
|
override protected fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
super.onRestoreInstanceState(savedInstanceState)
|
|
/* findViewById<TextView>(R.id.edittext_lifecycle).text = savedInstanceState.getString("test")*/
|
|
/*webView.evaluateJavascript("getValue();", ValueCallback<String>() {
|
|
saveFile(it)
|
|
})*/
|
|
|
|
Log.i(javaClass.simpleName, "\"onRestoreInstanceState\" durchlaufen")
|
|
}
|
|
}
|
|
|
|
|