mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 16:21:52 +01:00
Refactor and Convert Adapters to RecyclerView.Adapter
This commit mainly refactors the adapters by adding spacing, comments and following other guidelines. In addition, it moves from using `BaseAdapter` to `RecyclerView.Adapter` which leads to much cleaner adapter classes. a
This commit is contained in:
parent
55a9f8e937
commit
d86d5c1a35
@ -15,7 +15,7 @@ import android.view.SurfaceHolder
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import kotlinx.android.synthetic.main.game_activity.*
|
||||
import kotlinx.android.synthetic.main.app_activity.*
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
@ -15,7 +15,11 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.adapter.LogAdapter
|
||||
import kotlinx.android.synthetic.main.log_activity.*
|
||||
import org.json.JSONObject
|
||||
import java.io.*
|
||||
import java.net.URL
|
||||
@ -32,10 +36,19 @@ class LogActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.log_activity)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val logList = findViewById<ListView>(R.id.log_list)
|
||||
adapter = LogAdapter(this, prefs.getBoolean("log_compact", false), prefs.getString("log_level", "3")!!.toInt(), resources.getStringArray(R.array.log_level))
|
||||
logList.adapter = adapter
|
||||
val compact = prefs.getBoolean("log_compact", false)
|
||||
val logLevel = prefs.getString("log_level", "3")!!.toInt()
|
||||
|
||||
adapter = LogAdapter(this, compact, logLevel, resources.getStringArray(R.array.log_level))
|
||||
|
||||
log_list.adapter = adapter
|
||||
log_list.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
if (!compact)
|
||||
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
|
||||
try {
|
||||
logFile = File("${applicationInfo.dataDir}/skyline.log")
|
||||
logFile.forEachLine {
|
||||
|
@ -18,6 +18,9 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.adapter.AppAdapter
|
||||
import emu.skyline.adapter.AppItem
|
||||
@ -128,12 +131,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
setSupportActionBar(toolbar)
|
||||
open_fab.setOnClickListener(this)
|
||||
log_fab.setOnClickListener(this)
|
||||
game_list.adapter = adapter
|
||||
game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long ->
|
||||
val item = parent.getItemAtPosition(position)
|
||||
if (item is AppItem) {
|
||||
val intent = Intent(this, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
adapter = AppAdapter(this)
|
||||
app_list.adapter = adapter
|
||||
|
||||
app_list.layoutManager = LinearLayoutManager(this)
|
||||
app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@ -181,9 +184,35 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
intent.type = "*/*"
|
||||
startActivityForResult(intent, 2)
|
||||
}
|
||||
R.id.app_item_linear -> {
|
||||
val tag = view.tag
|
||||
|
||||
if (tag is AppItem) {
|
||||
val intent = Intent(this, EmulationActivity::class.java)
|
||||
intent.data = tag.uri
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View?): Boolean {
|
||||
when (view?.id) {
|
||||
R.id.app_item_linear -> {
|
||||
val tag = view.tag
|
||||
|
||||
if (tag is AppItem) {
|
||||
val dialog = AppDialog(tag)
|
||||
dialog.show(supportFragmentManager, "game")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
|
@ -19,7 +19,10 @@ import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ElementType.Header
|
||||
import emu.skyline.adapter.ElementType.Item
|
||||
import emu.skyline.loader.AppEntry
|
||||
|
||||
/**
|
||||
@ -36,7 +39,7 @@ class AppItem(val meta: AppEntry) : BaseItem() {
|
||||
* The title of the application
|
||||
*/
|
||||
val title: String
|
||||
get() = meta.name + " (" + type + ")"
|
||||
get() = meta.name
|
||||
|
||||
/**
|
||||
* The string used as the sub-title, we currently use the author
|
||||
@ -56,6 +59,9 @@ class AppItem(val meta: AppEntry) : BaseItem() {
|
||||
private val type: String
|
||||
get() = meta.format.name
|
||||
|
||||
/**
|
||||
* The name and author is used as the key
|
||||
*/
|
||||
override fun key(): String? {
|
||||
return if (meta.author != null) meta.name + " " + meta.author else meta.name
|
||||
}
|
||||
@ -64,29 +70,41 @@ class AppItem(val meta: AppEntry) : BaseItem() {
|
||||
/**
|
||||
* This adapter is used to display all found applications using their metadata
|
||||
*/
|
||||
internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHeader>(), View.OnClickListener {
|
||||
internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>(), View.OnClickListener {
|
||||
/**
|
||||
* This adds a string header to the view
|
||||
* The icon to use on items that don't have a valid icon
|
||||
*/
|
||||
private val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context.theme)?.toBitmap(256, 256)
|
||||
|
||||
/**
|
||||
* The string to use as a description for items that don't have a valid description
|
||||
*/
|
||||
private val missingString = context?.getString(R.string.metadata_missing)
|
||||
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
*/
|
||||
fun addHeader(string: String) {
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* The onClick handler, it's for displaying the icon preview
|
||||
*
|
||||
* @param view The specific view that was clicked
|
||||
* The onClick handler for the supplied [view], used for the icon preview
|
||||
*/
|
||||
override fun onClick(view: View) {
|
||||
val position = view.tag as Int
|
||||
|
||||
if (getItem(position) is AppItem) {
|
||||
val item = getItem(position) as AppItem
|
||||
|
||||
if (view.id == R.id.icon) {
|
||||
val builder = Dialog(context!!)
|
||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
val imageView = ImageView(context)
|
||||
imageView.setImageBitmap(item.icon)
|
||||
imageView.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
builder.show()
|
||||
}
|
||||
@ -94,60 +112,74 @@ internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHe
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the view for an element at a specific position
|
||||
*
|
||||
* @param position The position of the requested item
|
||||
* @param convertView An existing view (If any)
|
||||
* @param parent The parent view group used for layout inflation
|
||||
*/
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var view = convertView
|
||||
val viewHolder: ViewHolder
|
||||
val item = elementArray[visibleArray[position]]
|
||||
|
||||
if (view == null) {
|
||||
viewHolder = ViewHolder()
|
||||
if (item is AppItem) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
view = inflater.inflate(R.layout.game_item, parent, false)
|
||||
|
||||
viewHolder.icon = view.findViewById(R.id.icon)
|
||||
viewHolder.title = view.findViewById(R.id.text_title)
|
||||
viewHolder.subtitle = view.findViewById(R.id.text_subtitle)
|
||||
view.tag = viewHolder
|
||||
} else if (item is BaseHeader) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
|
||||
viewHolder.title = view.findViewById(R.id.text_title)
|
||||
view.tag = viewHolder
|
||||
}
|
||||
} else {
|
||||
viewHolder = view.tag as ViewHolder
|
||||
}
|
||||
|
||||
if (item is AppItem) {
|
||||
val data = getItem(position) as AppItem
|
||||
|
||||
viewHolder.title!!.text = data.title
|
||||
viewHolder.subtitle!!.text = data.subTitle ?: context?.getString(R.string.metadata_missing)!!
|
||||
|
||||
viewHolder.icon!!.setImageBitmap(data.icon ?: context!!.resources.getDrawable(R.drawable.ic_missing, context.theme).toBitmap(256, 256))
|
||||
viewHolder.icon!!.setOnClickListener(this)
|
||||
viewHolder.icon!!.tag = position
|
||||
} else {
|
||||
viewHolder.title!!.text = (getItem(position) as BaseHeader).title
|
||||
}
|
||||
|
||||
return view!!
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder object is used to hold the views associated with an object
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param icon The ImageView associated with the icon
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
*/
|
||||
private class ViewHolder(var icon: ImageView? = null, var title: TextView? = null, var subtitle: TextView? = null)
|
||||
private class ItemViewHolder(val parent: View, var icon: ImageView, var title: TextView, var subtitle: TextView, var card: View? = null) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent: View, var header: TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder: RecyclerView.ViewHolder? = null
|
||||
|
||||
if (viewType == Item.ordinal) {
|
||||
val view = inflater.inflate(R.layout.app_item_linear, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
if (context is View.OnClickListener)
|
||||
view.setOnClickListener(context as View.OnClickListener)
|
||||
|
||||
if (context is View.OnLongClickListener)
|
||||
view.setOnLongClickListener(context as View.OnLongClickListener)
|
||||
}
|
||||
} else if (viewType == Header.ordinal) {
|
||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
holder = HeaderViewHolder(view)
|
||||
|
||||
holder.header = view.findViewById(R.id.text_title)
|
||||
}
|
||||
|
||||
return holder!!
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [viewHolder]
|
||||
*/
|
||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is AppItem) {
|
||||
val holder = viewHolder as ItemViewHolder
|
||||
|
||||
holder.title.text = item.title
|
||||
holder.subtitle.text = item.subTitle ?: missingString
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
holder.icon.setOnClickListener(this)
|
||||
holder.icon.tag = position
|
||||
|
||||
holder.card?.tag = item
|
||||
holder.parent.tag = item
|
||||
} else if (item is BaseHeader) {
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
holder.header!!.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,10 @@
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.util.SparseIntArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.io.*
|
||||
@ -18,6 +17,9 @@ import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
/**
|
||||
* An enumeration of the type of elements in this adapter
|
||||
*/
|
||||
enum class ElementType(val type: Int) {
|
||||
Header(0x0),
|
||||
Item(0x1)
|
||||
@ -43,13 +45,30 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||
abstract fun key(): String?
|
||||
}
|
||||
|
||||
internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?> : BaseAdapter(), Filterable, Serializable {
|
||||
/**
|
||||
* This adapter has the ability to have 2 types of elements specifically headers and items
|
||||
*/
|
||||
abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?> : RecyclerView.Adapter<ViewHolder>(), Filterable, Serializable {
|
||||
/**
|
||||
* This holds all the elements in an array even if they may not be visible
|
||||
*/
|
||||
var elementArray: ArrayList<BaseElement?> = ArrayList()
|
||||
|
||||
/**
|
||||
* This holds the indices of all the visible items in [elementArray]
|
||||
*/
|
||||
var visibleArray: ArrayList<Int> = ArrayList()
|
||||
|
||||
/**
|
||||
* This holds the search term if there is any, to filter any items added during a search
|
||||
*/
|
||||
private var searchTerm = ""
|
||||
|
||||
fun addItem(element: ItemType) {
|
||||
elementArray.add(element)
|
||||
/**
|
||||
* This functions adds [item] to [elementArray] and [visibleArray] based on the filter
|
||||
*/
|
||||
fun addItem(item: ItemType) {
|
||||
elementArray.add(item)
|
||||
if (searchTerm.isNotEmpty())
|
||||
filter.filter(searchTerm)
|
||||
else {
|
||||
@ -58,18 +77,19 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
|
||||
}
|
||||
}
|
||||
|
||||
fun addHeader(element: HeaderType) {
|
||||
elementArray.add(element)
|
||||
if (searchTerm.isNotEmpty())
|
||||
filter.filter(searchTerm)
|
||||
else {
|
||||
/**
|
||||
* This function adds [header] to [elementArray] and [visibleArray] based on if the filter is active
|
||||
*/
|
||||
fun addHeader(header: HeaderType) {
|
||||
elementArray.add(header)
|
||||
if (searchTerm.isEmpty())
|
||||
visibleArray.add(elementArray.size - 1)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
internal inner class State(val elementArray: ArrayList<BaseElement?>) : Serializable
|
||||
|
||||
/**
|
||||
* This serializes [elementArray] into [file]
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun save(file: File) {
|
||||
val fileObj = FileOutputStream(file)
|
||||
@ -79,6 +99,9 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
|
||||
fileObj.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* This reads in [elementArray] from [file]
|
||||
*/
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
open fun load(file: File) {
|
||||
val fileObj = FileInputStream(file)
|
||||
@ -90,82 +113,124 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
|
||||
filter.filter(searchTerm)
|
||||
}
|
||||
|
||||
/**
|
||||
* This clears the view by clearing [elementArray] and [visibleArray]
|
||||
*/
|
||||
fun clear() {
|
||||
elementArray.clear()
|
||||
visibleArray.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return visibleArray.size
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): BaseElement? {
|
||||
return elementArray[visibleArray[index]]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
/**
|
||||
* This returns the amount of elements that should be drawn to the list
|
||||
*/
|
||||
override fun getItemCount(): Int = visibleArray.size
|
||||
|
||||
/**
|
||||
* This returns a particular element at [position]
|
||||
*/
|
||||
fun getItem(position: Int): BaseElement? {
|
||||
return elementArray[visibleArray[position]]
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the type of an element at the specified position
|
||||
*
|
||||
* @param position The position of the element
|
||||
*/
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return elementArray[visibleArray[position]]!!.elementType.type
|
||||
}
|
||||
|
||||
override fun getViewTypeCount(): Int {
|
||||
return ElementType.values().size
|
||||
}
|
||||
|
||||
abstract override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
|
||||
|
||||
/**
|
||||
* This returns an instance of the filter object which is used to search for items in the view
|
||||
*/
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
inner class ScoredItem(val score: Double, val index: Int, val item:String) {}
|
||||
/**
|
||||
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
|
||||
*/
|
||||
private val jw = JaroWinkler()
|
||||
|
||||
/**
|
||||
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
|
||||
*/
|
||||
private val cos = Cosine()
|
||||
|
||||
/**
|
||||
* This class is used to store the results of the item sorting
|
||||
*
|
||||
* @param score The score of this result
|
||||
* @param index The index of this item
|
||||
*/
|
||||
inner class ScoredItem(val score: Double, val index: Int) {}
|
||||
|
||||
/**
|
||||
* This sorts the items in [keyArray] in relation to how similar they are to [term]
|
||||
*/
|
||||
fun extractSorted(term: String, keyArray: ArrayList<String>): Array<ScoredItem> {
|
||||
val scoredItems : MutableList<ScoredItem> = ArrayList()
|
||||
val scoredItems: MutableList<ScoredItem> = ArrayList()
|
||||
|
||||
val jw = JaroWinkler()
|
||||
val cos = Cosine()
|
||||
keyArray.forEachIndexed { index, item ->
|
||||
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
||||
|
||||
if (similarity != 0.0)
|
||||
scoredItems.add(ScoredItem(similarity, index))
|
||||
}
|
||||
|
||||
keyArray.forEachIndexed { index, item -> scoredItems.add(ScoredItem((jw.similarity(term, item) + cos.similarity(term, item)) / 2, index, item)) }
|
||||
scoredItems.sortWith(compareByDescending { it.score })
|
||||
|
||||
return scoredItems.toTypedArray()
|
||||
}
|
||||
|
||||
override fun performFiltering(charSequence: CharSequence): FilterResults {
|
||||
/**
|
||||
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
||||
*/
|
||||
override fun performFiltering(term: CharSequence): FilterResults {
|
||||
val results = FilterResults()
|
||||
searchTerm = (charSequence as String).toLowerCase(Locale.getDefault())
|
||||
if (charSequence.isEmpty()) {
|
||||
searchTerm = (term as String).toLowerCase(Locale.getDefault())
|
||||
|
||||
if (term.isEmpty()) {
|
||||
results.values = elementArray.indices.toMutableList()
|
||||
results.count = elementArray.size
|
||||
} else {
|
||||
val filterData = ArrayList<Int>()
|
||||
|
||||
val keyArray = ArrayList<String>()
|
||||
val keyIndex = SparseIntArray()
|
||||
|
||||
for (index in elementArray.indices) {
|
||||
val item = elementArray[index]!!
|
||||
|
||||
if (item is BaseItem) {
|
||||
keyIndex.append(keyArray.size, index)
|
||||
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
}
|
||||
|
||||
val topResults = extractSorted(searchTerm, keyArray)
|
||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||
|
||||
for (result in topResults)
|
||||
if (result.score > avgScore)
|
||||
filterData.add(keyIndex[result.index])
|
||||
|
||||
results.values = filterData
|
||||
results.count = filterData.size
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* This publishes the results that were calculated in [performFiltering] to the view
|
||||
*/
|
||||
override fun publishResults(charSequence: CharSequence, results: FilterResults) {
|
||||
if (results.values is ArrayList<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
visibleArray = results.values as ArrayList<Int>
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
@ -14,77 +14,118 @@ import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
|
||||
/**
|
||||
* This class is used to hold all data about a log entry
|
||||
*/
|
||||
internal class LogItem(val message: String, val level: String) : BaseItem() {
|
||||
/**
|
||||
* The log message itself is used as the search key
|
||||
*/
|
||||
override fun key(): String? {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
internal class LogAdapter internal constructor(val context: Context, val compact: Boolean, private val debug_level: Int, private val level_str: Array<String>) : HeaderAdapter<LogItem, BaseHeader>(), OnLongClickListener {
|
||||
/**
|
||||
* This adapter is used for displaying logs outputted by the application
|
||||
*/
|
||||
internal class LogAdapter internal constructor(val context: Context, val compact: Boolean, private val debug_level: Int, private val level_str: Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>(), OnLongClickListener {
|
||||
private val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
/**
|
||||
* This function adds a line to this log adapter
|
||||
*/
|
||||
fun add(logLine: String) {
|
||||
try {
|
||||
val logMeta = logLine.split("|", limit = 3)
|
||||
|
||||
if (logMeta[0].startsWith("1")) {
|
||||
val level = logMeta[1].toInt()
|
||||
if (level > debug_level) return
|
||||
|
||||
addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level]))
|
||||
} else {
|
||||
addHeader(BaseHeader(logMeta[1]))
|
||||
}
|
||||
} catch (ignored: IndexOutOfBoundsException) {
|
||||
} catch (ignored: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
*/
|
||||
private class ItemViewHolder(val parent: View, var title: TextView, var subtitle: TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent: View, var header: TextView) : RecyclerView.ViewHolder(parent)
|
||||
|
||||
/**
|
||||
* The onLongClick handler for the supplied [view], used to copy a log into the clipboard
|
||||
*/
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
val item = getItem((view.tag as ViewHolder).position) as LogItem
|
||||
val item = view.tag as LogItem
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var view = convertView
|
||||
val viewHolder: ViewHolder
|
||||
val item = elementArray[visibleArray[position]]
|
||||
if (view == null) {
|
||||
viewHolder = ViewHolder()
|
||||
val inflater = LayoutInflater.from(context)
|
||||
if (item is LogItem) {
|
||||
if (compact) {
|
||||
view = inflater.inflate(R.layout.log_item_compact, parent, false)
|
||||
viewHolder.txtTitle = view.findViewById(R.id.text_title)
|
||||
} else {
|
||||
view = inflater.inflate(R.layout.log_item, parent, false)
|
||||
viewHolder.txtTitle = view.findViewById(R.id.text_title)
|
||||
viewHolder.txtSub = view.findViewById(R.id.text_subtitle)
|
||||
}
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder: RecyclerView.ViewHolder? = null
|
||||
|
||||
if (viewType == ElementType.Item.ordinal) {
|
||||
if (compact) {
|
||||
val view = inflater.inflate(R.layout.log_item_compact, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title))
|
||||
|
||||
view.setOnLongClickListener(this)
|
||||
} else {
|
||||
val view = inflater.inflate(R.layout.log_item, parent, false)
|
||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
|
||||
view.setOnLongClickListener(this)
|
||||
} else if (item is BaseHeader) {
|
||||
view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
viewHolder.txtTitle = view.findViewById(R.id.text_title)
|
||||
}
|
||||
view!!.tag = viewHolder
|
||||
} else {
|
||||
viewHolder = view.tag as ViewHolder
|
||||
} else if (viewType == ElementType.Header.ordinal) {
|
||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
||||
holder = HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
||||
}
|
||||
|
||||
return holder!!
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [viewHolder]
|
||||
*/
|
||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is LogItem) {
|
||||
viewHolder.txtTitle!!.text = item.message
|
||||
if (!compact) viewHolder.txtSub!!.text = item.level
|
||||
val holder = viewHolder as ItemViewHolder
|
||||
|
||||
holder.title.text = item.message
|
||||
holder.subtitle?.text = item.level
|
||||
|
||||
holder.parent.tag = item
|
||||
} else if (item is BaseHeader) {
|
||||
viewHolder.txtTitle!!.text = item.title
|
||||
val holder = viewHolder as HeaderViewHolder
|
||||
|
||||
holder.header.text = item.title
|
||||
}
|
||||
viewHolder.position = position
|
||||
return view
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
var txtTitle: TextView? = null
|
||||
var txtSub: TextView? = null
|
||||
var position = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
14
app/src/main/res/README.md
Normal file
14
app/src/main/res/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Credits (Drawables)
|
||||
## [Material Design Icons](https://material.io/resources/icons) (Apache-2)
|
||||
* ic_clear
|
||||
* ic_log
|
||||
* ic_open
|
||||
* ic_play
|
||||
* ic_refresh
|
||||
* ic_search
|
||||
* ic_settings
|
||||
* ic_share
|
||||
## [Default Icon](https://github.com/switchbrew/libnx/blob/master/nx/default_icon.jpg)
|
||||
We've recieved permission to use the icon from it's author [jaames](https://github.com/jaames)
|
||||
## Skyline Logo
|
||||
Skyline's logo was designed by [PixelyIon](https://github.com/PixelyIon)
|
BIN
app/src/main/res/drawable/default_icon.jpg
Normal file
BIN
app/src/main/res/drawable/default_icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
@ -1,7 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_item_linear"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
|
||||
@ -12,20 +17,21 @@
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/icon"
|
||||
android:src="@drawable/ic_missing" />
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||
android:layout_alignTop="@id/icon"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_title"
|
||||
android:layout_alignStart="@id/text_title"
|
@ -18,10 +18,10 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ListView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/log_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fastScrollEnabled="true"
|
||||
android:transcriptMode="normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -4,7 +4,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:orientation="vertical"
|
||||
android:padding="6dp">
|
||||
android:padding="1dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/app_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
|
@ -3,8 +3,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
@ -14,6 +13,6 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textColor="?colorSecondary"
|
||||
android:textSize="13sp" />
|
||||
android:textSize="15sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
Loading…
Reference in New Issue
Block a user