mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-28 10:25:29 +03:00
Rewrite adapter to handle any layout
This commit is contained in:
parent
e023dbbf0a
commit
22140defae
@ -59,6 +59,13 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.input.onscreen.OnScreenEditActivity"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.input.ControllerActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
|
@ -16,10 +16,11 @@ 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 com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.adapter.LogAdapter
|
||||
import emu.skyline.adapter.GenericAdapter
|
||||
import emu.skyline.adapter.HeaderViewItem
|
||||
import emu.skyline.adapter.LogViewItem
|
||||
import kotlinx.android.synthetic.main.log_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
import org.json.JSONObject
|
||||
@ -38,7 +39,7 @@ class LogActivity : AppCompatActivity() {
|
||||
/**
|
||||
* The adapter used for adding elements from the log to [log_list]
|
||||
*/
|
||||
private lateinit var adapter : LogAdapter
|
||||
private val adapter = GenericAdapter()
|
||||
|
||||
/**
|
||||
* This initializes [toolbar] and fills [log_list] with data from the logs
|
||||
@ -54,11 +55,9 @@ class LogActivity : AppCompatActivity() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
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))
|
||||
val logLevels = resources.getStringArray(R.array.log_level)
|
||||
|
||||
log_list.adapter = adapter
|
||||
log_list.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
if (!compact)
|
||||
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
@ -66,8 +65,21 @@ class LogActivity : AppCompatActivity() {
|
||||
try {
|
||||
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
|
||||
|
||||
logFile.forEachLine {
|
||||
adapter.add(it)
|
||||
logFile.forEachLine { logLine ->
|
||||
try {
|
||||
val logMeta = logLine.split("|", limit = 3)
|
||||
|
||||
if (logMeta[0].startsWith("1")) {
|
||||
val level = logMeta[1].toInt()
|
||||
if (level > logLevel) return@forEachLine
|
||||
|
||||
adapter.addItem(LogViewItem(compact, logMeta[2].replace('\\', '\n'), logLevels[level]))
|
||||
} else {
|
||||
adapter.addItem(HeaderViewItem(logMeta[1]))
|
||||
}
|
||||
} catch (ignored : IndexOutOfBoundsException) {
|
||||
} catch (ignored : NumberFormatException) {
|
||||
}
|
||||
}
|
||||
} catch (e : FileNotFoundException) {
|
||||
Log.w("Logger", "IO Error during access of log file: " + e.message)
|
||||
@ -149,10 +161,10 @@ class LogActivity : AppCompatActivity() {
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
urlConnection.setRequestProperty("Referer", "https://hastebin.com/")
|
||||
|
||||
val bufferedWriter = urlConnection.outputStream.bufferedWriter()
|
||||
bufferedWriter.write(logFile.readText())
|
||||
bufferedWriter.flush()
|
||||
bufferedWriter.close()
|
||||
urlConnection.outputStream.bufferedWriter().use {
|
||||
it.write(logFile.readText())
|
||||
it.flush()
|
||||
}
|
||||
|
||||
if (urlConnection.responseCode != 200) {
|
||||
Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.responseCode)
|
||||
|
@ -18,17 +18,21 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
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.GridLayoutSpan
|
||||
import emu.skyline.adapter.AppViewItem
|
||||
import emu.skyline.adapter.GenericAdapter
|
||||
import emu.skyline.adapter.HeaderViewItem
|
||||
import emu.skyline.adapter.LayoutType
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseElement
|
||||
import emu.skyline.data.BaseHeader
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.RomFormat
|
||||
@ -41,6 +45,10 @@ import kotlin.concurrent.thread
|
||||
import kotlin.math.ceil
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val TAG = MainActivity::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to get/set shared preferences
|
||||
*/
|
||||
@ -49,29 +57,38 @@ class MainActivity : AppCompatActivity() {
|
||||
/**
|
||||
* The adapter used for adding elements to [app_list]
|
||||
*/
|
||||
private lateinit var adapter : AppAdapter
|
||||
private val adapter = GenericAdapter()
|
||||
|
||||
private var reloading = AtomicBoolean()
|
||||
|
||||
private val layoutType get() = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
|
||||
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||
|
||||
private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog)
|
||||
|
||||
/**
|
||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
|
||||
*/
|
||||
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
||||
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, romElements : ArrayList<BaseElement>, found : Boolean = false) : Boolean {
|
||||
var foundCurrent = found
|
||||
|
||||
directory.listFiles().forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
|
||||
foundCurrent = addEntries(extension, romFormat, file, romElements, foundCurrent)
|
||||
} else {
|
||||
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||
RomFile(this, romFormat, file.uri).let { romFile ->
|
||||
val finalFoundCurrent = foundCurrent
|
||||
runOnUiThread {
|
||||
if (!finalFoundCurrent) adapter.addHeader(romFormat.name)
|
||||
if (!finalFoundCurrent) {
|
||||
romElements.add(BaseHeader(romFormat.name))
|
||||
adapter.addItem(HeaderViewItem(romFormat.name))
|
||||
}
|
||||
|
||||
adapter.addItem(AppItem(romFile.appEntry))
|
||||
romElements.add(AppItem(romFile.appEntry).also {
|
||||
adapter.addItem(it.toViewItem())
|
||||
})
|
||||
}
|
||||
|
||||
foundCurrent = true
|
||||
@ -86,15 +103,22 @@ class MainActivity : AppCompatActivity() {
|
||||
/**
|
||||
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
|
||||
*
|
||||
* @param tryLoad If this is false then trying to load cached adapter data is skipped entirely
|
||||
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
|
||||
*/
|
||||
private fun refreshAdapter(tryLoad : Boolean) {
|
||||
if (tryLoad) {
|
||||
private fun refreshAdapter(loadFromFile : Boolean) {
|
||||
val romsFile = File(applicationContext.filesDir.canonicalPath + "/roms.bin")
|
||||
|
||||
if (loadFromFile) {
|
||||
try {
|
||||
adapter.load(File(applicationContext.filesDir.canonicalPath + "/roms.bin"))
|
||||
loadSerializedList<BaseElement>(romsFile).forEach {
|
||||
if (it is BaseHeader)
|
||||
adapter.addItem(HeaderViewItem(it.title))
|
||||
else if (it is AppItem)
|
||||
adapter.addItem(it.toViewItem())
|
||||
}
|
||||
return
|
||||
} catch (e : Exception) {
|
||||
Log.w("refreshFiles", "Ran into exception while loading: ${e.message}")
|
||||
Log.w(TAG, "Ran into exception while loading: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,22 +131,26 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
try {
|
||||
runOnUiThread { adapter.clear() }
|
||||
runOnUiThread { adapter.removeAllItems() }
|
||||
|
||||
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!
|
||||
|
||||
var foundRoms = addEntries("nro", RomFormat.NRO, searchLocation)
|
||||
foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, searchLocation)
|
||||
foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, searchLocation)
|
||||
foundRoms = foundRoms or addEntries("nsp", RomFormat.NSP, searchLocation)
|
||||
val romElements = ArrayList<BaseElement>()
|
||||
addEntries("nro", RomFormat.NRO, searchLocation, romElements)
|
||||
addEntries("nso", RomFormat.NSO, searchLocation, romElements)
|
||||
addEntries("nca", RomFormat.NCA, searchLocation, romElements)
|
||||
addEntries("nsp", RomFormat.NSP, searchLocation, romElements)
|
||||
|
||||
runOnUiThread {
|
||||
if (!foundRoms) adapter.addHeader(getString(R.string.no_rom))
|
||||
if (romElements.isEmpty()) {
|
||||
romElements.add(BaseHeader(getString(R.string.no_rom)))
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.no_rom)))
|
||||
}
|
||||
|
||||
try {
|
||||
adapter.save(File(applicationContext.filesDir.canonicalPath + "/roms.bin"))
|
||||
romElements.serialize(romsFile)
|
||||
} catch (e : IOException) {
|
||||
Log.w("refreshFiles", "Ran into exception while saving: ${e.message}")
|
||||
Log.w(TAG, "Ran into exception while saving: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,22 +244,27 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAppListDecoration() {
|
||||
when (layoutType) {
|
||||
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
|
||||
LayoutType.Grid, LayoutType.GridCompact -> if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAppList() {
|
||||
app_list.adapter = adapter
|
||||
|
||||
val itemWidth = 225
|
||||
val metrics = resources.displayMetrics
|
||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||
app_list.adapter = adapter
|
||||
app_list.layoutManager = when (adapter.layoutType) {
|
||||
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||
|
||||
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
||||
spanSizeLookup = GridLayoutSpan(adapter, gridSpan).also {
|
||||
if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
||||
}
|
||||
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
|
||||
}
|
||||
}
|
||||
setAppListDecoration()
|
||||
|
||||
if (sharedPreferences.getString("search_location", "") == "") {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
@ -337,8 +370,19 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (layoutType != adapter.layoutType) {
|
||||
setupAppList()
|
||||
var layoutTypeChanged = false
|
||||
for (appViewItem in adapter.allItems.filterIsInstance(AppViewItem::class.java)) {
|
||||
if (layoutType != appViewItem.layoutType) {
|
||||
appViewItem.layoutType = layoutType
|
||||
layoutTypeChanged = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutTypeChanged) {
|
||||
adapter.notifyAllItemsChanged()
|
||||
setAppListDecoration()
|
||||
}
|
||||
|
||||
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)
|
||||
|
22
app/src/main/java/emu/skyline/SerializationHelper.kt
Normal file
22
app/src/main/java/emu/skyline/SerializationHelper.kt
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline
|
||||
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
|
||||
fun <T : Serializable> ArrayList<T>.serialize(file : File) {
|
||||
ObjectOutputStream(file.outputStream()).use {
|
||||
it.writeObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Serializable> loadSerializedList(file : File) = ObjectInputStream(file.inputStream()).use {
|
||||
it.readObject()
|
||||
} as ArrayList<T>
|
@ -1,114 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.app_item_grid_compact.*
|
||||
|
||||
/**
|
||||
* This enumerates the type of layouts the menu can be in
|
||||
*/
|
||||
enum class LayoutType(val layoutRes : Int) {
|
||||
List(R.layout.app_item_linear),
|
||||
Grid(R.layout.app_item_grid),
|
||||
GridCompact(R.layout.app_item_grid_compact)
|
||||
}
|
||||
|
||||
private typealias InteractionFunction = (appItem : AppItem) -> Unit
|
||||
|
||||
/**
|
||||
* This adapter is used to display all found applications using their metadata
|
||||
*/
|
||||
internal class AppAdapter(val layoutType : LayoutType, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
private lateinit var context : Context
|
||||
private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
*/
|
||||
fun addHeader(string : String) {
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||
context = parent.context
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> inflater.inflate(layoutType.layoutRes, parent, false)
|
||||
|
||||
ElementType.Header -> inflater.inflate(R.layout.section_item, parent, false)
|
||||
}
|
||||
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> ItemViewHolder(view)
|
||||
|
||||
ElementType.Header -> HeaderViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is AppItem && holder is ItemViewHolder) {
|
||||
holder.text_title.text = item.title
|
||||
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
holder.icon.setOnClickListener { showIconDialog(item) }
|
||||
}
|
||||
|
||||
when (layoutType) {
|
||||
LayoutType.List -> holder.itemView
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
|
||||
}.apply {
|
||||
setOnClickListener { onClick.invoke(item) }
|
||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||
}
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
|
||||
private fun showIconDialog(appItem : AppItem) {
|
||||
val builder = Dialog(context)
|
||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
val imageView = ImageView(context)
|
||||
imageView.setImageBitmap(appItem.icon ?: missingIcon)
|
||||
|
||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
builder.show()
|
||||
}
|
||||
}
|
73
app/src/main/java/emu/skyline/adapter/AppViewItem.kt
Normal file
73
app/src/main/java/emu/skyline/adapter/AppViewItem.kt
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import kotlinx.android.synthetic.main.app_item_grid_compact.*
|
||||
|
||||
/**
|
||||
* This enumerates the type of layouts the menu can be in
|
||||
*/
|
||||
enum class LayoutType(val layoutRes : Int) {
|
||||
List(R.layout.app_item_linear),
|
||||
Grid(R.layout.app_item_grid),
|
||||
GridCompact(R.layout.app_item_grid_compact)
|
||||
}
|
||||
|
||||
private typealias InteractionFunction = (appItem : AppItem) -> Unit
|
||||
|
||||
private data class AppLayoutFactory(private val layoutType : LayoutType) : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false)
|
||||
}
|
||||
|
||||
class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericViewHolderBinder() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType)
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = item.title
|
||||
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
|
||||
}
|
||||
|
||||
when (layoutType) {
|
||||
LayoutType.List -> holder.itemView
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
|
||||
}.apply {
|
||||
setOnClickListener { onClick.invoke(item) }
|
||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showIconDialog(context : Context, appItem : AppItem) {
|
||||
val builder = Dialog(context)
|
||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
val imageView = ImageView(context)
|
||||
imageView.setImageBitmap(appItem.icon ?: missingIcon)
|
||||
|
||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
builder.show()
|
||||
}
|
||||
|
||||
override fun toString() = item.key()
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.controller_item.*
|
||||
import kotlinx.android.synthetic.main.section_item.text_title
|
||||
|
||||
/**
|
||||
* This is a class that holds everything relevant to a single item in the controller configuration list
|
||||
*
|
||||
* @param content The main line of text describing what the item is
|
||||
* @param subContent The secondary line of text to show data more specific data about the item
|
||||
*/
|
||||
abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() {
|
||||
lateinit var adapter : ControllerAdapter
|
||||
|
||||
var position : Int? = null
|
||||
|
||||
/**
|
||||
* This function updates the visible contents of the item
|
||||
*/
|
||||
fun update(content : String?, subContent : String?) {
|
||||
if (content != null)
|
||||
this.content = content
|
||||
|
||||
if (subContent != null)
|
||||
this.subContent = subContent
|
||||
|
||||
position?.let { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used as a generic function to update the contents of the item
|
||||
*/
|
||||
abstract fun update()
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display the [type] of the currently active controller
|
||||
*/
|
||||
class ControllerTypeItem(val context : Context, val type : ControllerType) : ControllerItem(context.getString(R.string.controller_type), context.getString(type.stringRes)) {
|
||||
/**
|
||||
* This function just updates [subContent] based on [type]
|
||||
*/
|
||||
override fun update() = update(null, context.getString(type.stringRes))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display general settings items regarding controller
|
||||
*
|
||||
* @param type The type of controller setting this item is displaying
|
||||
*/
|
||||
class ControllerGeneralItem(val context : ControllerActivity, val type : GeneralType) : ControllerItem(context.getString(type.stringRes), getSummary(context, type)) {
|
||||
companion object {
|
||||
/**
|
||||
* This returns the summary for [type] by using data encapsulated within [Controller]
|
||||
*/
|
||||
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
|
||||
val controller = InputManager.controllers[context.id]!!
|
||||
|
||||
return when (type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val partner = (controller as JoyConLeftController).partnerId
|
||||
|
||||
if (partner != null)
|
||||
"${context.getString(R.string.controller)} #${partner + 1}"
|
||||
else
|
||||
context.getString(R.string.none)
|
||||
}
|
||||
|
||||
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, type))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display a particular [button] mapping for the controller
|
||||
*/
|
||||
class ControllerButtonItem(val context : ControllerActivity, val button : ButtonId) : ControllerItem(button.long?.let { context.getString(it) } ?: button.toString(), getSummary(context, button)) {
|
||||
companion object {
|
||||
/**
|
||||
* This returns the summary for [button] by doing a reverse-lookup in [InputManager.eventMap]
|
||||
*/
|
||||
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
|
||||
val guestEvent = ButtonGuestEvent(context.id, button)
|
||||
return InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, button))
|
||||
}
|
||||
|
||||
/**
|
||||
* This item is used to display all information regarding a [stick] and it's mappings for the controller
|
||||
*/
|
||||
class ControllerStickItem(val context : ControllerActivity, val stick : StickId) : ControllerItem(stick.toString(), getSummary(context, stick)) {
|
||||
companion object {
|
||||
/**
|
||||
* This returns the summary for [stick] by doing reverse-lookups in [InputManager.eventMap]
|
||||
*/
|
||||
fun getSummary(context : ControllerActivity, stick : StickId) : String {
|
||||
val buttonGuestEvent = ButtonGuestEvent(context.id, stick.button)
|
||||
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
var axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, true)
|
||||
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, false)
|
||||
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, true)
|
||||
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, false)
|
||||
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
return "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function just updates [subContent] based on [getSummary]
|
||||
*/
|
||||
override fun update() = update(null, getSummary(context, stick))
|
||||
}
|
||||
|
||||
class ControllerCheckBox()
|
||||
|
||||
/**
|
||||
* This adapter is used to create a list which handles having a simple view
|
||||
*/
|
||||
class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
fun addHeader(string : String) {
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
fun addItem(item : ControllerItem) {
|
||||
item.adapter = this
|
||||
super.addItem(item)
|
||||
}
|
||||
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder = LayoutInflater.from(parent.context).let { layoutInflater ->
|
||||
when (ElementType.values()[viewType]) {
|
||||
ElementType.Header -> HeaderViewHolder(layoutInflater.inflate(R.layout.section_item, parent, false))
|
||||
|
||||
ElementType.Item -> ItemViewHolder(layoutInflater.inflate(R.layout.controller_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is ControllerItem && holder is ItemViewHolder) {
|
||||
item.position = position
|
||||
|
||||
holder.text_title.text = item.content
|
||||
holder.text_subtitle.text = item.subContent
|
||||
|
||||
holder.itemView.setOnClickListener { onItemClickCallback.invoke(item) }
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
116
app/src/main/java/emu/skyline/adapter/GenericAdapter.kt
Normal file
116
app/src/main/java/emu/skyline/adapter/GenericAdapter.kt
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.util.*
|
||||
|
||||
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
var currentSearchTerm = ""
|
||||
|
||||
val currentItems get() = if (currentSearchTerm.isEmpty()) allItems else filteredItems
|
||||
val allItems = mutableListOf<GenericViewHolderBinder>()
|
||||
private var filteredItems = listOf<GenericViewHolderBinder>()
|
||||
|
||||
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
|
||||
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createLayout(parent))
|
||||
|
||||
override fun onBindViewHolder(holder : GenericViewHolder, position : Int) {
|
||||
currentItems[position].apply {
|
||||
adapter = this@GenericAdapter
|
||||
bind(holder, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = currentItems.size
|
||||
|
||||
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
|
||||
|
||||
fun addItem(item : GenericViewHolderBinder) {
|
||||
allItems.add(item)
|
||||
notifyItemInserted(currentItems.size)
|
||||
}
|
||||
|
||||
fun removeAllItems() {
|
||||
val size = currentItems.size
|
||||
allItems.clear()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
fun notifyAllItemsChanged() {
|
||||
notifyItemRangeChanged(0, currentItems.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns an instance of the filter object which is used to search for items in the view
|
||||
*/
|
||||
override fun getFilter() = object : Filter() {
|
||||
/**
|
||||
* 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()
|
||||
|
||||
inner class ScoredItem(val score : Double, val item : GenericViewHolderBinder)
|
||||
|
||||
/**
|
||||
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
|
||||
*/
|
||||
fun extractSorted() = allItems.mapNotNull { item ->
|
||||
item.toString().toLowerCase(Locale.getDefault()).let {
|
||||
val similarity = (jw.similarity(currentSearchTerm, it)) + cos.similarity(currentSearchTerm, it) / 2
|
||||
if (similarity != 0.0) ScoredItem(similarity, item) else null
|
||||
}
|
||||
}.apply {
|
||||
sortedByDescending { it.score }
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs filtering on the items in [allItems] based on similarity to [term]
|
||||
*/
|
||||
override fun performFiltering(term : CharSequence) : FilterResults {
|
||||
val results = FilterResults()
|
||||
currentSearchTerm = (term as String).toLowerCase(Locale.getDefault())
|
||||
|
||||
if (term.isEmpty()) {
|
||||
results.values = allItems.toMutableList()
|
||||
results.count = allItems.size
|
||||
} else {
|
||||
val filterData = mutableListOf<GenericViewHolderBinder>()
|
||||
|
||||
val topResults = extractSorted()
|
||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||
|
||||
for (result in topResults)
|
||||
if (result.score > avgScore) filterData.add(result.item)
|
||||
|
||||
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) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
filteredItems = results.values as List<GenericViewHolderBinder>
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
|
||||
class GenericViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
interface GenericLayoutFactory {
|
||||
fun createLayout(parent : ViewGroup) : View
|
||||
}
|
||||
|
||||
abstract class GenericViewHolderBinder {
|
||||
var adapter : GenericAdapter? = null
|
||||
|
||||
abstract fun getLayoutFactory() : GenericLayoutFactory
|
||||
|
||||
abstract fun bind(holder : GenericViewHolder, position : Int)
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.util.SparseIntArray
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.data.BaseItem
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* An enumeration of the type of elements in this adapter
|
||||
*/
|
||||
enum class ElementType {
|
||||
Header,
|
||||
Item,
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an abstract class that all adapter element classes inherit from
|
||||
*/
|
||||
abstract class BaseElement constructor(val elementType : ElementType) : Serializable
|
||||
|
||||
/**
|
||||
* This is an abstract class that all adapter header classes inherit from
|
||||
*/
|
||||
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
|
||||
|
||||
/**
|
||||
* 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 = ""
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
visibleArray.add(elementArray.size - 1)
|
||||
notifyItemInserted(visibleArray.size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
notifyItemInserted(visibleArray.size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This serializes [elementArray] into [file]
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun save(file : File) {
|
||||
val fileObj = FileOutputStream(file)
|
||||
val out = ObjectOutputStream(fileObj)
|
||||
out.writeObject(elementArray)
|
||||
out.close()
|
||||
fileObj.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* This reads in [elementArray] from [file]
|
||||
*/
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
open fun load(file : File) {
|
||||
val fileObj = FileInputStream(file)
|
||||
val input = ObjectInputStream(fileObj)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
elementArray = input.readObject() as ArrayList<BaseElement?>
|
||||
input.close()
|
||||
fileObj.close()
|
||||
filter.filter(searchTerm)
|
||||
}
|
||||
|
||||
/**
|
||||
* This clears the view by clearing [elementArray] and [visibleArray]
|
||||
*/
|
||||
fun clear() {
|
||||
elementArray.clear()
|
||||
visibleArray.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.ordinal
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns an instance of the filter object which is used to search for items in the view
|
||||
*/
|
||||
override fun getFilter() = object : Filter() {
|
||||
/**
|
||||
* 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()
|
||||
|
||||
keyArray.forEachIndexed { index, item ->
|
||||
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
||||
|
||||
if (similarity != 0.0)
|
||||
scoredItems.add(ScoredItem(similarity, index))
|
||||
}
|
||||
|
||||
scoredItems.sortWith(compareByDescending { it.score })
|
||||
|
||||
return scoredItems.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
||||
*/
|
||||
override fun performFiltering(term : CharSequence) : FilterResults {
|
||||
val results = FilterResults()
|
||||
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 && item.key() != null) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to lookup the span based on the type of the element
|
||||
*
|
||||
* @param adapter The adapter which is used to deduce the type of the item based on the position
|
||||
* @param headerSpan The span size to return for headers
|
||||
*/
|
||||
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
|
||||
/**
|
||||
* This returns the size of the span based on the type of the element at [position]
|
||||
*/
|
||||
override fun getSpanSize(position : Int) : Int {
|
||||
val item = adapter.getItem(position)!!
|
||||
return if (item.elementType == ElementType.Item)
|
||||
1
|
||||
else
|
||||
headerSpan
|
||||
}
|
||||
}
|
26
app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt
Normal file
26
app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import emu.skyline.R
|
||||
import kotlinx.android.synthetic.main.section_item.*
|
||||
|
||||
private object HeaderLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
|
||||
}
|
||||
|
||||
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = text
|
||||
}
|
||||
|
||||
override fun toString() = ""
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.log_item.*
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>() {
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
val view = when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> inflater.inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
|
||||
|
||||
ElementType.Header -> inflater.inflate(R.layout.log_item, parent, false)
|
||||
}
|
||||
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
if (compact) {
|
||||
ItemViewHolder(view)
|
||||
} else {
|
||||
ItemViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
ElementType.Header -> {
|
||||
HeaderViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is LogItem && holder is ItemViewHolder) {
|
||||
holder.text_title.text = item.message
|
||||
holder.text_subtitle?.text = item.level
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
33
app/src/main/java/emu/skyline/adapter/LogViewItem.kt
Normal file
33
app/src/main/java/emu/skyline/adapter/LogViewItem.kt
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import emu.skyline.R
|
||||
import kotlinx.android.synthetic.main.log_item.*
|
||||
|
||||
private data class LogLayoutFactory(private val compact : Boolean) : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
|
||||
}
|
||||
|
||||
class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericViewHolderBinder() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = message
|
||||
holder.text_subtitle?.text = level
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
it.context.getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Log Message", "$message ($level)"))
|
||||
Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.ButtonGuestEvent
|
||||
import emu.skyline.input.ButtonId
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
/**
|
||||
* This item is used to display a particular [button] mapping for the controller
|
||||
*/
|
||||
class ControllerButtonViewItem(private val controllerId : Int, val button : ButtonId, private val onClick : (item : ControllerButtonViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
content = button.long?.let { holder.itemView.context.getString(it) } ?: button.toString()
|
||||
val guestEvent = ButtonGuestEvent(controllerId, button)
|
||||
subContent = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: ""
|
||||
|
||||
super.bind(holder, position)
|
||||
|
||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericLayoutFactory
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.adapter.GenericViewHolderBinder
|
||||
import kotlinx.android.synthetic.main.controller_checkbox_item.*
|
||||
|
||||
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
|
||||
}
|
||||
|
||||
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericViewHolderBinder() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = title
|
||||
holder.text_subtitle.text = summary
|
||||
holder.checkbox.isChecked = checked
|
||||
holder.itemView.setOnClickListener {
|
||||
checked = !checked
|
||||
onCheckedChange.invoke(this, position)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.GeneralType
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.input.JoyConLeftController
|
||||
|
||||
/**
|
||||
* This item is used to display general settings items regarding controller
|
||||
*
|
||||
* @param type The type of controller setting this item is displaying
|
||||
*/
|
||||
class ControllerGeneralViewItem(private val controllerId : Int, val type : GeneralType, private val onClick : (item : ControllerGeneralViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
val controller = InputManager.controllers[controllerId]!!
|
||||
|
||||
content = context.getString(type.stringRes)
|
||||
subContent = when (type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val partner = (controller as JoyConLeftController).partnerId
|
||||
|
||||
if (partner != null)
|
||||
"${context.getString(R.string.controller)} #${partner + 1}"
|
||||
else
|
||||
context.getString(R.string.none)
|
||||
}
|
||||
|
||||
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
|
||||
}
|
||||
super.bind(holder, position)
|
||||
|
||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.AxisGuestEvent
|
||||
import emu.skyline.input.ButtonGuestEvent
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.input.StickId
|
||||
|
||||
/**
|
||||
* This item is used to display all information regarding a [stick] and it's mappings for the controller
|
||||
*/
|
||||
class ControllerStickViewItem(private val controllerId : Int, val stick : StickId, private val onClick : (item : ControllerStickViewItem, position : Int) -> Unit) : ControllerViewItem(stick.toString()) {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
|
||||
val buttonGuestEvent = ButtonGuestEvent(controllerId, stick.button)
|
||||
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
var axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, true)
|
||||
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, false)
|
||||
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, true)
|
||||
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, false)
|
||||
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
subContent = "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
|
||||
|
||||
super.bind(holder, position)
|
||||
|
||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.ControllerType
|
||||
|
||||
/**
|
||||
* This item is used to display the [type] of the currently active controller
|
||||
*/
|
||||
class ControllerTypeViewItem(private val type : ControllerType, private val onClick : (item : ControllerTypeViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
|
||||
content = context.getString(R.string.controller_type)
|
||||
subContent = context.getString(type.stringRes)
|
||||
|
||||
super.bind(holder, position)
|
||||
|
||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericLayoutFactory
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.adapter.GenericViewHolderBinder
|
||||
import kotlinx.android.synthetic.main.controller_item.*
|
||||
|
||||
private object ControllerLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
|
||||
}
|
||||
|
||||
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericViewHolderBinder() {
|
||||
private var position = -1
|
||||
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
this.position = position
|
||||
holder.text_title.apply {
|
||||
isGone = content.isEmpty()
|
||||
text = content
|
||||
}
|
||||
holder.text_subtitle.apply {
|
||||
isGone = subContent.isEmpty()
|
||||
text = subContent
|
||||
}
|
||||
onClick?.let { onClick -> holder.itemView.setOnClickListener { onClick.invoke() } }
|
||||
}
|
||||
|
||||
fun update() = adapter?.notifyItemChanged(position)
|
||||
}
|
@ -6,8 +6,6 @@
|
||||
package emu.skyline.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import emu.skyline.R
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.loader.LoaderResult
|
||||
@ -19,32 +17,27 @@ class AppItem(val meta : AppEntry) : BaseItem() {
|
||||
/**
|
||||
* The icon of the application
|
||||
*/
|
||||
val icon : Bitmap?
|
||||
get() = meta.icon
|
||||
val icon get() = meta.icon
|
||||
|
||||
/**
|
||||
* The title of the application
|
||||
*/
|
||||
val title : String
|
||||
get() = meta.name
|
||||
val title get() = meta.name
|
||||
|
||||
/**
|
||||
* The string used as the sub-title, we currently use the author
|
||||
*/
|
||||
val subTitle : String?
|
||||
get() = meta.author
|
||||
val subTitle get() = meta.author
|
||||
|
||||
/**
|
||||
* The URI of the application's image file
|
||||
*/
|
||||
val uri : Uri
|
||||
get() = meta.uri
|
||||
val uri get() = meta.uri
|
||||
|
||||
/**
|
||||
* The format of the application ROM as a string
|
||||
*/
|
||||
private val type : String
|
||||
get() = meta.format.name
|
||||
private val type get() = meta.format.name
|
||||
|
||||
val loaderResult get() = meta.loaderResult
|
||||
|
||||
@ -63,7 +56,5 @@ class AppItem(val meta : AppEntry) : BaseItem() {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
override fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
|
||||
}
|
||||
|
15
app/src/main/java/emu/skyline/data/BaseElement.kt
Normal file
15
app/src/main/java/emu/skyline/data/BaseElement.kt
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
enum class ElementType {
|
||||
Header,
|
||||
Item
|
||||
}
|
||||
|
||||
abstract class BaseElement(elementType : ElementType) : Serializable
|
8
app/src/main/java/emu/skyline/data/BaseHeader.kt
Normal file
8
app/src/main/java/emu/skyline/data/BaseHeader.kt
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
class BaseHeader(val title : String) : BaseElement(ElementType.Header)
|
@ -5,9 +5,6 @@
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import emu.skyline.adapter.BaseElement
|
||||
import emu.skyline.adapter.ElementType
|
||||
|
||||
/**
|
||||
* This is an abstract class that all adapter item classes inherit from
|
||||
*/
|
||||
@ -15,5 +12,5 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||
/**
|
||||
* This function returns a string used for searching
|
||||
*/
|
||||
open fun key() : String? = null
|
||||
open fun key() : String = ""
|
||||
}
|
||||
|
@ -5,16 +5,20 @@
|
||||
|
||||
package emu.skyline.input
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.*
|
||||
import emu.skyline.adapter.GenericAdapter
|
||||
import emu.skyline.adapter.HeaderViewItem
|
||||
import emu.skyline.adapter.controller.*
|
||||
import emu.skyline.input.dialog.ButtonDialog
|
||||
import emu.skyline.input.dialog.RumbleDialog
|
||||
import emu.skyline.input.dialog.StickDialog
|
||||
import emu.skyline.input.onscreen.OnScreenEditActivity
|
||||
import kotlinx.android.synthetic.main.controller_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
@ -30,43 +34,54 @@ class ControllerActivity : AppCompatActivity() {
|
||||
/**
|
||||
* The adapter used by [controller_list] to hold all the items
|
||||
*/
|
||||
private val adapter = ControllerAdapter(::onControllerItemClick)
|
||||
private val adapter = GenericAdapter()
|
||||
|
||||
/**
|
||||
* This is a map between a button and it's corresponding [ControllerItem] in [adapter]
|
||||
*/
|
||||
val buttonMap = mutableMapOf<ButtonId, ControllerItem>()
|
||||
val buttonMap = mutableMapOf<ButtonId, ControllerViewItem>()
|
||||
|
||||
/**
|
||||
* This is a map between an axis and it's corresponding [ControllerStickItem] in [adapter]
|
||||
* This is a map between an axis and it's corresponding [ControllerStickViewItem] in [adapter]
|
||||
*/
|
||||
val axisMap = mutableMapOf<AxisId, ControllerStickItem>()
|
||||
val axisMap = mutableMapOf<AxisId, ControllerStickViewItem>()
|
||||
|
||||
/**
|
||||
* This function updates the [adapter] based on information from [InputManager]
|
||||
*/
|
||||
private fun update() {
|
||||
adapter.clear()
|
||||
adapter.removeAllItems()
|
||||
|
||||
val controller = InputManager.controllers[id]!!
|
||||
|
||||
adapter.addItem(ControllerTypeItem(this, controller.type))
|
||||
adapter.addItem(ControllerTypeViewItem(controller.type, onControllerTypeClick))
|
||||
|
||||
if (controller.type == ControllerType.None)
|
||||
return
|
||||
|
||||
if (id == 0 && controller.type.firstController) {
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.osc)))
|
||||
|
||||
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), getString(R.string.osc_not_shown), false) { item, position ->
|
||||
item.summary = getString(if (item.checked) R.string.osc_shown else R.string.osc_not_shown)
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
|
||||
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
|
||||
startActivity(Intent(this, OnScreenEditActivity::class.java))
|
||||
}))
|
||||
}
|
||||
|
||||
var wroteTitle = false
|
||||
|
||||
for (item in GeneralType.values()) {
|
||||
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.general))
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.general)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
adapter.addItem(ControllerGeneralItem(this, item))
|
||||
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,11 +89,11 @@ class ControllerActivity : AppCompatActivity() {
|
||||
|
||||
for (stick in controller.type.sticks) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.sticks))
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val stickItem = ControllerStickItem(this, stick)
|
||||
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
|
||||
|
||||
adapter.addItem(stickItem)
|
||||
buttonMap[stick.button] = stickItem
|
||||
@ -98,11 +113,11 @@ class ControllerActivity : AppCompatActivity() {
|
||||
|
||||
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(buttonArray.first))
|
||||
adapter.addItem(HeaderViewItem(getString(buttonArray.first)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val buttonItem = ControllerButtonItem(this, button)
|
||||
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
||||
|
||||
adapter.addItem(buttonItem)
|
||||
buttonMap[button] = buttonItem
|
||||
@ -113,11 +128,11 @@ class ControllerActivity : AppCompatActivity() {
|
||||
|
||||
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addHeader(getString(R.string.misc_buttons))
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.misc_buttons)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val buttonItem = ControllerButtonItem(this, button)
|
||||
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
||||
|
||||
adapter.addItem(buttonItem)
|
||||
buttonMap[button] = buttonItem
|
||||
@ -154,83 +169,81 @@ class ControllerActivity : AppCompatActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun onControllerItemClick(item : ControllerItem) {
|
||||
when (item) {
|
||||
is ControllerTypeItem -> {
|
||||
val controller = InputManager.controllers[id]!!
|
||||
private val onControllerTypeClick = { item : ControllerTypeViewItem, _ : Int ->
|
||||
val controller = InputManager.controllers[id]!!
|
||||
|
||||
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(item.content)
|
||||
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
|
||||
val selectedType = types[typeIndex]
|
||||
if (controller.type != selectedType) {
|
||||
if (controller is JoyConLeftController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
|
||||
else if (controller is JoyConRightController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
|
||||
|
||||
InputManager.controllers[id] = when (selectedType) {
|
||||
ControllerType.None -> Controller(id, ControllerType.None)
|
||||
ControllerType.HandheldProController -> HandheldController(id)
|
||||
ControllerType.ProController -> ProController(id)
|
||||
ControllerType.JoyConLeft -> JoyConLeftController(id)
|
||||
ControllerType.JoyConRight -> JoyConRightController(id)
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
Unit
|
||||
}
|
||||
|
||||
private val onControllerGeneralClick = { item : ControllerGeneralViewItem, _ : Int ->
|
||||
when (item.type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val controller = InputManager.controllers[id] as JoyConLeftController
|
||||
|
||||
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
||||
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
|
||||
|
||||
val partnerNameIndex = controller.partnerId?.let { partnerId ->
|
||||
rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
|
||||
} ?: 0
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(item.content)
|
||||
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
|
||||
val selectedType = types[typeIndex]
|
||||
if (controller.type != selectedType) {
|
||||
if (controller is JoyConLeftController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
|
||||
else if (controller is JoyConRightController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
|
||||
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
||||
|
||||
InputManager.controllers[id] = when (selectedType) {
|
||||
ControllerType.None -> Controller(id, ControllerType.None)
|
||||
ControllerType.HandheldProController -> HandheldController(id)
|
||||
ControllerType.ProController -> ProController(id)
|
||||
ControllerType.JoyConLeft -> JoyConLeftController(id)
|
||||
ControllerType.JoyConRight -> JoyConRightController(id)
|
||||
}
|
||||
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
|
||||
|
||||
update()
|
||||
}
|
||||
if (controller.partnerId != null)
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
||||
|
||||
item.update()
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
is ControllerGeneralItem -> {
|
||||
when (item.type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val controller = InputManager.controllers[id] as JoyConLeftController
|
||||
|
||||
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
||||
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
|
||||
|
||||
val partnerNameIndex = controller.partnerId?.let { partnerId ->
|
||||
rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
|
||||
} ?: 0
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(item.content)
|
||||
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
||||
|
||||
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
|
||||
|
||||
if (controller.partnerId != null)
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
||||
|
||||
item.update()
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
GeneralType.RumbleDevice -> {
|
||||
RumbleDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ControllerButtonItem -> {
|
||||
ButtonDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
is ControllerStickItem -> {
|
||||
StickDialog(item).show(supportFragmentManager, null)
|
||||
GeneralType.RumbleDevice -> {
|
||||
RumbleDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
private val onControllerButtonClick = { item : ControllerButtonViewItem, _ : Int ->
|
||||
ButtonDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
private val onControllerStickClick = { item : ControllerStickViewItem, _ : Int ->
|
||||
StickDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerButtonItem
|
||||
import emu.skyline.adapter.controller.ControllerButtonViewItem
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.synthetic.main.button_dialog.*
|
||||
import kotlin.math.abs
|
||||
@ -22,9 +22,9 @@ import kotlin.math.abs
|
||||
/**
|
||||
* This dialog is used to set a device to map any buttons
|
||||
*
|
||||
* @param item This is used to hold the [ControllerButtonItem] between instances
|
||||
* @param item This is used to hold the [ControllerButtonViewItem] between instances
|
||||
*/
|
||||
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonItem? = null) : BottomSheetDialogFragment() {
|
||||
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null, private val position : Int? = null) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
|
@ -15,7 +15,7 @@ import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerGeneralItem
|
||||
import emu.skyline.adapter.controller.ControllerGeneralViewItem
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
import kotlinx.android.synthetic.main.rumble_dialog.*
|
||||
@ -23,9 +23,9 @@ import kotlinx.android.synthetic.main.rumble_dialog.*
|
||||
/**
|
||||
* This dialog is used to set a device to pass on any rumble/force feedback data onto
|
||||
*
|
||||
* @param item This is used to hold the [ControllerGeneralItem] between instances
|
||||
* @param item This is used to hold the [ControllerGeneralViewItem] between instances
|
||||
*/
|
||||
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralItem? = null) : BottomSheetDialogFragment() {
|
||||
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewItem? = null) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
|
@ -15,7 +15,7 @@ import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerStickItem
|
||||
import emu.skyline.adapter.controller.ControllerStickViewItem
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.input.MotionHostEvent.Companion.axes
|
||||
import kotlinx.android.synthetic.main.stick_dialog.*
|
||||
@ -26,9 +26,9 @@ import kotlin.math.max
|
||||
/**
|
||||
* This dialog is used to set a device to map any sticks
|
||||
*
|
||||
* @param item This is used to hold the [ControllerStickItem] between instances
|
||||
* @param item This is used to hold the [ControllerStickViewItem] between instances
|
||||
*/
|
||||
class StickDialog @JvmOverloads constructor(val item : ControllerStickItem? = null) : BottomSheetDialogFragment() {
|
||||
class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem? = null) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This enumerates all of the stages this dialog can be in
|
||||
*/
|
||||
|
@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.input.onscreen
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class OnScreenEditActivity : AppCompatActivity() {
|
||||
|
||||
}
|
@ -10,7 +10,10 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
@ -60,12 +63,40 @@ enum class LoaderResult(val value : Int) {
|
||||
/**
|
||||
* This class is used to hold an application's metadata in a serializable way
|
||||
*/
|
||||
class AppEntry(val name : String, val author : String?, val icon : Bitmap?, val format : RomFormat, val uri : Uri, val loaderResult : LoaderResult) : Serializable {
|
||||
class AppEntry(var name : String, var author : String?, var icon : Bitmap?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult) : Serializable {
|
||||
constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
cursor.getString(nameIndex)
|
||||
}!!.dropLast(format.name.length + 1), null, null, format, uri, loaderResult)
|
||||
|
||||
private fun writeObject(output : ObjectOutputStream) {
|
||||
output.writeUTF(name)
|
||||
output.writeObject(format)
|
||||
output.writeUTF(uri.toString())
|
||||
output.writeBoolean(author != null)
|
||||
if (author != null)
|
||||
output.writeUTF(author)
|
||||
output.writeInt(loaderResult.value)
|
||||
output.writeBoolean(icon != null)
|
||||
icon?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
it.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, output)
|
||||
else
|
||||
it.compress(Bitmap.CompressFormat.WEBP, 100, output)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readObject(input : ObjectInputStream) {
|
||||
name = input.readUTF()
|
||||
format = input.readObject() as RomFormat
|
||||
uri = Uri.parse(input.readUTF())
|
||||
if (input.readBoolean())
|
||||
author = input.readUTF()
|
||||
loaderResult = LoaderResult.get(input.readInt())
|
||||
if (input.readBoolean())
|
||||
icon = BitmapFactory.decodeStream(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
52
app/src/main/res/layout/controller_checkbox_item.xml
Normal file
52
app/src/main/res/layout/controller_checkbox_item.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<?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/controller_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
|
||||
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_title"
|
||||
tools:text="Summary" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -8,7 +8,7 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
|
@ -15,5 +15,6 @@
|
||||
android:fastScrollEnabled="true"
|
||||
android:focusedByDefault="true"
|
||||
android:transcriptMode="normal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -52,9 +52,11 @@
|
||||
<string name="import_keys_failed">Failed to import keys</string>
|
||||
<!-- Input -->
|
||||
<string name="input">Input</string>
|
||||
<string name="show_osc">Show On-Screen Controls</string>
|
||||
<string name="osc">On-Screen Controls</string>
|
||||
<string name="osc_enable">Enable On-Screen Controls</string>
|
||||
<string name="osc_not_shown">On-Screen Controls won\'t be shown</string>
|
||||
<string name="osc_shown">On-Screen Controls will be shown</string>
|
||||
<string name="osc_edit">Edit On-Screen Controls layout</string>
|
||||
<string name="controller">Controller</string>
|
||||
<string name="config_controller">Configure Controller</string>
|
||||
<string name="controller_type">Controller Type</string>
|
||||
|
Loading…
Reference in New Issue
Block a user