Compare commits

...

61 Commits

Author SHA1 Message Date
yova f9b2f87b8f bugoff
2 weeks ago
yova 2bc5d4b7bc bugoff
2 weeks ago
yova b30243916f copy all
2 weeks ago
yova f6dd99e59a truncate file in webview
2 months ago
yova 65107d07dd set editor load status correctly
3 months ago
yova c4828bdda5 release 0.23-3
3 months ago
yova b9ffe73652 improve auto layout adjustements
3 months ago
yova 3b0b00d89c allow exit from login activity
3 months ago
yova d14d96f3a0 make switch landscape/portait mode funtional
3 months ago
yova 09227c2712 refactor push to ghost
3 months ago
yova 8e9e09ba32 hquntED release 0.23-2
3 months ago
yova 37d82663b2 save us from the null
3 months ago
yova 7b54072fb5 catch more exceptions
3 months ago
yova 7c087562df cleanup JSI
3 months ago
yova 1c4ebfca18 refactor thisFileUri & metadata
3 months ago
yova ed94b85006 refactor load/save meta to shared prefs
3 months ago
yova 4997dd067b refactor HTML file share
3 months ago
yova f0d90c175f default creds update on ghost update
3 months ago
yova 1583c0009b add author to metadata
3 months ago
yova 3a7409ee6a stop autosavetimer for read. Close file after read.
3 months ago
yova 36b9d7bd0b trim down JSON parsing
3 months ago
yova 821ad615ee more constraints to layout
3 months ago
yova 818347427e save status display
3 months ago
yova 9f02934faf confirm push to ghost
4 months ago
yova da174106ee show delete icon again after push
4 months ago
yova 2f777055ea some error handling
4 months ago
yova 67050857e3 open/new alert dialog unCreate
4 months ago
yova a8d710c22a call onRead one less
4 months ago
yova 79d7855973 wait till editor's loaded before start
4 months ago
yova 482c3e9756 toggle themes
4 months ago
yova 96c2fefd0d better read
4 months ago
yova 2b816a923d run openInputstream in IO context
4 months ago
yova da60d7395e remove save file toast
4 months ago
yova 182122babf revisit Actionbar and File IO
4 months ago
yova 4c807243eb add note about experimental status
4 months ago
yova 32de26229d improve delete
4 months ago
yova cddee77017 rename to hauntED
4 months ago
yova af2814b3e0 improve acceptance of intent data
4 months ago
yova 65a5f9976f clean intent from extra data after usage
4 months ago
yova c227792cfb some more error handling
6 months ago
yova d306b1b50f consolidate saveAs
6 months ago
yova 407939f8b0 allow more images
6 months ago
yova 5590bd6d51 support some sharing
6 months ago
yova 9e94cdfbff hauntED release 0.23
6 months ago
yova 634cc6fcb1 paste instead of append images
6 months ago
yova 4bec414930 metadata activity
6 months ago
yova 997c56913e hauntED release 0.22
6 months ago
yova c4c9a40eff sort threads
6 months ago
yova 65c02a6060 make it work
6 months ago
yova 1f4d6ef77d UPDATE post
7 months ago
yova 307eb5eddf basic DELETE functionality
7 months ago
yova 0b80315c88 release hauntED 0.12
7 months ago
yova 92fae0f9bf smoothen usage of fileprovider
7 months ago
yova 9c00c828b4 cleanup credential management. No PW stored locally.
7 months ago
yova b759ce94f3 use persistent cookiejar
7 months ago
yova 4884967244 allow nextcloud access
7 months ago
yova 89796405ad emptyMdToAppend after use
7 months ago
yova e8aee0faea release hauntED POC
7 months ago
yova 6a3b6a970b open browser on upload
7 months ago
yova ef4d697e98 ui smoothed
7 months ago
yova 5d4b42e396 hauntED - ghost interface POC
7 months ago

