mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-22 10:29:16 +01:00
Redesign UI
* Change colors * Replace toolbar in main activity * Initial implementation of ViewModel
This commit is contained in:
parent
80ab22a627
commit
9f6a5df5e0
@ -68,6 +68,7 @@ android {
|
|||||||
|
|
||||||
/* NDK */
|
/* NDK */
|
||||||
ndkVersion '22.0.7026061'
|
ndkVersion '22.0.7026061'
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
version '3.18.1+'
|
version '3.18.1+'
|
||||||
@ -85,15 +86,22 @@ dependencies {
|
|||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
|
|
||||||
/* Google */
|
/* Google */
|
||||||
|
def lifecycle_version = "2.2.0"
|
||||||
|
|
||||||
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
|
||||||
/* Kotlin */
|
/* Kotlin */
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
|
|
||||||
/* Other Java */
|
/* Other Java */
|
||||||
implementation 'info.debatty:java-string-similarity:2.0.0'
|
implementation 'info.debatty:java-string-similarity:2.0.0'
|
||||||
|
@ -38,12 +38,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
*/
|
*/
|
||||||
private var vibrators = HashMap<Int, Vibrator>()
|
private var vibrators = HashMap<Int, Vibrator>()
|
||||||
|
|
||||||
/**
|
|
||||||
* The surface object used for displaying frames
|
|
||||||
*/
|
|
||||||
@Volatile
|
|
||||||
private var surface : Surface? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean flag denoting if the emulation thread should call finish() or not
|
* A boolean flag denoting if the emulation thread should call finish() or not
|
||||||
*/
|
*/
|
||||||
@ -262,14 +256,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
vibrators.clear()
|
vibrators.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This sets [surface] to [holder].surface and passes it into libskyline
|
|
||||||
*/
|
|
||||||
override fun surfaceCreated(holder : SurfaceHolder) {
|
override fun surfaceCreated(holder : SurfaceHolder) {
|
||||||
Log.d(Tag, "surfaceCreated Holder: $holder")
|
Log.d(Tag, "surfaceCreated Holder: $holder")
|
||||||
surface = holder.surface
|
|
||||||
while (emulationThread.isAlive)
|
while (emulationThread.isAlive)
|
||||||
if (setSurface(surface))
|
if (setSurface(holder.surface))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,14 +270,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height")
|
Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This sets [surface] to null and passes it into libskyline
|
|
||||||
*/
|
|
||||||
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
||||||
Log.d(Tag, "surfaceDestroyed Holder: $holder")
|
Log.d(Tag, "surfaceDestroyed Holder: $holder")
|
||||||
surface = null
|
|
||||||
while (emulationThread.isAlive)
|
while (emulationThread.isAlive)
|
||||||
if (setSurface(surface))
|
if (setSurface(null))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,14 +32,13 @@ object KeyReader {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
|
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
|
||||||
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() ?: false)
|
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() == true)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
|
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
|
||||||
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val outputStream = tmpOutputFile.bufferedWriter()
|
tmpOutputFile.bufferedWriter().use { writer ->
|
||||||
|
|
||||||
val valid = inputStream!!.bufferedReader().useLines {
|
val valid = inputStream!!.bufferedReader().useLines {
|
||||||
for (line in it) {
|
for (line in it) {
|
||||||
val pair = line.split("=")
|
val pair = line.split("=")
|
||||||
@ -63,23 +62,19 @@ object KeyReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream.append("$key=$value\n")
|
writer.append("$key=$value\n")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream.flush()
|
if (valid) tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
|
||||||
outputStream.close()
|
|
||||||
|
|
||||||
if (valid)
|
|
||||||
tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
|
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isHexString(str : String) : Boolean {
|
private fun isHexString(str : String) : Boolean {
|
||||||
for (c in str)
|
for (c in str)
|
||||||
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'))
|
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) return false
|
||||||
return false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,22 +66,23 @@ class LogActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
|
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
|
||||||
|
|
||||||
logFile.forEachLine { logLine ->
|
adapter.setItems(logFile.readLines().mapNotNull { logLine ->
|
||||||
try {
|
try {
|
||||||
val logMeta = logLine.split("|", limit = 3)
|
val logMeta = logLine.split("|", limit = 3)
|
||||||
|
|
||||||
if (logMeta[0].startsWith("1")) {
|
if (logMeta[0].startsWith("1")) {
|
||||||
val level = logMeta[1].toInt()
|
val level = logMeta[1].toInt()
|
||||||
if (level > logLevel) return@forEachLine
|
if (level > logLevel) return@mapNotNull null
|
||||||
|
|
||||||
adapter.addItem(LogViewItem(compact, "(" + logMeta[2] + ") " + logMeta[3].replace('\\', '\n'), logLevels[level]))
|
return@mapNotNull LogViewItem(compact, "(" + logMeta[2] + ") " + logMeta[3].replace('\\', '\n'), logLevels[level])
|
||||||
} else {
|
} else {
|
||||||
adapter.addItem(HeaderViewItem(logMeta[1]))
|
return@mapNotNull HeaderViewItem(logMeta[1])
|
||||||
}
|
}
|
||||||
} catch (ignored : IndexOutOfBoundsException) {
|
} catch (ignored : IndexOutOfBoundsException) {
|
||||||
} catch (ignored : NumberFormatException) {
|
} catch (ignored : NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
null
|
||||||
|
})
|
||||||
} catch (e : FileNotFoundException) {
|
} catch (e : FileNotFoundException) {
|
||||||
Log.w("Logger", "IO Error during access of log file: " + e.message)
|
Log.w("Logger", "IO Error during access of log file: " + e.message)
|
||||||
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()
|
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()
|
||||||
|
@ -5,22 +5,20 @@
|
|||||||
|
|
||||||
package emu.skyline
|
package emu.skyline
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.graphics.Color
|
||||||
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.res.use
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.core.view.size
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
@ -34,222 +32,137 @@ import emu.skyline.data.AppItem
|
|||||||
import emu.skyline.data.DataItem
|
import emu.skyline.data.DataItem
|
||||||
import emu.skyline.data.HeaderItem
|
import emu.skyline.data.HeaderItem
|
||||||
import emu.skyline.loader.LoaderResult
|
import emu.skyline.loader.LoaderResult
|
||||||
import emu.skyline.loader.RomFile
|
|
||||||
import emu.skyline.loader.RomFormat
|
|
||||||
import emu.skyline.utils.Settings
|
import emu.skyline.utils.Settings
|
||||||
import emu.skyline.utils.loadSerializedList
|
|
||||||
import emu.skyline.utils.serialize
|
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import kotlinx.android.synthetic.main.titlebar.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
companion object {
|
|
||||||
private val TAG = MainActivity::class.java.simpleName
|
|
||||||
}
|
|
||||||
|
|
||||||
private val settings by lazy { Settings(this) }
|
private val settings by lazy { Settings(this) }
|
||||||
|
|
||||||
/**
|
|
||||||
* The adapter used for adding elements to [app_list]
|
|
||||||
*/
|
|
||||||
private val adapter = GenericAdapter()
|
private val adapter = GenericAdapter()
|
||||||
|
|
||||||
private var reloading = AtomicBoolean()
|
|
||||||
|
|
||||||
private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
|
private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
|
||||||
|
|
||||||
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||||
|
|
||||||
|
private val viewModel by viewModels<MainViewModel>()
|
||||||
|
|
||||||
private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog)
|
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, romElements : ArrayList<DataItem>, found : Boolean = false) : Boolean {
|
|
||||||
var foundCurrent = found
|
|
||||||
|
|
||||||
directory.listFiles().forEach { file ->
|
|
||||||
if (file.isDirectory) {
|
|
||||||
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) {
|
|
||||||
romElements.add(HeaderItem(romFormat.name))
|
|
||||||
adapter.addItem(HeaderViewItem(romFormat.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
romElements.add(AppItem(romFile.appEntry).also {
|
|
||||||
adapter.addItem(it.toViewItem())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
foundCurrent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundCurrent
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
|
|
||||||
*
|
|
||||||
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
|
|
||||||
*/
|
|
||||||
private fun refreshAdapter(loadFromFile : Boolean) {
|
|
||||||
val romsFile = File(applicationContext.filesDir.canonicalPath + "/roms.bin")
|
|
||||||
|
|
||||||
if (loadFromFile) {
|
|
||||||
try {
|
|
||||||
loadSerializedList<DataItem>(romsFile).forEach {
|
|
||||||
if (it is HeaderItem)
|
|
||||||
adapter.addItem(HeaderViewItem(it.title))
|
|
||||||
else if (it is AppItem)
|
|
||||||
adapter.addItem(it.toViewItem())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Log.w(TAG, "Ran into exception while loading: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reloading.getAndSet(true)) return
|
|
||||||
thread(start = true) {
|
|
||||||
val snackbar = Snackbar.make(coordinatorLayout, getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE)
|
|
||||||
runOnUiThread {
|
|
||||||
snackbar.show()
|
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
runOnUiThread { adapter.removeAllItems() }
|
|
||||||
|
|
||||||
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(settings.searchLocation))!!
|
|
||||||
|
|
||||||
val romElements = ArrayList<DataItem>()
|
|
||||||
addEntries("nro", RomFormat.NRO, searchLocation, romElements)
|
|
||||||
addEntries("nso", RomFormat.NSO, searchLocation, romElements)
|
|
||||||
addEntries("nca", RomFormat.NCA, searchLocation, romElements)
|
|
||||||
addEntries("xci", RomFormat.XCI, searchLocation, romElements)
|
|
||||||
addEntries("nsp", RomFormat.NSP, searchLocation, romElements)
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
if (romElements.isEmpty()) {
|
|
||||||
romElements.add(HeaderItem(getString(R.string.no_rom)))
|
|
||||||
adapter.addItem(HeaderViewItem(getString(R.string.no_rom)))
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
romElements.serialize(romsFile)
|
|
||||||
} catch (e : IOException) {
|
|
||||||
Log.w(TAG, "Ran into exception while saving: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.refreshRequired = false
|
|
||||||
} catch (e : IllegalArgumentException) {
|
|
||||||
runOnUiThread {
|
|
||||||
settings.searchLocation = ""
|
|
||||||
|
|
||||||
val intent = intent
|
|
||||||
finish()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} catch (e : Exception) {
|
|
||||||
runOnUiThread {
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
snackbar.dismiss()
|
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
reloading.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This initializes [toolbar], [open_fab], [log_fab] and [app_list]
|
|
||||||
*/
|
|
||||||
override fun onCreate(savedInstanceState : Bundle?) {
|
override fun onCreate(savedInstanceState : Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
|
when ((settings.appTheme.toInt())) {
|
||||||
setContentView(R.layout.main_activity)
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
|
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
|
||||||
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when ((settings.appTheme.toInt())) {
|
|
||||||
0 -> AppCompatDelegate.MODE_NIGHT_NO
|
0 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
1 -> AppCompatDelegate.MODE_NIGHT_YES
|
1 -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
|
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
|
||||||
})
|
|
||||||
|
|
||||||
refresh_fab.setOnClickListener { refreshAdapter(false) }
|
|
||||||
|
|
||||||
settings_fab.setOnClickListener { startActivityForResult(Intent(this, SettingsActivity::class.java), 3) }
|
|
||||||
|
|
||||||
open_fab.setOnClickListener {
|
|
||||||
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
}, 2)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
log_fab.setOnClickListener { startActivity(Intent(this, LogActivity::class.java)) }
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.main_activity)
|
||||||
|
|
||||||
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
||||||
|
|
||||||
setupAppList()
|
setupAppList()
|
||||||
|
|
||||||
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
swipe_refresh_layout.apply {
|
||||||
var y = 0
|
setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) })
|
||||||
|
setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) })
|
||||||
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
|
post { setDistanceToTriggerSync(swipe_refresh_layout.height / 3) }
|
||||||
y += dy
|
setOnRefreshListener { loadRoms(false) }
|
||||||
|
|
||||||
if (!app_list.isInTouchMode)
|
|
||||||
toolbar_layout.setExpanded(y == 0)
|
|
||||||
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.state.observe(owner = this, onChanged = ::handleState)
|
||||||
|
loadRoms(!settings.refreshRequired)
|
||||||
|
|
||||||
|
search_bar.apply {
|
||||||
|
setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
|
||||||
|
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
|
||||||
|
setRefreshIconListener { loadRoms(false) }
|
||||||
|
addTextChangedListener(afterTextChanged = { editable ->
|
||||||
|
editable?.let { text -> adapter.filter.filter(text.toString()) }
|
||||||
})
|
})
|
||||||
|
if (!viewModel.searchBarAnimated) {
|
||||||
val controllerFabX = controller_fabs.translationX
|
viewModel.searchBarAnimated = true
|
||||||
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener {
|
post { startTitleAnimation() }
|
||||||
if (!it) {
|
|
||||||
toolbar_layout.setExpanded(false)
|
|
||||||
|
|
||||||
controller_fabs.visibility = View.VISIBLE
|
|
||||||
ObjectAnimator.ofFloat(controller_fabs, "translationX", 0f).apply {
|
|
||||||
duration = 250
|
|
||||||
start()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ObjectAnimator.ofFloat(controller_fabs, "translationX", controllerFabX).apply {
|
|
||||||
duration = 250
|
|
||||||
start()
|
|
||||||
}.doOnEnd { controller_fabs.visibility = View.GONE }
|
|
||||||
}
|
}
|
||||||
|
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
|
||||||
|
search_bar.refreshIconVisible = !isInTouchMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class GridSpacingItemDecoration : RecyclerView.ItemDecoration() {
|
||||||
|
private val padding = resources.getDimensionPixelSize(R.dimen.grid_padding)
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
|
||||||
|
val gridLayoutManager = parent.layoutManager as GridLayoutManager
|
||||||
|
val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams
|
||||||
|
when (layoutParams.spanIndex) {
|
||||||
|
0 -> outRect.left = padding
|
||||||
|
|
||||||
|
gridLayoutManager.spanCount - 1 -> outRect.right = padding
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
outRect.left = padding / 2
|
||||||
|
outRect.right = padding / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutParams.spanSize == gridLayoutManager.spanCount) outRect.right = padding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAppListDecoration() {
|
private fun setAppListDecoration() {
|
||||||
|
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
||||||
when (layoutType) {
|
when (layoutType) {
|
||||||
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||||
|
|
||||||
LayoutType.Grid, LayoutType.GridCompact -> if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
LayoutType.Grid, LayoutType.GridCompact -> app_list.addItemDecoration(GridSpacingItemDecoration())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This layout manager handles situations where [onFocusSearchFailed] gets called, when possible we always want to focus on the item with the same span index
|
||||||
|
*/
|
||||||
|
private inner class CustomLayoutManager(gridSpan : Int) : GridLayoutManager(this, gridSpan) {
|
||||||
|
init {
|
||||||
|
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFocusSearchFailed(focused : View, focusDirection : Int, recycler : RecyclerView.Recycler, state : RecyclerView.State) : View? {
|
||||||
|
val nextFocus = super.onFocusSearchFailed(focused, focusDirection, recycler, state)
|
||||||
|
when (focusDirection) {
|
||||||
|
View.FOCUS_DOWN -> {
|
||||||
|
findContainingItemView(focused)?.let { focusedChild ->
|
||||||
|
val current = app_list.indexOfChild(focusedChild)
|
||||||
|
val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex
|
||||||
|
for (i in current + 1 until app_list.size) {
|
||||||
|
val candidate = getChildAt(i)!!
|
||||||
|
// Return candidate when span index matches
|
||||||
|
if (currentSpanIndex == (candidate.layoutParams as LayoutParams).spanIndex) return candidate
|
||||||
|
}
|
||||||
|
if (nextFocus == null) {
|
||||||
|
app_bar_layout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible
|
||||||
|
app_list.smoothScrollToPosition(adapter.itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
View.FOCUS_UP -> {
|
||||||
|
if (nextFocus?.isFocusable != true) {
|
||||||
|
search_bar.requestFocus()
|
||||||
|
app_bar_layout.setExpanded(true)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextFocus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,11 +173,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val metrics = resources.displayMetrics
|
val metrics = resources.displayMetrics
|
||||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||||
|
|
||||||
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply {
|
app_list.layoutManager = CustomLayoutManager(gridSpan)
|
||||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
|
||||||
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setAppListDecoration()
|
setAppListDecoration()
|
||||||
|
|
||||||
if (settings.searchLocation.isEmpty()) {
|
if (settings.searchLocation.isEmpty()) {
|
||||||
@ -272,35 +181,24 @@ class MainActivity : AppCompatActivity() {
|
|||||||
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
|
||||||
startActivityForResult(intent, 1)
|
startActivityForResult(intent, 1)
|
||||||
} else {
|
|
||||||
refreshAdapter(!settings.refreshRequired)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun handleState(state : MainState) = when (state) {
|
||||||
* This inflates the layout for the menu [R.menu.toolbar_main] and sets up searching the logs
|
MainState.Loading -> {
|
||||||
*/
|
search_bar.animateRefreshIcon()
|
||||||
override fun onCreateOptionsMenu(menu : Menu) : Boolean {
|
swipe_refresh_layout.isRefreshing = true
|
||||||
menuInflater.inflate(R.menu.toolbar_main, menu)
|
|
||||||
|
|
||||||
val searchView = menu.findItem(R.id.action_search_main).actionView as SearchView
|
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query : String) : Boolean {
|
|
||||||
searchView.clearFocus()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
is MainState.Loaded -> {
|
||||||
override fun onQueryTextChange(newText : String) : Boolean {
|
swipe_refresh_layout.isRefreshing = false
|
||||||
adapter.filter.filter(newText)
|
populateAdapter(state.items)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
})
|
is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectStartGame(appItem : AppItem) {
|
private fun selectStartGame(appItem : AppItem) {
|
||||||
|
if (swipe_refresh_layout.isRefreshing) return
|
||||||
|
|
||||||
if (settings.selectAction)
|
if (settings.selectAction)
|
||||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||||
else if (appItem.loaderResult == LoaderResult.Success)
|
else if (appItem.loaderResult == LoaderResult.Success)
|
||||||
@ -308,26 +206,24 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun selectShowGameDialog(appItem : AppItem) {
|
private fun selectShowGameDialog(appItem : AppItem) {
|
||||||
|
if (swipe_refresh_layout.isRefreshing) return
|
||||||
|
|
||||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun loadRoms(loadFromFile : Boolean) {
|
||||||
* This handles menu interaction for [R.id.action_settings] and [R.id.action_refresh]
|
viewModel.loadRoms(this, loadFromFile, Uri.parse(settings.searchLocation))
|
||||||
*/
|
settings.refreshRequired = false
|
||||||
override fun onOptionsItemSelected(item : MenuItem) : Boolean {
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_settings -> {
|
|
||||||
startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_refresh -> {
|
private fun populateAdapter(items : List<DataItem>) {
|
||||||
refreshAdapter(false)
|
adapter.setItems(items.map {
|
||||||
true
|
when (it) {
|
||||||
}
|
is HeaderItem -> HeaderViewItem(it.title)
|
||||||
|
is AppItem -> it.toViewItem()
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
if (items.isEmpty()) adapter.setItems(listOf(HeaderViewItem(getString(R.string.no_rom))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -343,7 +239,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
settings.searchLocation = uri.toString()
|
settings.searchLocation = uri.toString()
|
||||||
|
|
||||||
refreshAdapter(!settings.refreshRequired)
|
loadRoms(!settings.refreshRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
@ -360,9 +256,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
3 -> {
|
3 -> if (settings.refreshRequired) loadRoms(false)
|
||||||
if (settings.refreshRequired) refreshAdapter(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,7 +265,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
var layoutTypeChanged = false
|
var layoutTypeChanged = false
|
||||||
for (appViewItem in adapter.allItems.filterIsInstance(AppViewItem::class.java)) {
|
for (appViewItem in adapter.currentItems.filterIsInstance(AppViewItem::class.java)) {
|
||||||
if (layoutType != appViewItem.layoutType) {
|
if (layoutType != appViewItem.layoutType) {
|
||||||
appViewItem.layoutType = layoutType
|
appViewItem.layoutType = layoutType
|
||||||
layoutTypeChanged = true
|
layoutTypeChanged = true
|
||||||
@ -381,14 +275,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (layoutTypeChanged) {
|
if (layoutTypeChanged) {
|
||||||
adapter.notifyAllItemsChanged()
|
|
||||||
setAppListDecoration()
|
setAppListDecoration()
|
||||||
|
adapter.notifyItemRangeChanged(0, adapter.currentItems.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)
|
override fun onBackPressed() {
|
||||||
when (layoutType) {
|
search_bar.apply {
|
||||||
LayoutType.List -> app_list.post { app_list.setPadding(0, 0, 0, fab_parent.height) }
|
if (hasFocus() && text.isNotEmpty()) {
|
||||||
LayoutType.Grid, LayoutType.GridCompact -> app_list.post { app_list.setPadding(gridCardMagin, 0, gridCardMagin, fab_parent.height) }
|
text = ""
|
||||||
|
clearFocus()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
107
app/src/main/java/emu/skyline/MainViewModel.kt
Normal file
107
app/src/main/java/emu/skyline/MainViewModel.kt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package emu.skyline
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import emu.skyline.data.AppItem
|
||||||
|
import emu.skyline.data.DataItem
|
||||||
|
import emu.skyline.data.HeaderItem
|
||||||
|
import emu.skyline.loader.RomFile
|
||||||
|
import emu.skyline.loader.RomFormat
|
||||||
|
import emu.skyline.utils.loadSerializedList
|
||||||
|
import emu.skyline.utils.serialize
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
sealed class MainState {
|
||||||
|
object Loading : MainState()
|
||||||
|
class Loaded(val items : List<DataItem>) : MainState()
|
||||||
|
class Error(val ex : Exception) : MainState()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainViewModel : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
private val TAG = MainViewModel::class.java.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutableState = MutableLiveData<MainState>()
|
||||||
|
val state : LiveData<MainState> = mutableState
|
||||||
|
|
||||||
|
var searchBarAnimated = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This adds all files in [directory] with [extension] as an entry using [RomFile] to load metadata
|
||||||
|
*/
|
||||||
|
private fun addEntries(context : Context, extension : String, romFormat : RomFormat, directory : DocumentFile, romElements : ArrayList<DataItem>, found : Boolean = false) : Boolean {
|
||||||
|
var foundCurrent = found
|
||||||
|
|
||||||
|
directory.listFiles().forEach { file ->
|
||||||
|
if (file.isDirectory) {
|
||||||
|
foundCurrent = addEntries(context, extension, romFormat, file, romElements, foundCurrent)
|
||||||
|
} else {
|
||||||
|
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||||
|
RomFile(context, romFormat, file.uri).let { romFile ->
|
||||||
|
if (!foundCurrent) romElements.add(HeaderItem(romFormat.name))
|
||||||
|
romElements.add(AppItem(romFile.appEntry))
|
||||||
|
|
||||||
|
foundCurrent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
|
||||||
|
*
|
||||||
|
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
|
||||||
|
*/
|
||||||
|
fun loadRoms(context : Context, loadFromFile : Boolean, searchLocation : Uri) {
|
||||||
|
if (mutableState.value == MainState.Loading) return
|
||||||
|
|
||||||
|
mutableState.postValue(MainState.Loading)
|
||||||
|
|
||||||
|
val romsFile = File(context.filesDir.canonicalPath + "/roms.bin")
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (loadFromFile) {
|
||||||
|
try {
|
||||||
|
mutableState.postValue(MainState.Loaded(loadSerializedList(romsFile)))
|
||||||
|
return@launch
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Log.w(TAG, "Ran into exception while loading: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val searchDocument = DocumentFile.fromTreeUri(context, searchLocation)!!
|
||||||
|
|
||||||
|
val romElements = ArrayList<DataItem>()
|
||||||
|
addEntries(context, "nro", RomFormat.NRO, searchDocument, romElements)
|
||||||
|
addEntries(context, "nso", RomFormat.NSO, searchDocument, romElements)
|
||||||
|
addEntries(context, "nca", RomFormat.NCA, searchDocument, romElements)
|
||||||
|
addEntries(context, "xci", RomFormat.XCI, searchDocument, romElements)
|
||||||
|
addEntries(context, "nsp", RomFormat.NSP, searchDocument, romElements)
|
||||||
|
|
||||||
|
try {
|
||||||
|
romElements.serialize(romsFile)
|
||||||
|
} catch (e : IOException) {
|
||||||
|
Log.w(TAG, "Ran into exception while saving: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableState.postValue(MainState.Loaded(romElements))
|
||||||
|
} catch (e : Exception) {
|
||||||
|
mutableState.postValue(MainState.Error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
import androidx.preference.PreferenceGroup
|
import androidx.preference.PreferenceGroup
|
||||||
import emu.skyline.preference.ActivityResultDelegate
|
import emu.skyline.preference.ActivityResultDelegate
|
||||||
import emu.skyline.preference.DocumentActivity
|
import emu.skyline.preference.DocumentActivity
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
|
||||||
import kotlinx.android.synthetic.main.titlebar.*
|
import kotlinx.android.synthetic.main.titlebar.*
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
@ -46,8 +45,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
|
preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
settings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,4 +98,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
super.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ private data class AppLayoutFactory(private val layoutType : LayoutType) : Gener
|
|||||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false)
|
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() {
|
class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem() {
|
||||||
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType)
|
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType)
|
||||||
|
|
||||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||||
@ -48,10 +48,7 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
|
|||||||
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
|
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (layoutType) {
|
holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
|
||||||
LayoutType.List -> holder.itemView
|
|
||||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
|
|
||||||
}.apply {
|
|
||||||
setOnClickListener { onClick.invoke(item) }
|
setOnClickListener { onClick.invoke(item) }
|
||||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||||
}
|
}
|
||||||
@ -70,4 +67,8 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun key() = item.key()
|
override fun key() = item.key()
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = key() == other.key()
|
||||||
|
|
||||||
|
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item
|
||||||
}
|
}
|
||||||
|
@ -8,20 +8,30 @@ package emu.skyline.adapter
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Filter
|
import android.widget.Filter
|
||||||
import android.widget.Filterable
|
import android.widget.Filterable
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import info.debatty.java.stringsimilarity.Cosine
|
import info.debatty.java.stringsimilarity.Cosine
|
||||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can handle any view types with [GenericViewHolderBinder] implemented, [GenericViewHolderBinder] are differentiated by the return value of [GenericViewHolderBinder.getLayoutFactory]
|
* Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getLayoutFactory]
|
||||||
*/
|
*/
|
||||||
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||||
var currentSearchTerm = ""
|
companion object {
|
||||||
|
private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areItemsTheSame(newItem)
|
||||||
|
|
||||||
val currentItems get() = if (currentSearchTerm.isEmpty()) allItems else filteredItems
|
override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem)
|
||||||
val allItems = mutableListOf<GenericViewHolderBinder>()
|
}
|
||||||
private var filteredItems = listOf<GenericViewHolderBinder>()
|
}
|
||||||
|
|
||||||
|
private val asyncListDiffer = AsyncListDiffer(this, DIFFER)
|
||||||
|
private val allItems = mutableListOf<GenericListItem>()
|
||||||
|
val currentItems : List<GenericListItem> get() = asyncListDiffer.currentList
|
||||||
|
|
||||||
|
var currentSearchTerm = ""
|
||||||
|
|
||||||
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
|
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
|
||||||
|
|
||||||
@ -38,19 +48,10 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
|||||||
|
|
||||||
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
|
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
|
||||||
|
|
||||||
fun addItem(item : GenericViewHolderBinder) {
|
fun setItems(items : List<GenericListItem>) {
|
||||||
allItems.add(item)
|
|
||||||
notifyItemInserted(currentItems.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllItems() {
|
|
||||||
val size = currentItems.size
|
|
||||||
allItems.clear()
|
allItems.clear()
|
||||||
notifyItemRangeRemoved(0, size)
|
allItems.addAll(items)
|
||||||
}
|
filter.filter(currentSearchTerm)
|
||||||
|
|
||||||
fun notifyAllItemsChanged() {
|
|
||||||
notifyItemRangeChanged(0, currentItems.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +68,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
|||||||
*/
|
*/
|
||||||
private val cos = Cosine()
|
private val cos = Cosine()
|
||||||
|
|
||||||
inner class ScoredItem(val score : Double, val item : GenericViewHolderBinder)
|
inner class ScoredItem(val score : Double, val item : GenericListItem)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
|
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
|
||||||
@ -92,7 +93,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
|||||||
results.values = allItems.toMutableList()
|
results.values = allItems.toMutableList()
|
||||||
results.count = allItems.size
|
results.count = allItems.size
|
||||||
} else {
|
} else {
|
||||||
val filterData = mutableListOf<GenericViewHolderBinder>()
|
val filterData = mutableListOf<GenericListItem>()
|
||||||
|
|
||||||
val topResults = extractSorted()
|
val topResults = extractSorted()
|
||||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||||
@ -111,9 +112,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
|||||||
*/
|
*/
|
||||||
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
filteredItems = results.values as List<GenericViewHolderBinder>
|
asyncListDiffer.submitList(results.values as List<GenericListItem>)
|
||||||
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ interface GenericLayoutFactory {
|
|||||||
fun createLayout(parent : ViewGroup) : View
|
fun createLayout(parent : ViewGroup) : View
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenericViewHolderBinder {
|
abstract class GenericListItem {
|
||||||
var adapter : GenericAdapter? = null
|
var adapter : GenericAdapter? = null
|
||||||
|
|
||||||
abstract fun getLayoutFactory() : GenericLayoutFactory
|
abstract fun getLayoutFactory() : GenericLayoutFactory
|
||||||
@ -27,4 +27,11 @@ abstract class GenericViewHolderBinder {
|
|||||||
* Used for filtering
|
* Used for filtering
|
||||||
*/
|
*/
|
||||||
open fun key() : String = ""
|
open fun key() : String = ""
|
||||||
|
|
||||||
|
open fun areItemsTheSame(other : GenericListItem) = this == other
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will only be called when [areItemsTheSame] returns true, thus returning true by default
|
||||||
|
*/
|
||||||
|
open fun areContentsTheSame(other : GenericListItem) = true
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ private object HeaderLayoutFactory : GenericLayoutFactory {
|
|||||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
|
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
|
class HeaderViewItem(private val text : String) : GenericListItem() {
|
||||||
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
|
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
|
||||||
|
|
||||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||||
@ -23,4 +23,6 @@ class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = ""
|
override fun toString() = ""
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ private data class LogLayoutFactory(private val compact : Boolean) : GenericLayo
|
|||||||
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)
|
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() {
|
data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem() {
|
||||||
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
|
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
|
||||||
|
|
||||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package emu.skyline.adapter.controller
|
package emu.skyline.adapter.controller
|
||||||
|
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.input.ButtonGuestEvent
|
import emu.skyline.input.ButtonGuestEvent
|
||||||
import emu.skyline.input.ButtonId
|
import emu.skyline.input.ButtonId
|
||||||
@ -23,4 +24,8 @@ class ControllerButtonViewItem(private val controllerId : Int, val button : Butt
|
|||||||
|
|
||||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,14 @@ import androidx.core.view.isGone
|
|||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.adapter.GenericLayoutFactory
|
import emu.skyline.adapter.GenericLayoutFactory
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.adapter.GenericViewHolderBinder
|
import emu.skyline.adapter.GenericListItem
|
||||||
import kotlinx.android.synthetic.main.controller_checkbox_item.*
|
import kotlinx.android.synthetic.main.controller_checkbox_item.*
|
||||||
|
|
||||||
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
|
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
|
||||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
|
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() {
|
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericListItem() {
|
||||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
|
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
|
||||||
|
|
||||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package emu.skyline.adapter.controller
|
package emu.skyline.adapter.controller
|
||||||
|
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.input.GeneralType
|
import emu.skyline.input.GeneralType
|
||||||
import emu.skyline.input.InputManager
|
import emu.skyline.input.InputManager
|
||||||
@ -38,4 +39,8 @@ class ControllerGeneralViewItem(private val controllerId : Int, val type : Gener
|
|||||||
|
|
||||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && controllerId == other.controllerId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package emu.skyline.adapter.controller
|
package emu.skyline.adapter.controller
|
||||||
|
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.input.AxisGuestEvent
|
import emu.skyline.input.AxisGuestEvent
|
||||||
import emu.skyline.input.ButtonGuestEvent
|
import emu.skyline.input.ButtonGuestEvent
|
||||||
@ -40,4 +41,8 @@ class ControllerStickViewItem(private val controllerId : Int, val stick : StickI
|
|||||||
|
|
||||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = other is ControllerStickViewItem && controllerId == other.controllerId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package emu.skyline.adapter.controller
|
package emu.skyline.adapter.controller
|
||||||
|
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.input.ControllerType
|
import emu.skyline.input.ControllerType
|
||||||
|
|
||||||
@ -23,4 +24,8 @@ class ControllerTypeViewItem(private val type : ControllerType, private val onCl
|
|||||||
|
|
||||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other : GenericListItem) = other is ControllerTypeViewItem
|
||||||
|
|
||||||
|
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,15 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.adapter.GenericLayoutFactory
|
import emu.skyline.adapter.GenericLayoutFactory
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.GenericViewHolder
|
import emu.skyline.adapter.GenericViewHolder
|
||||||
import emu.skyline.adapter.GenericViewHolderBinder
|
|
||||||
import kotlinx.android.synthetic.main.controller_item.*
|
import kotlinx.android.synthetic.main.controller_item.*
|
||||||
|
|
||||||
private object ControllerLayoutFactory : GenericLayoutFactory {
|
private object ControllerLayoutFactory : GenericLayoutFactory {
|
||||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
|
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() {
|
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem() {
|
||||||
private var position = -1
|
private var position = -1
|
||||||
|
|
||||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
|
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
|
||||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package emu.skyline.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import emu.skyline.R
|
|
||||||
import emu.skyline.loader.AppEntry
|
|
||||||
import emu.skyline.loader.LoaderResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
|
||||||
*/
|
|
||||||
class AppItem(private val meta : AppEntry) : DataItem {
|
|
||||||
/**
|
|
||||||
* The icon of the application
|
|
||||||
*/
|
|
||||||
val icon get() = meta.icon
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The title of the application
|
|
||||||
*/
|
|
||||||
val title get() = meta.name
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The string used as the sub-title, we currently use the author
|
|
||||||
*/
|
|
||||||
val subTitle get() = meta.author
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URI of the application's image file
|
|
||||||
*/
|
|
||||||
val uri get() = meta.uri
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The format of the application ROM as a string
|
|
||||||
*/
|
|
||||||
private val type get() = meta.format.name
|
|
||||||
|
|
||||||
val loaderResult get() = meta.loaderResult
|
|
||||||
|
|
||||||
fun loaderResultString(context : Context) = context.getString(when (meta.loaderResult) {
|
|
||||||
LoaderResult.Success -> R.string.metadata_missing
|
|
||||||
|
|
||||||
LoaderResult.ParsingError -> R.string.invalid_file
|
|
||||||
|
|
||||||
LoaderResult.MissingTitleKey -> R.string.missing_title_key
|
|
||||||
|
|
||||||
LoaderResult.MissingHeaderKey,
|
|
||||||
LoaderResult.MissingTitleKek,
|
|
||||||
LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name and author is used as the key
|
|
||||||
*/
|
|
||||||
fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
|
|
||||||
}
|
|
@ -5,6 +5,56 @@
|
|||||||
|
|
||||||
package emu.skyline.data
|
package emu.skyline.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import emu.skyline.R
|
||||||
|
import emu.skyline.loader.AppEntry
|
||||||
|
import emu.skyline.loader.LoaderResult
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface DataItem : Serializable
|
sealed class DataItem : Serializable
|
||||||
|
|
||||||
|
class HeaderItem(val title : String) : DataItem()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
||||||
|
*/
|
||||||
|
data class AppItem(private val meta : AppEntry) : DataItem() {
|
||||||
|
/**
|
||||||
|
* The icon of the application
|
||||||
|
*/
|
||||||
|
val icon get() = meta.icon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the application
|
||||||
|
*/
|
||||||
|
val title get() = meta.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string used as the sub-title, we currently use the author
|
||||||
|
*/
|
||||||
|
val subTitle get() = meta.author
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI of the application's image file
|
||||||
|
*/
|
||||||
|
val uri get() = meta.uri
|
||||||
|
|
||||||
|
val loaderResult get() = meta.loaderResult
|
||||||
|
|
||||||
|
fun loaderResultString(context : Context) = context.getString(when (meta.loaderResult) {
|
||||||
|
LoaderResult.Success -> R.string.metadata_missing
|
||||||
|
|
||||||
|
LoaderResult.ParsingError -> R.string.invalid_file
|
||||||
|
|
||||||
|
LoaderResult.MissingTitleKey -> R.string.missing_title_key
|
||||||
|
|
||||||
|
LoaderResult.MissingHeaderKey,
|
||||||
|
LoaderResult.MissingTitleKek,
|
||||||
|
LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and author is used as the key
|
||||||
|
*/
|
||||||
|
fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
|
||||||
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
|
||||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package emu.skyline.data
|
|
||||||
|
|
||||||
class HeaderItem(val title : String) : DataItem
|
|
@ -10,9 +10,11 @@ import android.os.Bundle
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.adapter.GenericAdapter
|
import emu.skyline.adapter.GenericAdapter
|
||||||
|
import emu.skyline.adapter.GenericListItem
|
||||||
import emu.skyline.adapter.HeaderViewItem
|
import emu.skyline.adapter.HeaderViewItem
|
||||||
import emu.skyline.adapter.controller.*
|
import emu.skyline.adapter.controller.*
|
||||||
import emu.skyline.input.dialog.ButtonDialog
|
import emu.skyline.input.dialog.ButtonDialog
|
||||||
@ -53,31 +55,32 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
* This function updates the [adapter] based on information from [InputManager]
|
* This function updates the [adapter] based on information from [InputManager]
|
||||||
*/
|
*/
|
||||||
private fun update() {
|
private fun update() {
|
||||||
adapter.removeAllItems()
|
val items = mutableListOf<GenericListItem>()
|
||||||
|
|
||||||
|
try {
|
||||||
val controller = InputManager.controllers[id]!!
|
val controller = InputManager.controllers[id]!!
|
||||||
|
|
||||||
adapter.addItem(ControllerTypeViewItem(controller.type, onControllerTypeClick))
|
items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick))
|
||||||
|
|
||||||
if (controller.type == ControllerType.None)
|
if (controller.type == ControllerType.None)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (id == 0 && controller.type.firstController) {
|
if (id == 0 && controller.type.firstController) {
|
||||||
adapter.addItem(HeaderViewItem(getString(R.string.osc)))
|
items.add(HeaderViewItem(getString(R.string.osc)))
|
||||||
|
|
||||||
val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
|
val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
|
||||||
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
|
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
|
||||||
item.summary = oscSummary.invoke(item.checked)
|
item.summary = oscSummary.invoke(item.checked)
|
||||||
settings.onScreenControl = item.checked
|
settings.onScreenControl = item.checked
|
||||||
adapter.notifyItemChanged(position)
|
adapter.notifyItemChanged(position)
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
|
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
|
||||||
settings.onScreenControlRecenterSticks = item.checked
|
settings.onScreenControlRecenterSticks = item.checked
|
||||||
adapter.notifyItemChanged(position)
|
adapter.notifyItemChanged(position)
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
|
items.add(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
|
||||||
startActivity(Intent(this, OnScreenEditActivity::class.java))
|
startActivity(Intent(this, OnScreenEditActivity::class.java))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -87,11 +90,11 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
for (item in GeneralType.values()) {
|
for (item in GeneralType.values()) {
|
||||||
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
|
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
|
||||||
if (!wroteTitle) {
|
if (!wroteTitle) {
|
||||||
adapter.addItem(HeaderViewItem(getString(R.string.general)))
|
items.add(HeaderViewItem(getString(R.string.general)))
|
||||||
wroteTitle = true
|
wroteTitle = true
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
|
items.add(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,13 +102,13 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
for (stick in controller.type.sticks) {
|
for (stick in controller.type.sticks) {
|
||||||
if (!wroteTitle) {
|
if (!wroteTitle) {
|
||||||
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
|
items.add(HeaderViewItem(getString(R.string.sticks)))
|
||||||
wroteTitle = true
|
wroteTitle = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
|
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
|
||||||
|
|
||||||
adapter.addItem(stickItem)
|
items.add(stickItem)
|
||||||
buttonMap[stick.button] = stickItem
|
buttonMap[stick.button] = stickItem
|
||||||
axisMap[stick.xAxis] = stickItem
|
axisMap[stick.xAxis] = stickItem
|
||||||
axisMap[stick.yAxis] = stickItem
|
axisMap[stick.yAxis] = stickItem
|
||||||
@ -123,13 +126,13 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
|
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
|
||||||
if (!wroteTitle) {
|
if (!wroteTitle) {
|
||||||
adapter.addItem(HeaderViewItem(getString(buttonArray.first)))
|
items.add(HeaderViewItem(getString(buttonArray.first)))
|
||||||
wroteTitle = true
|
wroteTitle = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
||||||
|
|
||||||
adapter.addItem(buttonItem)
|
items.add(buttonItem)
|
||||||
buttonMap[button] = buttonItem
|
buttonMap[button] = buttonItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,15 +141,18 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
|
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
|
||||||
if (!wroteTitle) {
|
if (!wroteTitle) {
|
||||||
adapter.addItem(HeaderViewItem(getString(R.string.misc_buttons)))
|
items.add(HeaderViewItem(getString(R.string.misc_buttons)))
|
||||||
wroteTitle = true
|
wroteTitle = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
||||||
|
|
||||||
adapter.addItem(buttonItem)
|
items.add(buttonItem)
|
||||||
buttonMap[button] = buttonItem
|
buttonMap[button] = buttonItem
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
adapter.setItems(items)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,9 +171,18 @@ class ControllerActivity : AppCompatActivity() {
|
|||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
controller_list.layoutManager = LinearLayoutManager(this)
|
val layoutManager = LinearLayoutManager(this)
|
||||||
|
controller_list.layoutManager = layoutManager
|
||||||
controller_list.adapter = adapter
|
controller_list.adapter = adapter
|
||||||
|
|
||||||
|
controller_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
|
if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) app_bar_layout.setExpanded(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
package emu.skyline.input.onscreen
|
package emu.skyline.input.onscreen
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
@ -17,7 +17,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.utils.Settings
|
import emu.skyline.utils.Settings
|
||||||
import kotlinx.android.synthetic.main.main_activity.fab_parent
|
|
||||||
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
|
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
|
||||||
|
|
||||||
class OnScreenEditActivity : AppCompatActivity() {
|
class OnScreenEditActivity : AppCompatActivity() {
|
||||||
@ -87,10 +86,8 @@ class OnScreenEditActivity : AppCompatActivity() {
|
|||||||
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
|
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
|
||||||
|
|
||||||
actions.forEach { pair ->
|
actions.forEach { pair ->
|
||||||
fab_parent.addView(FloatingActionButton(this).apply {
|
fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply {
|
||||||
size = FloatingActionButton.SIZE_MINI
|
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
|
||||||
setColorFilter(Color.WHITE)
|
|
||||||
setImageDrawable(ContextCompat.getDrawable(context, pair.first))
|
|
||||||
setOnClickListener { pair.second.invoke() }
|
setOnClickListener { pair.second.invoke() }
|
||||||
fabMapping[pair.first] = this
|
fabMapping[pair.first] = this
|
||||||
})
|
})
|
||||||
|
@ -63,7 +63,7 @@ enum class LoaderResult(val value : Int) {
|
|||||||
/**
|
/**
|
||||||
* This class is used to hold an application's metadata in a serializable way
|
* This class is used to hold an application's metadata in a serializable way
|
||||||
*/
|
*/
|
||||||
class AppEntry(var name : String, var author : String?, var icon : Bitmap?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult) : Serializable {
|
data 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 ->
|
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)
|
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
@ -10,6 +10,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import emu.skyline.utils.Settings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This activity is used to launch a document picker and saves the result to preferences
|
* This activity is used to launch a document picker and saves the result to preferences
|
||||||
@ -45,9 +46,9 @@ abstract class DocumentActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
Settings(this).refreshRequired = true
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString(keyName, uri.toString())
|
.putString(keyName, uri.toString())
|
||||||
.putBoolean("refresh_required", true)
|
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
|
||||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package emu.skyline.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom linear layout with support for [CoordinatorLayout] to move children, when [com.google.android.material.snackbar.Snackbar] shows up
|
|
||||||
*/
|
|
||||||
class CustomLinearLayout : LinearLayout, CoordinatorLayout.AttachedBehavior {
|
|
||||||
constructor(context : Context) : this(context, null)
|
|
||||||
constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, 0)
|
|
||||||
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr)
|
|
||||||
|
|
||||||
override fun getBehavior() : CoordinatorLayout.Behavior<CustomLinearLayout> = MoveUpwardBehavior()
|
|
||||||
|
|
||||||
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect): Boolean = getChildAt(if (direction == View.FOCUS_UP) childCount - 1 else 0 )?.requestFocus() ?: false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines behaviour when [com.google.android.material.snackbar.Snackbar] is shown
|
|
||||||
* Simply sets an offset to y translation to move children out of the way
|
|
||||||
*/
|
|
||||||
class MoveUpwardBehavior : CoordinatorLayout.Behavior<CustomLinearLayout>() {
|
|
||||||
override fun layoutDependsOn(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) : Boolean = dependency is SnackbarLayout
|
|
||||||
|
|
||||||
override fun onDependentViewChanged(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) : Boolean {
|
|
||||||
child.translationY = (0f).coerceAtMost(dependency.translationY - dependency.height)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDependentViewRemoved(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) {
|
|
||||||
child.translationY = 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
104
app/src/main/java/emu/skyline/views/SearchBarView.kt
Normal file
104
app/src/main/java/emu/skyline/views/SearchBarView.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package emu.skyline.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.core.view.MarginLayoutParamsCompat
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.google.android.material.card.MaterialCardView
|
||||||
|
import emu.skyline.R
|
||||||
|
import kotlinx.android.synthetic.main.view_search_bar.view.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class SearchBarView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = com.google.android.material.R.attr.materialCardViewStyle) : MaterialCardView(context, attrs, defStyleAttr) {
|
||||||
|
init {
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.view_search_bar, this)
|
||||||
|
useCompatPadding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics).roundToInt()
|
||||||
|
MarginLayoutParamsCompat.setMarginStart(layoutParams as MarginLayoutParams?, margin)
|
||||||
|
MarginLayoutParamsCompat.setMarginEnd(layoutParams as MarginLayoutParams?, margin)
|
||||||
|
|
||||||
|
radius = margin / 2f
|
||||||
|
cardElevation = radius / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRefreshIconListener(listener : OnClickListener) = refresh_icon.setOnClickListener(listener)
|
||||||
|
fun setLogIconListener(listener : OnClickListener) = log_icon.setOnClickListener(listener)
|
||||||
|
fun setSettingsIconListener(listener : OnClickListener) = settings_icon.setOnClickListener(listener)
|
||||||
|
|
||||||
|
var refreshIconVisible = false
|
||||||
|
set(visible) {
|
||||||
|
field = visible
|
||||||
|
refresh_icon.apply {
|
||||||
|
if (visible != isVisible) {
|
||||||
|
refresh_icon.alpha = if (visible) 0f else 1f
|
||||||
|
animate().alpha(if (visible) 1f else 0f).withStartAction { isVisible = true }.withEndAction { isInvisible = !visible }.apply { duration = 500 }.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text : CharSequence
|
||||||
|
get() = search_field.text
|
||||||
|
set(value) = search_field.setText(value)
|
||||||
|
|
||||||
|
fun startTitleAnimation() {
|
||||||
|
motion_layout.progress = 0f
|
||||||
|
motion_layout.transitionToEnd()
|
||||||
|
search_field.apply {
|
||||||
|
setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
this@SearchBarView.motion_layout.progress = 1f
|
||||||
|
context.getSystemService(InputMethodManager::class.java).showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
onFocusChangeListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun animateRefreshIcon() {
|
||||||
|
refresh_icon.animate().rotationBy(-180f)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun addTextChangedListener(
|
||||||
|
crossinline beforeTextChanged : (
|
||||||
|
text : CharSequence?,
|
||||||
|
start : Int,
|
||||||
|
count : Int,
|
||||||
|
after : Int
|
||||||
|
) -> Unit = { _, _, _, _ -> },
|
||||||
|
crossinline onTextChanged : (
|
||||||
|
text : CharSequence?,
|
||||||
|
start : Int,
|
||||||
|
before : Int,
|
||||||
|
count : Int
|
||||||
|
) -> Unit = { _, _, _, _ -> },
|
||||||
|
crossinline afterTextChanged : (text : Editable?) -> Unit = {}
|
||||||
|
) : TextWatcher {
|
||||||
|
val textWatcher = object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s : Editable?) {
|
||||||
|
afterTextChanged.invoke(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(text : CharSequence?, start : Int, count : Int, after : Int) {
|
||||||
|
beforeTextChanged.invoke(text, start, count, after)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(text : CharSequence?, start : Int, before : Int, count : Int) {
|
||||||
|
onTextChanged.invoke(text, start, before, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
search_field.addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
|
return textWatcher
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,9 @@
|
|||||||
android:layout_marginEnd="6dp"
|
android:layout_marginEnd="6dp"
|
||||||
android:focusedByDefault="true"
|
android:focusedByDefault="true"
|
||||||
android:text="@string/play"
|
android:text="@string/play"
|
||||||
app:icon="@drawable/ic_play" />
|
android:textColor="?attr/colorAccent"
|
||||||
|
app:icon="@drawable/ic_play"
|
||||||
|
app:iconTint="?attr/colorAccent" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/game_pin"
|
android:id="@+id/game_pin"
|
||||||
@ -64,8 +66,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="6dp"
|
android:layout_marginStart="6dp"
|
||||||
android:text="@string/pin" />
|
android:text="@string/pin"
|
||||||
|
android:textColor="?attr/colorAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:id="@+id/card_app_item_grid"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_margin="@dimen/app_card_margin_half"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:foreground="?attr/selectableItemBackground"
|
android:foreground="?attr/selectableItemBackground"
|
||||||
app:cardCornerRadius="4dp"
|
android:orientation="vertical">
|
||||||
app:cardElevation="@dimen/app_card_margin"
|
|
||||||
app:cardUseCompatPadding="true">
|
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/item_click_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_marginStart="12dp"
|
||||||
android:orientation="vertical">
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
@ -31,35 +25,31 @@
|
|||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:contentDescription="@string/icon"
|
android:contentDescription="@string/icon"
|
||||||
tools:src="@drawable/default_icon" />
|
tools:src="@drawable/default_icon" />
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:paddingStart="15dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingEnd="15dp"
|
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
android:textSize="12sp"
|
||||||
tools:text="Title" />
|
tools:text="Title" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_subtitle"
|
android:id="@+id/text_subtitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:paddingStart="15dp"
|
|
||||||
android:paddingEnd="15dp"
|
|
||||||
android:paddingBottom="15dp"
|
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textSize="12sp"
|
||||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
|
||||||
android:textColor="@android:color/tertiary_text_light"
|
|
||||||
tools:text="Subtitle" />
|
tools:text="Subtitle" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</FrameLayout>
|
|
||||||
|
@ -1,22 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/item_click_layout"
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:id="@+id/card_app_item_grid"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_margin="@dimen/app_card_margin_half"
|
android:layout_marginTop="12dp"
|
||||||
android:clickable="true"
|
android:layout_marginEnd="12dp"
|
||||||
android:focusable="true"
|
android:layout_marginBottom="12dp"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
app:cardCornerRadius="16dp"
|
||||||
app:cardCornerRadius="4dp"
|
app:cardElevation="4dp">
|
||||||
app:cardElevation="@dimen/app_card_margin"
|
|
||||||
app:cardUseCompatPadding="true">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -73,5 +67,4 @@
|
|||||||
app:layout_constraintStart_toStartOf="@id/icon"
|
app:layout_constraintStart_toStartOf="@id/icon"
|
||||||
tools:text="Subtitle" />
|
tools:text="Subtitle" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
</FrameLayout>
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/item_click_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
@ -14,6 +15,7 @@
|
|||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:contentDescription="@string/icon"
|
android:contentDescription="@string/icon"
|
||||||
|
android:focusable="false"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@drawable/default_icon" />
|
tools:src="@drawable/default_icon" />
|
||||||
|
@ -76,6 +76,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:text="@string/reset" />
|
android:text="@string/reset"
|
||||||
|
android:textColor="?attr/colorAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -2,76 +2,45 @@
|
|||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/coordinatorLayout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<include layout="@layout/titlebar" />
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:keyboardNavigationCluster="false"
|
||||||
|
android:touchscreenBlocksFocus="false"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="62dp"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_scrollFlags="scroll">
|
||||||
|
|
||||||
|
<emu.skyline.views.SearchBarView
|
||||||
|
android:id="@+id/search_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="-4dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/app_list"
|
android:id="@+id/app_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
android:paddingTop="4dp" />
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
<emu.skyline.views.CustomLinearLayout
|
|
||||||
android:id="@+id/fab_parent"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<emu.skyline.views.CustomLinearLayout
|
|
||||||
android:id="@+id/controller_fabs"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:focusable="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:translationX="72dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/refresh_fab"
|
|
||||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:maxImageSize="26dp"
|
|
||||||
app:srcCompat="@drawable/ic_refresh" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/settings_fab"
|
|
||||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:maxImageSize="26dp"
|
|
||||||
app:srcCompat="@drawable/ic_settings" />
|
|
||||||
</emu.skyline.views.CustomLinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/open_fab"
|
|
||||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:maxImageSize="26dp"
|
|
||||||
app:srcCompat="@drawable/ic_open" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/log_fab"
|
|
||||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:maxImageSize="26dp"
|
|
||||||
app:srcCompat="@drawable/ic_log" />
|
|
||||||
</emu.skyline.views.CustomLinearLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<emu.skyline.views.CustomLinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/fab_parent"
|
android:id="@+id/fab_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
10
app/src/main/res/layout/on_screen_edit_mini_fab.xml
Normal file
10
app/src/main/res/layout/on_screen_edit_mini_fab.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:backgroundTint="?attr/colorPrimary"
|
||||||
|
app:fabSize="mini"
|
||||||
|
app:tint="?attr/colorAccent"
|
||||||
|
tools:ignore="ContentDescription" />
|
@ -60,7 +60,6 @@
|
|||||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
android:textColor="@android:color/tertiary_text_light"
|
android:textColor="@android:color/tertiary_text_light"
|
||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -76,6 +75,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:text="@string/builtin"
|
android:text="@string/builtin"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -84,8 +84,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:text="@string/reset" />
|
android:text="@string/reset"
|
||||||
|
android:textColor="?attr/colorAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:textColor="?colorSecondary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textSize="15sp" />
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:text="@string/stick_button"
|
android:text="@string/stick_button"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||||
android:textColor="@color/colorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -116,13 +116,15 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:text="@string/reset" />
|
android:text="@string/reset"
|
||||||
|
android:textColor="?attr/colorAccent" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/stick_next"
|
android:id="@+id/stick_next"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/next" />
|
android:text="@string/next"
|
||||||
|
android:textColor="?attr/colorAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/toolbar_layout"
|
android:id="@+id/app_bar_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
android:keyboardNavigationCluster="false"
|
||||||
app:liftOnScroll="true">
|
android:touchscreenBlocksFocus="false">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
style="@style/Widget.MaterialComponents.Toolbar.Primary"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:theme="@style/AppTheme.ActionBar"
|
app:elevation="16dp"
|
||||||
app:layout_scrollFlags="scroll|enterAlways|snap" />
|
app:layout_scrollFlags="scroll" />
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
107
app/src/main/res/layout/view_search_bar.xml
Normal file
107
app/src/main/res/layout/view_search_bar.xml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="com.google.android.material.card.MaterialCardView">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
android:id="@+id/motion_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutDescription="@xml/view_search_bar_xml_constraintlayout_scene"
|
||||||
|
app:motionProgress="1"
|
||||||
|
tools:motionDebug="SHOW_ALL">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/search_icon"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:contentDescription="@string/search"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_search"
|
||||||
|
app:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/search_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:focusedByDefault="false"
|
||||||
|
android:hint="@string/search"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="text"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/refresh_icon"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/search_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="LabelFor" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/refresh_icon"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/refresh"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/log_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_refresh"
|
||||||
|
app:tint="?android:attr/textColorSecondary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/log_icon"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/log"
|
||||||
|
android:padding="5dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/settings_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_log"
|
||||||
|
app:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/settings_icon"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/settings"
|
||||||
|
android:padding="5dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_settings"
|
||||||
|
app:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/skyline_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||||
|
</merge>
|
@ -6,15 +6,18 @@
|
|||||||
android:icon="@drawable/ic_search"
|
android:icon="@drawable/ic_search"
|
||||||
android:title="@string/search"
|
android:title="@string/search"
|
||||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:iconTint="?android:attr/textColorSecondary"
|
||||||
app:showAsAction="ifRoom|withText" />
|
app:showAsAction="ifRoom|withText" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_share_log"
|
android:id="@+id/action_share_log"
|
||||||
android:icon="@drawable/ic_share"
|
android:icon="@drawable/ic_share"
|
||||||
android:title="@string/share"
|
android:title="@string/share"
|
||||||
|
app:iconTint="?android:attr/textColorSecondary"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_clear"
|
android:id="@+id/action_clear"
|
||||||
android:icon="@drawable/ic_clear"
|
android:icon="@drawable/ic_clear"
|
||||||
android:title="@string/clear"
|
android:title="@string/clear"
|
||||||
|
app:iconTint="?android:attr/textColorSecondary"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_search_main"
|
|
||||||
android:icon="@drawable/ic_search"
|
|
||||||
android:iconTint="@color/colorOnPrimary"
|
|
||||||
android:title="@string/search"
|
|
||||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
|
||||||
app:showAsAction="ifRoom|withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_settings"
|
|
||||||
android:icon="@drawable/ic_settings"
|
|
||||||
android:title="@string/settings"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_refresh"
|
|
||||||
android:icon="@drawable/ic_refresh"
|
|
||||||
android:title="@string/refresh"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
</menu>
|
|
8
app/src/main/res/values-night-v27/styles.xml
Normal file
8
app/src/main/res/values-night-v27/styles.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="AppTheme" parent="BaseAppTheme">
|
||||||
|
<item name="android:windowLightNavigationBar">false</item>
|
||||||
|
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -1,9 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#FFFF0000</color>
|
<color name="colorPrimary">#FF424242</color>
|
||||||
<color name="colorPrimaryDark">#BFFF0000</color>
|
<color name="colorPrimaryDark">@android:color/black</color>
|
||||||
<color name="colorOnPrimary">#D7000000</color>
|
|
||||||
<color name="colorSecondary">#FFFF0000</color>
|
|
||||||
<color name="colorSecondaryDark">#BFFF0000</color>
|
|
||||||
<color name="colorOnSecondary">#DF000000</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
9
app/src/main/res/values-night/styles.xml
Normal file
9
app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
8
app/src/main/res/values-v27/styles.xml
Normal file
8
app/src/main/res/values-v27/styles.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="AppTheme" parent="BaseAppTheme">
|
||||||
|
<item name="android:windowLightNavigationBar">true</item>
|
||||||
|
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -1,9 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#FFFF0000</color>
|
<color name="colorPrimary">@color/cardview_light_background</color>
|
||||||
<color name="colorPrimaryDark">#BFFF0000</color>
|
<color name="colorPrimaryDark">@android:color/white</color>
|
||||||
<color name="colorOnPrimary">#DFFFFFFF</color>
|
<color name="colorAccent">#FFFF0000</color>
|
||||||
<color name="colorSecondary">#FFFF0000</color>
|
|
||||||
<color name="colorSecondaryDark">#BFFF0000</color>
|
|
||||||
<color name="colorOnSecondary">#DFFFFFFF</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="app_card_margin">8dp</dimen>
|
<dimen name="grid_padding">8dp</dimen>
|
||||||
<dimen name="app_card_margin_half">4dp</dimen>
|
|
||||||
</resources>
|
</resources>
|
@ -1,20 +1,15 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorOnPrimary">@color/colorOnPrimary</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
<item name="colorSecondary">@color/colorSecondary</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
|
||||||
<item name="colorOnSecondary">@color/colorOnSecondary</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.ActionBar" parent="">
|
<style name="AppTheme" parent="BaseAppTheme" />
|
||||||
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
|
|
||||||
<item name="android:textColorSecondary">@color/colorOnPrimary</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="roundedAppImage" parent="">
|
<style name="roundedAppImage">
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
<item name="cornerSize">6dp</item>
|
<item name="cornerSize">6dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:motion="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
motion:autoTransition="none"
|
||||||
|
motion:constraintSetEnd="@+id/end"
|
||||||
|
motion:constraintSetStart="@id/start"
|
||||||
|
motion:duration="2000">
|
||||||
|
<KeyFrameSet>
|
||||||
|
<KeyPosition
|
||||||
|
motion:motionTarget="@+id/skyline_text"
|
||||||
|
motion:framePosition="50"
|
||||||
|
motion:keyPositionType="deltaRelative"
|
||||||
|
motion:percentX="0" />
|
||||||
|
<KeyPosition
|
||||||
|
motion:motionTarget="@+id/skyline_text"
|
||||||
|
motion:framePosition="90"
|
||||||
|
motion:keyPositionType="deltaRelative"
|
||||||
|
motion:percentX="1" />
|
||||||
|
<KeyAttribute
|
||||||
|
motion:motionTarget="@+id/search_field"
|
||||||
|
motion:framePosition="90"
|
||||||
|
android:alpha="0" />
|
||||||
|
<KeyAttribute
|
||||||
|
motion:motionTarget="@+id/skyline_text"
|
||||||
|
motion:framePosition="55"
|
||||||
|
android:scaleY="1"
|
||||||
|
android:scaleX="1" />
|
||||||
|
<KeyPosition
|
||||||
|
motion:motionTarget="@+id/skyline_text"
|
||||||
|
motion:framePosition="70"
|
||||||
|
motion:keyPositionType="deltaRelative"
|
||||||
|
motion:percentX="-0.5" />
|
||||||
|
</KeyFrameSet>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<ConstraintSet android:id="@+id/start">
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/skyline_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
motion:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
motion:layout_constraintEnd_toEndOf="parent"
|
||||||
|
motion:layout_constraintStart_toStartOf="parent"
|
||||||
|
motion:layout_constraintTop_toTopOf="parent" />
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/search_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
motion:layout_constrainedWidth="true"
|
||||||
|
motion:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
motion:layout_constraintEnd_toStartOf="@id/refresh_icon"
|
||||||
|
motion:layout_constraintHorizontal_bias="0"
|
||||||
|
motion:layout_constraintStart_toEndOf="@id/search_icon"
|
||||||
|
motion:layout_constraintTop_toTopOf="parent"
|
||||||
|
motion:transitionEasing="linear"
|
||||||
|
android:alpha="0" />
|
||||||
|
</ConstraintSet>
|
||||||
|
|
||||||
|
<ConstraintSet android:id="@+id/end">
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/skyline_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="invisible"
|
||||||
|
motion:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
motion:layout_constraintEnd_toEndOf="parent"
|
||||||
|
motion:layout_constraintHorizontal_bias="0.0"
|
||||||
|
motion:layout_constraintStart_toStartOf="@id/search_field"
|
||||||
|
motion:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:scaleX="0.5"
|
||||||
|
android:scaleY="0.5" />
|
||||||
|
<Constraint
|
||||||
|
android:id="@+id/search_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:visibility="visible"
|
||||||
|
motion:layout_constrainedWidth="true"
|
||||||
|
motion:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
motion:layout_constraintEnd_toStartOf="@id/refresh_icon"
|
||||||
|
motion:layout_constraintHorizontal_bias="0"
|
||||||
|
motion:layout_constraintStart_toEndOf="@id/search_icon"
|
||||||
|
motion:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:alpha="1" />
|
||||||
|
</ConstraintSet>
|
||||||
|
</MotionScene>
|
Loading…
Reference in New Issue
Block a user