[Lab] Add JSON editor.

This commit is contained in:
Kuba Szczodrzyński 2021-02-26 17:27:17 +01:00
parent 481af64137
commit e34e4d6906
8 changed files with 248 additions and 39 deletions

View File

@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonArrayViewHolder
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonElementViewHolder
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonObjectViewHolder
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonSubObjectViewHolder
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import kotlin.coroutines.CoroutineContext
@ -35,14 +36,20 @@ class LabJsonAdapter(
var onJsonElementClick: ((item: LabJsonElement) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope {
companion object {
private const val TAG = "AttendanceAdapter"
private const val TAG = "LabJsonAdapter"
private const val ITEM_TYPE_OBJECT = 0
private const val ITEM_TYPE_ARRAY = 1
private const val ITEM_TYPE_ELEMENT = 2
private const val ITEM_TYPE_SUB_OBJECT = 1
private const val ITEM_TYPE_ARRAY = 2
private const val ITEM_TYPE_ELEMENT = 3
const val STATE_CLOSED = 0
const val STATE_OPENED = 1
fun expand(item: Any, level: Int): MutableList<Any> {
val path = when (item) {
is LabJsonObject -> item.key + ":"
is LabJsonArray -> item.key + ":"
else -> ""
}
val json = when (item) {
is LabJsonObject -> item.jsonObject
is LabJsonArray -> item.jsonArray
@ -53,17 +60,16 @@ class LabJsonAdapter(
}
return when (json) {
is JsonObject -> json.entrySet().mapNotNull { wrap(it.key, it.value, level) }
is JsonArray -> json.mapIndexedNotNull { index, jsonElement -> wrap(index.toString(), jsonElement, level) }
else -> listOf(LabJsonElement("?", json, level))
is JsonObject -> json.entrySet().mapNotNull { wrap(path + it.key, it.value, level) }
is JsonArray -> json.mapIndexedNotNull { index, jsonElement -> wrap(path + index.toString(), jsonElement, level) }
else -> listOf(LabJsonElement("$path?", json, level))
}.toMutableList()
}
fun wrap(key: String, item: JsonElement, level: Int = 0): Any? {
fun wrap(key: String, item: JsonElement, level: Int = 0): Any {
return when (item) {
is JsonObject -> LabJsonObject(key, item, level + 1)
is JsonArray -> LabJsonArray(key, item, level + 1)
is JsonElement -> LabJsonElement(key, item, level + 1)
else -> null
else -> LabJsonElement(key, item, level + 1)
}
}
}
@ -80,6 +86,7 @@ class LabJsonAdapter(
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ITEM_TYPE_OBJECT -> JsonObjectViewHolder(inflater, parent)
ITEM_TYPE_SUB_OBJECT -> JsonSubObjectViewHolder(inflater, parent)
ITEM_TYPE_ARRAY -> JsonArrayViewHolder(inflater, parent)
ITEM_TYPE_ELEMENT -> JsonElementViewHolder(inflater, parent)
else -> throw IllegalArgumentException("Incorrect viewType")
@ -87,8 +94,10 @@ class LabJsonAdapter(
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is LabJsonObject -> ITEM_TYPE_OBJECT
return when (val item = items[position]) {
is LabJsonObject ->
if (item.level == 1) ITEM_TYPE_OBJECT
else ITEM_TYPE_SUB_OBJECT
is LabJsonArray -> ITEM_TYPE_ARRAY
is LabJsonElement -> ITEM_TYPE_ELEMENT
else -> throw IllegalArgumentException("Incorrect viewType")
@ -118,7 +127,7 @@ class LabJsonAdapter(
View.ROTATION,
if (model.state == STATE_CLOSED) 0f else 180f,
if (model.state == STATE_CLOSED) 180f else 0f
).setDuration(200).start();
).setDuration(200).start()
}
// hide the preview, show summary
@ -143,7 +152,9 @@ class LabJsonAdapter(
var end: Int = items.size
for (i in start until items.size) {
val model1 = items[i]
val level = (model1 as? ExpandableItemModel<*>)?.level ?: 3
val level = (model1 as? ExpandableItemModel<*>)?.level
?: (model1 as? LabJsonElement)?.level
?: model.level
if (level <= model.level) {
end = i
break
@ -170,6 +181,7 @@ class LabJsonAdapter(
val viewType = when (holder) {
is JsonObjectViewHolder -> ITEM_TYPE_OBJECT
is JsonSubObjectViewHolder -> ITEM_TYPE_SUB_OBJECT
is JsonArrayViewHolder -> ITEM_TYPE_ARRAY
is JsonElementViewHolder -> ITEM_TYPE_ELEMENT
else -> throw IllegalArgumentException("Incorrect viewType")
@ -180,6 +192,7 @@ class LabJsonAdapter(
when {
holder is JsonObjectViewHolder && item is LabJsonObject -> holder.onBind(activity, app, item, position, this)
holder is JsonSubObjectViewHolder && item is LabJsonObject -> holder.onBind(activity, app, item, position, this)
holder is JsonArrayViewHolder && item is LabJsonArray -> holder.onBind(activity, app, item, position, this)
holder is JsonElementViewHolder && item is LabJsonElement -> holder.onBind(activity, app, item, position, this)
}

View File

@ -10,16 +10,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.afollestad.materialdialogs.MaterialDialog
import com.google.gson.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
@ -38,6 +36,10 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
get() = job + Dispatchers.Main
// local/private variables go here
private lateinit var adapter: LabJsonAdapter
private val loginStore by lazy {
app.db.loginStoreDao().getByIdNow(app.profile.loginStoreId)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
@ -48,22 +50,96 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
}
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
val adapter = LabJsonAdapter(activity)
val json = JsonObject().also { json ->
json.add("app.profile", app.profile.studentData)
json.add("app.config", JsonParser().parse(app.gson.toJson(app.config.values)))
EdziennikTask.profile?.let {
json.add("API.profile", it.studentData)
} ?: {
json.addProperty("API.profile", "null")
}()
EdziennikTask.loginStore?.let {
json.add("API.loginStore", it.data)
} ?: {
json.addProperty("API.loginStore", "null")
}()
}
adapter.items = LabJsonAdapter.expand(json, 0)
adapter = LabJsonAdapter(activity, onJsonElementClick = { item ->
try {
var parent: Any = Unit
var obj: Any = Unit
var objName: String = ""
item.key.split(":").forEach { el ->
parent = obj
obj = when (el) {
"App.profile" -> app.profile
"App.profile.studentData" -> app.profile.studentData
"App.profile.loginStore" -> loginStore?.data ?: JsonObject()
"App.config" -> app.config.values
else -> when (obj) {
is JsonObject -> (obj as JsonObject).get(el)
is JsonArray -> (obj as JsonArray).get(el.toInt())
is HashMap<*, *> -> (obj as HashMap<String, String?>)[el].toString()
else -> {
val field = obj::class.java.getDeclaredField(el)
field.isAccessible = true
field.get(obj) ?: return@forEach
}
}
}
objName = el
}
val objVal = obj
val value = when (objVal) {
is JsonPrimitive -> when {
objVal.isString -> objVal.asString
objVal.isNumber -> objVal.asNumber.toString()
objVal.isBoolean -> objVal.asBoolean.toString()
else -> objVal.asString
}
else -> objVal.toString()
}
MaterialDialog.Builder(activity)
.input("value", value, false) { _, input ->
val input = input.toString()
when (parent) {
is JsonObject -> {
val v = objVal as JsonPrimitive
when {
v.isString -> (parent as JsonObject)[objName] = input
v.isNumber -> (parent as JsonObject)[objName] = input.toLong()
v.isBoolean -> (parent as JsonObject)[objName] = input.toBoolean()
}
}
is JsonArray -> {
}
is HashMap<*, *> -> app.config.set(objName, input)
else -> {
val field = parent::class.java.getDeclaredField(objName)
field.isAccessible = true
val newVal = when (objVal) {
is Int -> input.toInt()
is Boolean -> input.toBoolean()
is Float -> input.toFloat()
is Char -> input.toCharArray()[0]
is String -> input
is Long -> input.toLong()
is Double -> input.toDouble()
else -> input
}
field.set(parent, newVal)
}
}
when (item.key.substringBefore(":")) {
"App.profile" -> app.profileSave()
"App.profile.studentData" -> app.profileSave()
"App.profile.loginStore" -> app.db.loginStoreDao().add(loginStore)
}
showJson()
}
.title(item.key)
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.show()
}
catch (e: Exception) {
activity.error(ApiError.fromThrowable(TAG, e))
}
})
showJson()
b.list.adapter = adapter
b.list.apply {
@ -79,4 +155,15 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
b.noData.isVisible = false
}; return true }
private fun showJson() {
val json = JsonObject().also { json ->
json.add("App.profile", app.gson.toJsonTree(app.profile))
json.add("App.profile.studentData", app.profile.studentData)
json.add("App.profile.loginStore", loginStore?.data ?: JsonObject())
json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values)))
}
adapter.items = LabJsonAdapter.expand(json, 0)
adapter.notifyDataSetChanged()
}
}

View File

@ -13,6 +13,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonArray
@ -32,6 +33,10 @@ class JsonArrayViewHolder(
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonArray, position: Int, adapter: LabJsonAdapter) {
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
b.type.text = "Array"
b.dropdownIcon.rotation = when (item.state) {
AttendanceAdapter.STATE_CLOSED -> 0f
else -> 180f
@ -39,7 +44,7 @@ class JsonArrayViewHolder(
b.previewContainer.isInvisible = item.state != AttendanceAdapter.STATE_CLOSED
b.summaryContainer.isInvisible = item.state == AttendanceAdapter.STATE_CLOSED
b.key.text = item.key
b.key.text = item.key.substringAfterLast(":")
b.previewContainer.text = item.jsonArray.toString().take(200)
b.summaryContainer.text = item.jsonArray.size().toString() + " elements"
}

View File

@ -32,6 +32,8 @@ class JsonElementViewHolder(
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonElement, position: Int, adapter: LabJsonAdapter) {
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
b.type.text = when (item.jsonElement) {
is JsonPrimitive -> when {
item.jsonElement.isNumber -> "Number"
@ -45,7 +47,9 @@ class JsonElementViewHolder(
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
b.key.text = listOf(
item.key.asColoredSpannable(colorSecondary),
item.key
.substringAfterLast(":")
.asColoredSpannable(colorSecondary),
": ",
item.jsonElement.toString().asItalicSpannable()
).concat("")

View File

@ -13,6 +13,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
@ -32,6 +33,10 @@ class JsonObjectViewHolder(
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) {
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
b.type.text = "Object"
b.dropdownIcon.rotation = when (item.state) {
AttendanceAdapter.STATE_CLOSED -> 0f
else -> 180f
@ -39,7 +44,7 @@ class JsonObjectViewHolder(
b.previewContainer.isInvisible = item.state != AttendanceAdapter.STATE_CLOSED
b.summaryContainer.isInvisible = item.state == AttendanceAdapter.STATE_CLOSED
b.key.text = item.key
b.key.text = item.key.substringAfterLast(":")
b.previewContainer.text = item.jsonObject.toString().take(200)
b.summaryContainer.text = item.jsonObject.size().toString() + " elements"
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-26.
*/
package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.LabItemSubObjectBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes
class JsonSubObjectViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: LabItemSubObjectBinding = LabItemSubObjectBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<LabJsonObject, LabJsonAdapter> {
companion object {
private const val TAG = "JsonSubObjectViewHolder"
}
@SuppressLint("SetTextI18n")
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) {
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
b.type.text = "Object"
b.dropdownIcon.rotation = when (item.state) {
AttendanceAdapter.STATE_CLOSED -> 0f
else -> 180f
}
b.key.text = item.key.substringAfterLast(":")
}
}

View File

@ -30,11 +30,12 @@
tools:text="lessonRanges" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:fontFamily="monospace"
android:text="JsonObject"
android:text="Object"
android:textAppearance="@style/NavView.TextView.Helper" />
<com.mikepenz.iconics.view.IconicsImageView

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-2-26.
-->
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/key"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="monospace"
android:maxLines="2"
tools:text="lessonRanges" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:fontFamily="monospace"
android:text="Object"
android:textAppearance="@style/NavView.TextView.Helper" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/dropdownIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="centerInside"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-chevron-down"
app:iiv_size="18dp"
tools:src="@android:drawable/ic_menu_more" />
</LinearLayout>
</layout>