@ -1,17 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_5_API_30_2.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-10-14T19:25:23.306847449Z" />
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

@ -4,16 +4,16 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$USER_HOME$/gradle" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

@ -1,28 +1,49 @@
# Markdown Editor for Android
![](screenshots/Screenshot1.png)
![](screenshots/Screenshot2.png)
![](screenshots/Screenshot3.png)
# MDEditor 🚀
A simple markdown editor for android. Built with [easy markdown editor](https://github.com/Ionaru/easy-markdown-editor), codemirror5 and marked on webview.
---
url: https://mary.joefix.it/haunted-intro/
title: hauntED Readme
feature_image: https://mary.joefix.it/content/images/size/w1200/2023/11/IMG_20231105_132234.jpg
author: yova@freedomhost.de
---
# hauntED 🚀
A simple markdown editor for ghost on android. Built with [simple-markdown-editor](https://github.com/Ionaru/easy-markdown-editor), [codemirror5](https://github.com/codemirror/codemirror5) and [marked](https://marked.js.org/). Get apk [here](https://git.gugelfrei.de/android-fun/mdeditor/src/branch/hauntED/app/release/app-release.apk).
This very early stage experimental software. Use at your own risk.
## Features 💪
- elegant and stressless offline writing
- preview of rendered text
- *stable* load/save files
- **themes**
- directly push to [ghost CMS](https://ghost.org/). example [here](https://mary.joefix.it/)
- push images to ghost and include as markdown
- send links from other apps to include as markdown syntax
- dark mode
- a lot of md functionality, like [links](https://git.gugelfrei.de)
- perfect for spantaneous content creation right from your hammock. 🏞️
- perfect for spontaneous content creation right from your hammock. 🏞️
## Recommendations
as this uses webview, that is the chromium rendering engine adapted as android service, in respect to your privacy, it is advised to install an edited version like bromite or ungoogled webview. Most simple is to just buy a prepared fone [from me](https://gugelfrei.de).
made with ❤️ by yv@wntr.org
made with ❤️ by yv@wntr.org ©️ 2024
![Screenshot_20240201-200704_MDEditor.png](https://mary.joefix.it/content/images/2024/02/Screenshot_20240201-200704_MDEditor.png)
![Screenshot_20240201-200737_MDEditor.png](https://mary.joefix.it/content/images/2024/02/Screenshot_20240201-200737_MDEditor.png)
![Screenshot_20240201-201115_MDEditor.png](https://mary.joefix.it/content/images/2024/02/Screenshot_20240201-201115_MDEditor.png)

@ -9,10 +9,10 @@ android {
defaultConfig {
applicationId "org.wntr.mdeditor"
minSdk 23
minSdk 26
targetSdk 33
versionCode 1
versionName "0.1.1"
versionName "hauntED 0.23"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -35,6 +35,7 @@ android {
}
buildFeatures {
compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
@ -47,6 +48,11 @@ android {
}
dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
implementation 'com.squareup.retrofit2:retrofit:<VERSION>'
implementation 'com.squareup.retrofit2:converter-jackson:2.9.0'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.11.2'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.8.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
@ -58,7 +64,12 @@ dependencies {
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.webkit:webkit:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'

Binary file not shown.

@ -12,7 +12,7 @@
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "0.1.1",
"versionName": "hauntED 0.23",
"outputFile": "app-release.apk"
}
],

@ -2,8 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<queries>
<intent>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent>
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -14,24 +22,39 @@
android:supportsRtl="true"
android:theme="@style/Theme.MDEditor"
tools:targetApi="31">
<activity
android:name=".MetadataActivity"
android:exported="false"
android:label="Metadata"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".LoginActivity"
android:exported="false"
android:label="ghost CMS login"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MDEditor">
android:theme="@style/Theme.MDEditor"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter
android:scheme="http" tools:ignore="AppLinkUrlError">
android:scheme="http"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
@ -42,8 +65,7 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

@ -0,0 +1,33 @@
package org.wntr.mdeditor
import android.content.Context
class CredentialManager(applicationContext: Context) {
val sharedPrefs= applicationContext.getSharedPreferences("prefs", Context.MODE_PRIVATE)
var instance = "nowhere"
get() =
if (field.equals("nowhere")) loadCredentialsFromSharedPrefs().instance
else field
var username = "nobody"
get() =
if (field.equals("nobody")) loadCredentialsFromSharedPrefs().username
else field
fun saveCredentialsToSharedPrefs(creds: Credentials) {
val (instance, username) = creds
sharedPrefs.edit().apply {
putString("instance", instance)
putString("username", username)
apply()
}
}
fun loadCredentialsFromSharedPrefs() : Credentials {
val instance = sharedPrefs.getString("instance", "nowhere")!!
val username = sharedPrefs.getString("username", "nobody")!!
return (Credentials(instance!!, username!!, "nothing"))
}
}

@ -0,0 +1,161 @@
package org.wntr.mdeditor
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.wntr.mdeditor.MainActivity.Companion.ghostConnection
import org.wntr.mdeditor.databinding.ActivityLoginBinding
import org.wntr.mdeditor.R
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val creds = MainActivity.credManager.loadCredentialsFromSharedPrefs()
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
if (creds.instance != "nowhere") binding.instance.setText(creds.instance)
if (creds.username != "nobody") binding.username.setText(creds.username)
}
fun onButtonLoginClick(view: View) {
Log.d(javaClass.simpleName,"entered: instance: ${binding.instance.text} username:${binding.username.text}")
val creds= Credentials(binding.instance.text.toString(), binding.username.text.toString(), binding.password.text.toString())
registerGhost(creds)
}
fun registerGhost(credentials: Credentials){
if (Uri.parse(credentials.instance).scheme == null) {
Toast.makeText(
this@LoginActivity,
"Scheme wrong. Add https://",
Toast.LENGTH_SHORT
).show()
MainActivity.credManager.saveCredentialsToSharedPrefs(
Credentials(
"nowhere",
credentials.username
)
)
return
}
MainActivity.api = ghostAPI(applicationContext, credentials.instance)
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
var result =""
try {
result = MainActivity.api.register(credentials)
} catch (e: ExceptionInInitializerError) {
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"Probably host name wrong",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName, "Probably host name wrong")
MainActivity.credManager.saveCredentialsToSharedPrefs(
Credentials(
"nowhere",
credentials.username,
"nothing"
)
)
finishAffinity()
} catch (e: NoClassDefFoundError) {
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"API client needs restart for setting hostname",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName, "API client needs restart for setting hostname")
finishAffinity()
}
when (result) {
"PASSWORD_INCORRECT" -> {
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"Password Incorrect",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName, "Password Incorrect")
MainActivity.credManager.saveCredentialsToSharedPrefs(
Credentials(
credentials.instance,
credentials.username,
"nothing"
)
)
}
"404" -> {
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"Username nonexistant",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName,"Username nonexistant")
MainActivity.credManager.saveCredentialsToSharedPrefs(
Credentials(
credentials.instance,
"nobody",
"nothing"
)
)
}
"SUCCESS" -> {
MainActivity.credManager.saveCredentialsToSharedPrefs(credentials)
ghostConnection = true
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"Successfully logged in!",
Toast.LENGTH_SHORT
).show()
})
finish()
}
else -> {
this@LoginActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@LoginActivity,
"Credentials wrong",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName, "Credentials wrong")
MainActivity.credManager.saveCredentialsToSharedPrefs(
Credentials(
"nowhere",
"nobody",
"nothing"
)
)
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,41 @@
package org.wntr.mdeditor
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import org.wntr.mdeditor.MainActivity.Companion.credManager
import org.wntr.mdeditor.databinding.ActivityMetadataBinding
class MetadataActivity : AppCompatActivity() {
private lateinit var binding: ActivityMetadataBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMetadataBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.title.setText(metaData.get("title") ?: getDisplayName(applicationContext, thisFileUri))
binding.url.setText(metaData.get("url"))
binding.featureImage.setText(metaData.get("feature_image"))
binding.author.setText(metaData.get("author") ?: credManager.username)
}
fun onButtonSaveClick(view: View) {
Log.d(javaClass.simpleName, "Getting Metadata:\ntitle:\t${binding.title.text}\nfeature_image:\t${binding.featureImage.text}")
metaData.put("title", binding.title.text.toString())
metaData.put("feature_image", binding.featureImage.text.toString())
if (binding.author.text.toString() != MainActivity.credManager.username) {
var apiHost: String? = null
if (metaData.get("url") !== null) {
val url = Uri.parse(metaData.get("url"))
apiHost = url.scheme + "://" + url.host
} else apiHost = MainActivity.credManager.instance
credManager.saveCredentialsToSharedPrefs(Credentials(apiHost, binding.author.text.toString()))
}
metaData.put("author", binding.author.text.toString())
MainActivity.readOnResume = false
finish()
}
}

@ -0,0 +1,145 @@
package org.wntr.mdeditor
import android.content.Context
import android.util.Log
import android.widget.Toast
import com.franmontiel.persistentcookiejar.ClearableCookieJar
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import org.json.JSONObject
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
object RetrofitClient {
fun getClient(applicationContext: Context, baseUrl: String): Retrofit {
/* Detailed logging
val loggingInterceptor = HttpLoggingInterceptor().apply {
this.setLevel(HttpLoggingInterceptor.Level.BASIC)
}*/
Log.i(javaClass.simpleName, "Retrofitclient baseurl: $baseUrl")
for (cookie in SharedPrefsCookiePersistor(applicationContext).loadAll()) {
Log.d(javaClass.simpleName, "cookie for: ${cookie.domain}")
}
val cookieJar: ClearableCookieJar =
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
val okHttpClient = OkHttpClient()
.newBuilder()
/*
.addInterceptor(RequestInterceptor)
*/
/*
.addInterceptor(loggingInterceptor)
*/
.cookieJar(cookieJar)
.build()
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl + "/ghost/api/admin/")
.addConverterFactory(JacksonConverterFactory.create())
.build()
}
}
class ghostAPI(applicationContext: Context, instance: String) {
val retrofit = RetrofitClient.getClient(applicationContext, instance)
val postApi = retrofit.create(PostApi::class.java)
val baseUrl = instance
fun register(credentials: Credentials): String {
return try {
val response = postApi.getCookie(credentials).execute()
if (!response.isSuccessful) {
Log.d(javaClass.simpleName, "Response code ${response.code()}")
when (response.code()) {
401 -> {
return "404"
}
404 -> {
return "404"
}
429 -> {
Log.d(javaClass.simpleName, "Too many failures")
return "TOO_MANY_FAILURES"
}
else -> {
val errors = JSONObject(response.errorBody()!!.string())
val code =
errors.getJSONArray("errors").getJSONObject(0).getString("code")
Log.d(javaClass.simpleName, "Error code $code")
if (code.equals("PASSWORD_INCORRECT")) {
Log.d(javaClass.simpleName, "Password incorrect")
}
if (code !== null) return code
else return response.code().toString()
}
}
}
return "SUCCESS"
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't log in. Exception: ${ex}")
return "EXCEPTION"
}
}
/* Detailed logging
object RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = request.newBuilder()
*//*.header("Authorization", "Ghost "+ API_KEY)*//*
.header("Origin", "https://mary.joefix.it")
.build()
Log.i(javaClass.simpleName, "Outgoing request to ${newRequest.url}")
//Log.i(javaClass.simpleName, newRequest.body.toString())
return chain.proceed(newRequest)
}
}
*/
}
interface PostApi {
@GET("posts/")
fun getPosts(): Call<ResponseBody>
/* fun getPosts(): Call<ResponseBody> <- for json string*/
@DELETE("posts/{id}")
fun deletePost(@Path("id") id: String): Call<ResponseBody>
@POST("session")
fun getCookie(@Body credentials: Credentials): Call<ResponseBody>
@POST("posts/?source=html")
fun pushPost(@Body postings: sendPostList): Call<ResponseBody>
@PUT("posts/{id}?source=html")
fun updatePost(@Path("id") id: String, @Body postings:sendPostList): Call<ResponseBody>
@Multipart
@POST("images/upload")
fun pushMyImage(@Part file: MultipartBody.Part): Call<imagesObj>
}

@ -0,0 +1,187 @@
package org.wntr.mdeditor
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
var thisFileUri: Uri? = null
var metaData = mdMeta()
fun checkURIResource(context: Context, uri: Uri?): Boolean {
val cursor = context.contentResolver.query(uri!!, null, null, null, null)
val doesExist = cursor != null && cursor.moveToFirst()
cursor?.close()
return doesExist
}
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 img tmp file", ex.message!!)
ex.printStackTrace()
}
}
fun createHtmlFile(context: Context, htmlString: String): File? {
val TAG="share HTML"
lateinit var htmlFile : File
try {
val storageDir = File(context.cacheDir, "html")
storageDir.mkdir()
htmlFile = File(storageDir.path + "/${getDisplayName(context,thisFileUri)
.split(".")[0]}.html")
if (htmlFile.exists()) htmlFile.delete()
htmlFile.createNewFile()
} catch (e: Exception) {
Log.d(TAG, "Problem with accessing file\n${e.stackTraceToString()}")
Toast.makeText(
context,
"Problem with accessing file\n$e",
Toast.LENGTH_LONG
).show()
return null
}
try {
FileOutputStream(htmlFile).use {
it.write(htmlString.toByteArray())
}
} catch (e: Exception) {
Toast.makeText(
context,
"Error during writing.\n$e",
Toast.LENGTH_LONG
).show()
return null
}
Log.d(TAG, "File saved: ${htmlFile.toURI()}")
Toast.makeText(
context,
"HTML output produced.",
Toast.LENGTH_LONG
).show()
return htmlFile
}
fun deleteCache(context: Context) {
try {
val dir = File(context.cacheDir, "html")
deleteDir(dir)
deleteDir(context.filesDir)
} 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
}
}
@SuppressLint("Range")
fun getDisplayName(context: Context, 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 = context.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;
}
@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 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 saveMetaToSharedPrefs(context: Context) {
if (thisFileUri == null) return
Log.d("saveMetaToSharedPrefs", "saving to shared prefs cursor: ${metaData.cursor} in file: ${thisFileUri}")
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit().apply {
putString("lastFile", thisFileUri.toString())
putString("cursor", metaData.cursor)
apply()
}
}
fun loadMetaFromSharedPrefs(context: Context): Boolean{
val prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val uriString = prefs.getString("lastFile", "noLastFile")
if (uriString == "noLastFile") {
Log.i("loadMetaFromSharedPrefs","No lastfile saved")
return false
} else {
thisFileUri = Uri.parse(uriString)
metaData.cursor = prefs.getString("cursor", "nocursor") ?: "{ line: 0, ch: 0, sticky: null }"
Log.i("loadMetaFromSharedPrefs","Loaded cursor: ${metaData.cursor}")
return true
}
}

