Compare commits
61 Commits
Author | SHA1 | Date |
---|---|---|
yova | f9b2f87b8f | 2 weeks ago |
yova | 2bc5d4b7bc | 2 weeks ago |
yova | b30243916f | 2 weeks ago |
yova | f6dd99e59a | 2 months ago |
yova | 65107d07dd | 3 months ago |
yova | c4828bdda5 | 3 months ago |
yova | b9ffe73652 | 3 months ago |
yova | 3b0b00d89c | 3 months ago |
yova | d14d96f3a0 | 3 months ago |
yova | 09227c2712 | 3 months ago |
yova | 8e9e09ba32 | 3 months ago |
yova | 37d82663b2 | 3 months ago |
yova | 7b54072fb5 | 3 months ago |
yova | 7c087562df | 3 months ago |
yova | 1c4ebfca18 | 3 months ago |
yova | ed94b85006 | 3 months ago |
yova | 4997dd067b | 3 months ago |
yova | f0d90c175f | 3 months ago |
yova | 1583c0009b | 3 months ago |
yova | 3a7409ee6a | 3 months ago |
yova | 36b9d7bd0b | 3 months ago |
yova | 821ad615ee | 3 months ago |
yova | 818347427e | 3 months ago |
yova | 9f02934faf | 4 months ago |
yova | da174106ee | 4 months ago |
yova | 2f777055ea | 4 months ago |
yova | 67050857e3 | 4 months ago |
yova | a8d710c22a | 4 months ago |
yova | 79d7855973 | 4 months ago |
yova | 482c3e9756 | 4 months ago |
yova | 96c2fefd0d | 4 months ago |
yova | 2b816a923d | 4 months ago |
yova | da60d7395e | 4 months ago |
yova | 182122babf | 4 months ago |
yova | 4c807243eb | 4 months ago |
yova | 32de26229d | 4 months ago |
yova | cddee77017 | 4 months ago |
yova | af2814b3e0 | 4 months ago |
yova | 65a5f9976f | 4 months ago |
yova | c227792cfb | 6 months ago |
yova | d306b1b50f | 6 months ago |
yova | 407939f8b0 | 6 months ago |
yova | 5590bd6d51 | 6 months ago |
yova | 9e94cdfbff | 6 months ago |
yova | 634cc6fcb1 | 6 months ago |
yova | 4bec414930 | 6 months ago |
yova | 997c56913e | 6 months ago |
yova | c4c9a40eff | 6 months ago |
yova | 65c02a6060 | 6 months ago |
yova | 1f4d6ef77d | 7 months ago |
yova | 307eb5eddf | 7 months ago |
yova | 0b80315c88 | 7 months ago |
yova | 92fae0f9bf | 7 months ago |
yova | 9c00c828b4 | 7 months ago |
yova | b759ce94f3 | 7 months ago |
yova | 4884967244 | 7 months ago |
yova | 89796405ad | 7 months ago |
yova | e8aee0faea | 7 months ago |
yova | 6a3b6a970b | 7 months ago |
yova | ef4d697e98 | 7 months ago |
yova | 5d4b42e396 | 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>
|
@ -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,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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
)
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 961 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 831 B |
After Width: | Height: | Size: 758 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
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,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>
|