@ -0,0 +1,98 @@
package org.wntr.mdeditor
import android.util.Log
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.json.JSONObject
class mdMeta {
var metaData = mutableMapOf<String,String>()
var ID: String? = null
var updatedAt: String? = null
var cursor: String = "{ line: 0, ch: 0, sticky: null }"
get() =
if (field.equals("null")) "{ line: 0, ch: 0, sticky: null }"
else field
fun put(key: String, value: String) {
metaData.put(key, value)
}
fun get(key: String): String?{
return metaData.get(key)
}
override fun toString(): String {
if (metaData.size == 0) return ""
var metaString = "---\n"
for (key in metaData.keys) {
if (metaData.get(key) != null) metaString += "$key: ${metaData.get(key)}\n"
}
metaString += "---\n"
return metaString
}
fun extractMetadataFromMarkdown(markdown: String) : String {
val charactersBetweenGroupedHyphens = Regex("^---([\\s\\S]*?)---\n")
val metadataMatched = charactersBetweenGroupedHyphens.find(markdown)
ID = null
if (metadataMatched == null || metadataMatched.value == "---\n---\n") {
Log.d(javaClass.simpleName,"No metadata included")
metaData = mutableMapOf()
return markdown
}
val metadata = metadataMatched.value
val mapper = ObjectMapper(YAMLFactory())
mapper.registerModule(KotlinModule())
metaData = mapper.readValue(metadata, Map::class.java) as MutableMap<String,String>
for (entry in metaData) {
Log.d(javaClass.simpleName,"${entry.key}: ${entry.value}")
}
return markdown.substring(metadata.length)
}
fun getId(): String? {
if (ID !== null)
return ID as String
if (metaData.get("url") == "") return null
try {
val response = MainActivity.api.postApi.getPosts().execute()
Log.d(javaClass.simpleName, "result: ${response.code()}")
if (response.isSuccessful) {
val resp = JSONObject(response.body()!!.string())
val posts = resp.getJSONArray("posts")
var remoteDoc : JSONObject? = null
for (i in 0 until posts.length()) {
remoteDoc = posts.getJSONObject(i)
if (remoteDoc.getString("url") == metaData.get("url")) break
}
if (remoteDoc == null ){
Log.d(javaClass.simpleName, "Could not find post ID ${metaData.get("url")}. Already deleted?")
metaData.put("url", "")
ID = null
return ID
}
ID = remoteDoc.getString("id")
updatedAt = remoteDoc.getString("updated_at")
Log.d(javaClass.simpleName, "Document is online at ${
metaData.get("url")}\nID: ${ID}")
return ID
} else {
return null
}
} catch(e: Exception) {
Log.d(javaClass.simpleName, "Couldn't get post list: $e")
return null
}
}
}

@ -0,0 +1,36 @@
package org.wntr.mdeditor
import com.fasterxml.jackson.annotation.JsonProperty
data class Credentials (
@JsonProperty("instance") val instance: String = "nowhere",
@JsonProperty("username") val username: String = "nobody",
@JsonProperty("password") val password: String = "nothing"
)
data class sendPost (
@JsonProperty("title") val title: String,
@JsonProperty("updated_at") val updated_at: String,
@JsonProperty("authors") val authors: List<String>,
@JsonProperty("html") val html: String,
@JsonProperty("feature_image") val feature_image: String?,
@JsonProperty("status") val status: String = "published"
)
data class sendPostList (
@JsonProperty("posts") val posts: List<sendPost>
)
data class imagesObj(
@JsonProperty("images") val images: List<imageObj>
)
data class imageObj(
@JsonProperty("url") val url: String,
@JsonProperty("ref") val ref: String?
)
data class MetaData(
val url: String?
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:layout_alignParentBottom="true"
android:fitsSystemWindows="true"
tools:context="LoginActivity">
<EditText
android:id="@+id/instance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Ghost instance"
android:inputType="textUri"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/username"/>
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/instance"
app:layout_constraintBottom_toTopOf="@id/password"/>
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textVisiblePassword"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username"
app:layout_constraintBottom_toTopOf="@id/login"/>
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/action_sign_in"
android:onClick="onButtonLoginClick"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,9 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mde_webview"
tools:context=".MainActivity">
</WebView>
android:layout_alignParentBottom="true"
android:fitsSystemWindows="true">
<WebView
android:id="@+id/mde_webview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constrainedHeight="true"
tools:context=".MainActivity"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:layout_alignParentBottom="true"
android:fitsSystemWindows="true"
tools:context="MetadataActivity">
<TextView
android:id="@+id/textView0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Title:"
app:layout_constraintEnd_toEndOf="@+id/barrier2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"/>
<EditText
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="text"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/author"/>
<TextView
android:id="@+id/textView1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Author email:"
app:layout_constraintBottom_toBottomOf="@+id/author"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier2"
app:layout_constraintTop_toTopOf="@+id/author" />
<EditText
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autofillHints="author email"
android:inputType="text"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier2"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toTopOf="@id/feature_image"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Feature Image:"
app:layout_constraintBottom_toBottomOf="@+id/feature_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier2"
app:layout_constraintTop_toTopOf="@+id/feature_image" />
<EditText
android:id="@+id/feature_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autofillHints="drop img URL"
android:inputType="text"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier2"
app:layout_constraintTop_toBottomOf="@id/author"
app:layout_constraintBottom_toTopOf="@id/url"/>
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="URL:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/barrier2"
app:layout_constraintTop_toTopOf="@id/url"
app:layout_constraintBottom_toBottomOf="@id/url" />
<TextView
android:id="@+id/url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autoLink="web"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier2"
app:layout_constraintTop_toBottomOf="@id/feature_image"
app:layout_constraintBottom_toTopOf="@id/login"/>
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onButtonSaveClick"
android:text="Save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/url" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right"
app:constraint_referenced_ids="textView0,textView1,textView2,textView3" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,39 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/new_file"
android:title="New File"
android:icon="@android:drawable/ic_menu_add"
app:showAsAction="always" />
<item android:id="@+id/open_file"
android:title="Open File"
android:icon="@drawable/ic_menu_archive"
app:showAsAction="always" />
<item android:id="@+id/save_file"
android:title="Save File"
android:icon="@android:drawable/ic_menu_save"
app:showAsAction="always" />
<item android:id="@+id/copyToClipboard"
android:title="Copy to clipboard"
android:icon="@drawable/ic_menu_copy"
app:showAsAction="always" />
<item android:id="@+id/push_ghost"
android:title="Push to ghost"
android:icon="@android:drawable/ic_menu_upload"
app:showAsAction="always" />
<item android:id="@+id/delete_ghost"
android:title="Delete on ghost"
android:icon="@android:drawable/ic_menu_delete"
app:showAsAction="ifRoom" />
<item android:id="@+id/settings"
android:title="Ghost CMS Login"
android:icon="@android:drawable/ic_menu_preferences"
app:showAsAction="ifRoom" />
<item android:id="@+id/image"
android:title="Image"
android:icon="@android:drawable/ic_menu_gallery"
app:showAsAction="always"/>
<item android:id="@+id/metadata"
android:title="Metadata"
android:icon="@android:drawable/ic_menu_more"
app:showAsAction="ifRoom"/>
</menu>

@ -1,39 +1,14 @@
function saveAs() {
Android.triggerNewBuffer(easyMDE.value(), false)
}
function blankBuffer() {
saveFile()
Android.triggerNewBuffer(easyMDE.value(), true)
}
function saveFile() {
if (!easyMDE.codemirror.doc.isClean()) Android.triggerSaveFile(easyMDE.value())
easyMDE.codemirror.doc.markClean()
}
function onRead() {
easyMDE.codemirror.doc.setValue(Android.getValue())
easyMDE.codemirror.doc.markClean()
}
function openFile() {
if (!easyMDE.codemirror.doc.isClean()) saveFile()
Android.triggerOpenFile()
onRead()
easyMDE.codemirror.doc.markClean()
}
function dispatchCut() {
console.log("dispatch cut")
easyMDE.codemirror.getTextArea().dispatchEvent(new Event("cut"))
}
function getValue() {
return easyMDE.value()
return encodeURIComponent(easyMDE.value())
}
function myPreview() {
saveFile()
easyMDE.togglePreview()
}
function refresh() {
Android.refresh()
onRead()
}
function displayName() {
if (typeof Android !== 'undefined') return Android.triggerDisplayName()
else return "NONdroid"
@ -42,102 +17,43 @@ function shareText() {
Android.triggerShare(easyMDE.markdown(easyMDE.codemirror.doc.getValue()))
}
const easyMDE = new EasyMDE({
spellChecker: false,
nativeSpellcheck: false,
maxHeight: "550px",
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: "more",
className: "fa-solid fa-angles-down",
title: "more",
children: [
{
name: "saveAs",
action: saveAs,
className: "fa fa-star",
title: "saveAs"
},
{
name: "new",
action: blankBuffer,
className: "fa fa-file",
title: "New"
},
/* {
name: "refresh",
action: refresh,
className: "fa fa-refresh",
title: "Refresh"
},*/
{
name: "day",
action: () => easyMDE.codemirror.setOption("theme","solarized"),
className: "fa fa-sun",
title: "Day Theme"
},
{
name: "night",
action: () => easyMDE.codemirror.setOption("theme","3024-night"),
className: "fa fa-moon",
title: "Night Theme"
},
"guide"
]
},
{
name: "save",
action: saveFile,
className: "fa fa-save",
title: "Save"
},
{
name: "open",
action: openFile,
className: "fa-regular fa-folder-open",
title: "Open"
},
{
name: "share",
action: shareText,
className: "fa fa-share-nodes",
title: "Share"
},
"link","undo",
{
name: "preview",
action: myPreview,
className: "fa fa-eye",
title: "Preview",
noDisable: true
},"redo",
"bold", "italic","strikethrough","code"
]
});
onRead()
function getHtml() {
return encodeURIComponent(easyMDE.markdown(easyMDE.codemirror.doc.getValue()))
}
function appendText() {
if ((appendix = Android.getMdToAppend()) !== "") {
cursor = easyMDE.codemirror.doc.getCursor()
easyMDE.codemirror.doc.setValue(easyMDE.codemirror.doc.getValue() + appendix)
easyMDE.codemirror.doc.setCursor(cursor)
}
}
function pasteText() {
data = new DataTransfer()
data.setData("text/plain", Android.getMdToAppend())
event = new Event("paste")
event.clipboardData = data
easyMDE.codemirror.focus()
document.getElementsByClassName("CodeMirror-scroll")[0].dispatchEvent(event);
}
windowHeight = window.innerHeight
function toggleBar() {
Android.toggleBar()
easyMDE.codemirror.focus()
}
themes = [ "solarized", "3024-night"]
i=1
function toggleTheme() {
easyMDE.codemirror.setOption("theme",themes[i])
i++
if (i>themes.length-1) i=0
}
function saveStatus() {
if (typeof easyMDE === 'undefined') return ''
else if (easyMDE.codemirror.doc.isClean()) return '*'
return ''
}

@ -38,3 +38,6 @@ body {
background: none;
}
img {
max-width: 100vw;
}

@ -1,3 +1,12 @@
<resources>
<string name="app_name">MDEditor</string>
<string name="title_activity_login">LoginActivity</string>
<string name="prompt_email">Email</string>
<string name="prompt_password">Password</string>
<string name="action_sign_in">Sign in</string>
<string name="action_sign_in_short">Sign in</string>
<string name="welcome">"Welcome !"</string>
<string name="invalid_username">Not a valid username</string>
<string name="invalid_password">Password must be >5 characters</string>
<string name="login_failed">"Login failed"</string>
</resources>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MDEditor" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
<style name="Theme.MDEditor" parent="Theme.MaterialComponents.DayNight" />
</resources>

@ -10,6 +10,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "MDEditor"

Loading…
Cancel
Save