Redesign UI

* Change colors
* Replace toolbar in main activity
* Initial implementation of ViewModel
This commit is contained in:
Willi Ye 2021-01-30 14:59:11 +01:00 committed by ◱ Mark
parent 80ab22a627
commit 9f6a5df5e0
51 changed files with 1014 additions and 772 deletions

View File

@ -68,6 +68,7 @@ android {
/* NDK */
ndkVersion '22.0.7026061'
externalNativeBuild {
cmake {
version '3.18.1+'
@ -85,15 +86,22 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
/* Google */
def lifecycle_version = "2.2.0"
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.3.0'
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 */
implementation "androidx.core:core-ktx:1.3.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
/* Other Java */
implementation 'info.debatty:java-string-similarity:2.0.0'

View File

@ -38,12 +38,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
*/
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
*/
@ -262,14 +256,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
vibrators.clear()
}
/**
* This sets [surface] to [holder].surface and passes it into libskyline
*/
override fun surfaceCreated(holder : SurfaceHolder) {
Log.d(Tag, "surfaceCreated Holder: $holder")
surface = holder.surface
while (emulationThread.isAlive)
if (setSurface(surface))
if (setSurface(holder.surface))
return
}
@ -280,14 +270,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
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) {
Log.d(Tag, "surfaceDestroyed Holder: $holder")
surface = null
while (emulationThread.isAlive)
if (setSurface(surface))
if (setSurface(null))
return
}

View File

@ -32,54 +32,49 @@ object KeyReader {
return false
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() ?: false)
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() == true)
return false
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = tmpOutputFile.bufferedWriter()
tmpOutputFile.bufferedWriter().use { writer ->
val valid = inputStream!!.bufferedReader().useLines {
for (line in it) {
val pair = line.split("=")
if (pair.size != 2)
return@useLines false
val valid = inputStream!!.bufferedReader().useLines {
for (line in it) {
val pair = line.split("=")
if (pair.size != 2)
return@useLines false
val key = pair[0].trim()
val value = pair[1].trim()
when (keyType) {
KeyType.Title -> {
if (key.length != 32 && !isHexString(key))
return@useLines false
if (value.length != 32 && !isHexString(value))
return@useLines false
}
KeyType.Prod -> {
if (!key.contains("_"))
return@useLines false
if (!isHexString(value))
return@useLines false
}
}
val key = pair[0].trim()
val value = pair[1].trim()
when (keyType) {
KeyType.Title -> {
if (key.length != 32 && !isHexString(key))
return@useLines false
if (value.length != 32 && !isHexString(value))
return@useLines false
}
KeyType.Prod -> {
if (!key.contains("_"))
return@useLines false
if (!isHexString(value))
return@useLines false
}
writer.append("$key=$value\n")
}
outputStream.append("$key=$value\n")
true
}
true
if (valid) tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
return valid
}
outputStream.flush()
outputStream.close()
if (valid)
tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
return valid
}
private fun isHexString(str : String) : Boolean {
for (c in str)
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'))
return false
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) return false
return true
}
}

View File

@ -66,22 +66,23 @@ class LogActivity : AppCompatActivity() {
try {
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
logFile.forEachLine { logLine ->
adapter.setItems(logFile.readLines().mapNotNull { logLine ->
try {
val logMeta = logLine.split("|", limit = 3)
if (logMeta[0].startsWith("1")) {
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 {
adapter.addItem(HeaderViewItem(logMeta[1]))
return@mapNotNull HeaderViewItem(logMeta[1])
}
} catch (ignored : IndexOutOfBoundsException) {
} catch (ignored : NumberFormatException) {
}
}
null
})
} catch (e : FileNotFoundException) {
Log.w("Logger", "IO Error during access of log file: " + e.message)
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()

View File

@ -5,22 +5,20 @@
package emu.skyline
import android.animation.ObjectAnimator
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
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.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
@ -34,222 +32,137 @@ import emu.skyline.data.AppItem
import emu.skyline.data.DataItem
import emu.skyline.data.HeaderItem
import emu.skyline.loader.LoaderResult
import emu.skyline.loader.RomFile
import emu.skyline.loader.RomFormat
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.titlebar.*
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
import kotlin.math.ceil
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = MainActivity::class.java.simpleName
}
private val settings by lazy { Settings(this) }
/**
* The adapter used for adding elements to [app_list]
*/
private val adapter = GenericAdapter()
private var reloading = AtomicBoolean()
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 viewModel by viewModels<MainViewModel>()
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?) {
AppCompatDelegate.setDefaultNightMode(
when ((settings.appTheme.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
}
)
super.onCreate(savedInstanceState)
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
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
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)) }
setupAppList()
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var y = 0
swipe_refresh_layout.apply {
setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) })
setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) })
post { setDistanceToTriggerSync(swipe_refresh_layout.height / 3) }
setOnRefreshListener { loadRoms(false) }
}
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
y += dy
viewModel.state.observe(owner = this, onChanged = ::handleState)
loadRoms(!settings.refreshRequired)
if (!app_list.isInTouchMode)
toolbar_layout.setExpanded(y == 0)
super.onScrolled(recyclerView, dx, dy)
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) {
viewModel.searchBarAnimated = true
post { startTitleAnimation() }
}
})
}
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
search_bar.refreshIconVisible = !isInTouchMode
}
}
val controllerFabX = controller_fabs.translationX
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener {
if (!it) {
toolbar_layout.setExpanded(false)
private inner class GridSpacingItemDecoration : RecyclerView.ItemDecoration() {
private val padding = resources.getDimensionPixelSize(R.dimen.grid_padding)
controller_fabs.visibility = View.VISIBLE
ObjectAnimator.ofFloat(controller_fabs, "translationX", 0f).apply {
duration = 250
start()
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
}
} else {
ObjectAnimator.ofFloat(controller_fabs, "translationX", controllerFabX).apply {
duration = 250
start()
}.doOnEnd { controller_fabs.visibility = View.GONE }
}
if (layoutParams.spanSize == gridLayoutManager.spanCount) outRect.right = padding
}
}
private fun setAppListDecoration() {
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
when (layoutType) {
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 gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
}
}
app_list.layoutManager = CustomLayoutManager(gridSpan)
setAppListDecoration()
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
startActivityForResult(intent, 1)
} else {
refreshAdapter(!settings.refreshRequired)
}
}
/**
* This inflates the layout for the menu [R.menu.toolbar_main] and sets up searching the logs
*/
override fun onCreateOptionsMenu(menu : Menu) : Boolean {
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
}
override fun onQueryTextChange(newText : String) : Boolean {
adapter.filter.filter(newText)
return true
}
})
return super.onCreateOptionsMenu(menu)
private fun handleState(state : MainState) = when (state) {
MainState.Loading -> {
search_bar.animateRefreshIcon()
swipe_refresh_layout.isRefreshing = true
}
is MainState.Loaded -> {
swipe_refresh_layout.isRefreshing = false
populateAdapter(state.items)
}
is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
}
private fun selectStartGame(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
if (settings.selectAction)
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
else if (appItem.loaderResult == LoaderResult.Success)
@ -308,26 +206,24 @@ class MainActivity : AppCompatActivity() {
}
private fun selectShowGameDialog(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
}
/**
* This handles menu interaction for [R.id.action_settings] and [R.id.action_refresh]
*/
override fun onOptionsItemSelected(item : MenuItem) : Boolean {
return when (item.itemId) {
R.id.action_settings -> {
startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
true
}
private fun loadRoms(loadFromFile : Boolean) {
viewModel.loadRoms(this, loadFromFile, Uri.parse(settings.searchLocation))
settings.refreshRequired = false
}
R.id.action_refresh -> {
refreshAdapter(false)
true
private fun populateAdapter(items : List<DataItem>) {
adapter.setItems(items.map {
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)
settings.searchLocation = uri.toString()
refreshAdapter(!settings.refreshRequired)
loadRoms(!settings.refreshRequired)
}
2 -> {
@ -360,9 +256,7 @@ class MainActivity : AppCompatActivity() {
}
}
3 -> {
if (settings.refreshRequired) refreshAdapter(false)
}
3 -> if (settings.refreshRequired) loadRoms(false)
}
}
}
@ -371,7 +265,7 @@ class MainActivity : AppCompatActivity() {
super.onResume()
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) {
appViewItem.layoutType = layoutType
layoutTypeChanged = true
@ -381,14 +275,19 @@ class MainActivity : AppCompatActivity() {
}
if (layoutTypeChanged) {
adapter.notifyAllItemsChanged()
setAppListDecoration()
adapter.notifyItemRangeChanged(0, adapter.currentItems.size)
}
}
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)
when (layoutType) {
LayoutType.List -> app_list.post { app_list.setPadding(0, 0, 0, fab_parent.height) }
LayoutType.Grid, LayoutType.GridCompact -> app_list.post { app_list.setPadding(gridCardMagin, 0, gridCardMagin, fab_parent.height) }
override fun onBackPressed() {
search_bar.apply {
if (hasFocus() && text.isNotEmpty()) {
text = ""
clearFocus()
} else {
super.onBackPressed()
}
}
}
}

View 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))
}
}
}
}

View File

@ -13,7 +13,6 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import emu.skyline.preference.ActivityResultDelegate
import emu.skyline.preference.DocumentActivity
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.android.synthetic.main.titlebar.*
class SettingsActivity : AppCompatActivity() {
@ -46,8 +45,6 @@ class SettingsActivity : AppCompatActivity() {
super.onActivityResult(requestCode, resultCode, data)
preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
settings
}
/**
@ -101,4 +98,9 @@ class SettingsActivity : AppCompatActivity() {
return super.onKeyUp(keyCode, event)
}
override fun finish() {
setResult(RESULT_OK)
super.finish()
}
}

View File

@ -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)
}
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 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) }
}
when (layoutType) {
LayoutType.List -> holder.itemView
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
}.apply {
holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
setOnClickListener { onClick.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 areItemsTheSame(other : GenericListItem) = key() == other.key()
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item
}

View File

@ -8,20 +8,30 @@ package emu.skyline.adapter
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import info.debatty.java.stringsimilarity.Cosine
import info.debatty.java.stringsimilarity.JaroWinkler
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 {
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
val allItems = mutableListOf<GenericViewHolderBinder>()
private var filteredItems = listOf<GenericViewHolderBinder>()
override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem)
}
}
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>()
@ -38,19 +48,10 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
fun addItem(item : GenericViewHolderBinder) {
allItems.add(item)
notifyItemInserted(currentItems.size)
}
fun removeAllItems() {
val size = currentItems.size
fun setItems(items : List<GenericListItem>) {
allItems.clear()
notifyItemRangeRemoved(0, size)
}
fun notifyAllItemsChanged() {
notifyItemRangeChanged(0, currentItems.size)
allItems.addAll(items)
filter.filter(currentSearchTerm)
}
/**
@ -67,7 +68,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
*/
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]
@ -92,7 +93,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
results.values = allItems.toMutableList()
results.count = allItems.size
} else {
val filterData = mutableListOf<GenericViewHolderBinder>()
val filterData = mutableListOf<GenericListItem>()
val topResults = extractSorted()
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) {
@Suppress("UNCHECKED_CAST")
filteredItems = results.values as List<GenericViewHolderBinder>
notifyDataSetChanged()
asyncListDiffer.submitList(results.values as List<GenericListItem>)
}
}
}

View File

@ -16,7 +16,7 @@ interface GenericLayoutFactory {
fun createLayout(parent : ViewGroup) : View
}
abstract class GenericViewHolderBinder {
abstract class GenericListItem {
var adapter : GenericAdapter? = null
abstract fun getLayoutFactory() : GenericLayoutFactory
@ -27,4 +27,11 @@ abstract class GenericViewHolderBinder {
* Used for filtering
*/
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
}

View File

@ -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)
}
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
class HeaderViewItem(private val text : String) : GenericListItem() {
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) {
@ -23,4 +23,6 @@ class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
}
override fun toString() = ""
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text
}

View File

@ -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)
}
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 bind(holder : GenericViewHolder, position : Int) {

View File

@ -5,6 +5,7 @@
package emu.skyline.adapter.controller
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.ButtonGuestEvent
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) }
}
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button
}

View File

@ -12,14 +12,14 @@ import androidx.core.view.isGone
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder
import emu.skyline.adapter.GenericListItem
import kotlinx.android.synthetic.main.controller_checkbox_item.*
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
}
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericViewHolderBinder() {
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 bind(holder : GenericViewHolder, position : Int) {

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.GeneralType
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) }
}
override fun areItemsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type
}

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.AxisGuestEvent
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) }
}
override fun areItemsTheSame(other : GenericListItem) = other is ControllerStickViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick
}

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder
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) }
}
override fun areItemsTheSame(other : GenericListItem) = other is ControllerTypeViewItem
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type
}

View File

@ -11,15 +11,15 @@ import android.view.ViewGroup
import androidx.core.view.isGone
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder
import kotlinx.android.synthetic.main.controller_item.*
private object ControllerLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
}
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericViewHolderBinder() {
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem() {
private var position = -1
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory

View File

@ -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 ""
}

View File

@ -5,6 +5,56 @@
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
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 ""
}

View File

@ -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

View File

@ -10,9 +10,11 @@ import android.os.Bundle
import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import emu.skyline.R
import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.controller.*
import emu.skyline.input.dialog.ButtonDialog
@ -53,99 +55,103 @@ class ControllerActivity : AppCompatActivity() {
* This function updates the [adapter] based on information from [InputManager]
*/
private fun update() {
adapter.removeAllItems()
val items = mutableListOf<GenericListItem>()
val controller = InputManager.controllers[id]!!
try {
val controller = InputManager.controllers[id]!!
adapter.addItem(ControllerTypeViewItem(controller.type, onControllerTypeClick))
items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick))
if (controller.type == ControllerType.None)
return
if (controller.type == ControllerType.None)
return
if (id == 0 && controller.type.firstController) {
adapter.addItem(HeaderViewItem(getString(R.string.osc)))
if (id == 0 && controller.type.firstController) {
items.add(HeaderViewItem(getString(R.string.osc)))
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 ->
item.summary = oscSummary.invoke(item.checked)
settings.onScreenControl = item.checked
adapter.notifyItemChanged(position)
})
val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
item.summary = oscSummary.invoke(item.checked)
settings.onScreenControl = item.checked
adapter.notifyItemChanged(position)
})
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
settings.onScreenControlRecenterSticks = item.checked
adapter.notifyItemChanged(position)
})
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
settings.onScreenControlRecenterSticks = item.checked
adapter.notifyItemChanged(position)
})
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
startActivity(Intent(this, OnScreenEditActivity::class.java))
}))
}
items.add(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
startActivity(Intent(this, OnScreenEditActivity::class.java))
}))
}
var wroteTitle = false
var wroteTitle = false
for (item in GeneralType.values()) {
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
for (item in GeneralType.values()) {
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
if (!wroteTitle) {
items.add(HeaderViewItem(getString(R.string.general)))
wroteTitle = true
}
items.add(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
}
}
wroteTitle = false
for (stick in controller.type.sticks) {
if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(R.string.general)))
items.add(HeaderViewItem(getString(R.string.sticks)))
wroteTitle = true
}
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
}
}
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
wroteTitle = false
for (stick in controller.type.sticks) {
if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
wroteTitle = true
items.add(stickItem)
buttonMap[stick.button] = stickItem
axisMap[stick.xAxis] = stickItem
axisMap[stick.yAxis] = stickItem
}
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
val dpadButtons = Pair(R.string.dpad, arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight))
val faceButtons = Pair(R.string.face_buttons, arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y))
val shoulderTriggerButtons = Pair(R.string.shoulder_trigger, arrayOf(ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR))
val shoulderRailButtons = Pair(R.string.shoulder_rail, arrayOf(ButtonId.LeftSL, ButtonId.LeftSR, ButtonId.RightSL, ButtonId.RightSR))
adapter.addItem(stickItem)
buttonMap[stick.button] = stickItem
axisMap[stick.xAxis] = stickItem
axisMap[stick.yAxis] = stickItem
}
val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons)
val dpadButtons = Pair(R.string.dpad, arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight))
val faceButtons = Pair(R.string.face_buttons, arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y))
val shoulderTriggerButtons = Pair(R.string.shoulder_trigger, arrayOf(ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR))
val shoulderRailButtons = Pair(R.string.shoulder_rail, arrayOf(ButtonId.LeftSL, ButtonId.LeftSR, ButtonId.RightSL, ButtonId.RightSR))
for (buttonArray in buttonArrays) {
wroteTitle = false
val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons)
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
if (!wroteTitle) {
items.add(HeaderViewItem(getString(buttonArray.first)))
wroteTitle = true
}
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
items.add(buttonItem)
buttonMap[button] = buttonItem
}
}
for (buttonArray in buttonArrays) {
wroteTitle = false
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(buttonArray.first)))
items.add(HeaderViewItem(getString(R.string.misc_buttons)))
wroteTitle = true
}
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem)
items.add(buttonItem)
buttonMap[button] = buttonItem
}
}
wroteTitle = false
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(R.string.misc_buttons)))
wroteTitle = true
}
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem)
buttonMap[button] = buttonItem
} finally {
adapter.setItems(items)
}
}
@ -165,9 +171,18 @@ class ControllerActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
controller_list.layoutManager = LinearLayoutManager(this)
val layoutManager = LinearLayoutManager(this)
controller_list.layoutManager = layoutManager
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()
}

View File

@ -5,9 +5,9 @@
package emu.skyline.input.onscreen
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
@ -17,7 +17,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import emu.skyline.R
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.main_activity.fab_parent
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
class OnScreenEditActivity : AppCompatActivity() {
@ -87,10 +86,8 @@ class OnScreenEditActivity : AppCompatActivity() {
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
actions.forEach { pair ->
fab_parent.addView(FloatingActionButton(this).apply {
size = FloatingActionButton.SIZE_MINI
setColorFilter(Color.WHITE)
setImageDrawable(ContextCompat.getDrawable(context, pair.first))
fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply {
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
setOnClickListener { pair.second.invoke() }
fabMapping[pair.first] = this
})

View File

@ -63,7 +63,7 @@ enum class LoaderResult(val value : Int) {
/**
* 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 ->
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()

View File

@ -10,6 +10,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import emu.skyline.utils.Settings
/**
* 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)
Settings(this).refreshRequired = true
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString(keyName, uri.toString())
.putBoolean("refresh_required", true)
.apply()
}
finish()

View File

@ -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
}
}
}

View 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
}
}

View File

@ -56,7 +56,9 @@
android:layout_marginEnd="6dp"
android:focusedByDefault="true"
android:text="@string/play"
app:icon="@drawable/ic_play" />
android:textColor="?attr/colorAccent"
app:icon="@drawable/ic_play"
app:iconTint="?attr/colorAccent" />
<Button
android:id="@+id/game_pin"
@ -64,8 +66,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="@string/pin" />
android:text="@string/pin"
android:textColor="?attr/colorAccent" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,65 +1,55 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/card_app_item_grid"
<com.google.android.material.card.MaterialCardView
android:id="@+id/item_click_layout"
android:layout_width="match_parent"
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"
app:cardCornerRadius="4dp"
app:cardElevation="@dimen/app_card_margin"
app:cardUseCompatPadding="true">
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
android:adjustViewBounds="true"
android:contentDescription="@string/icon"
tools:src="@drawable/default_icon" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/icon"
tools:src="@drawable/default_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="Title" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="15dp"
android:paddingTop="10dp"
android:paddingEnd="15dp"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingBottom="15dp"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light"
tools:text="Subtitle" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
<TextView
android:id="@+id/text_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textSize="12sp"
tools:text="Subtitle" />
</LinearLayout>

View File

@ -1,77 +1,70 @@
<?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:tools="http://schemas.android.com/tools"
android:id="@+id/item_click_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<androidx.cardview.widget.CardView
android:id="@+id/card_app_item_grid"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
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"
app:cardCornerRadius="4dp"
app:cardElevation="@dimen/app_card_margin"
app:cardUseCompatPadding="true">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/icon"
android:foreground="@drawable/background_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" />
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/icon"
android:foreground="@drawable/background_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="242.25"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Title" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="242.25"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:alpha="242.25"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
<TextView
android:id="@+id/text_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:alpha="242.25"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_click_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
@ -14,6 +15,7 @@
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@string/icon"
android:focusable="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" />

View File

@ -76,6 +76,6 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="10dp"
android:text="@string/reset" />
android:text="@string/reset"
android:textColor="?attr/colorAccent" />
</LinearLayout>

View File

@ -2,76 +2,45 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
<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:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<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>
android:layout_marginTop="-4dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="4dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -9,7 +9,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<emu.skyline.views.CustomLinearLayout
<LinearLayout
android:id="@+id/fab_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View 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" />

View File

@ -60,7 +60,6 @@
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light"
android:textSize="13sp" />
</RelativeLayout>
<LinearLayout
@ -76,6 +75,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/builtin"
android:textColor="?attr/colorAccent"
android:visibility="gone" />
<Button
@ -84,8 +84,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/reset" />
android:text="@string/reset"
android:textColor="?attr/colorAccent" />
</LinearLayout>
</LinearLayout>

View File

@ -4,8 +4,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp"
android:textColor="?colorSecondary"
android:textSize="15sp" />
android:layout_marginBottom="8dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:textStyle="bold" />

View File

@ -22,7 +22,7 @@
android:animateLayoutChanges="true"
android:text="@string/stick_button"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
android:textColor="@color/colorPrimary"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" />
<TextView
@ -116,13 +116,15 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/reset" />
android:text="@string/reset"
android:textColor="?attr/colorAccent" />
<Button
android:id="@+id/stick_next"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next" />
android:text="@string/next"
android:textColor="?attr/colorAccent" />
</LinearLayout>
</LinearLayout>

View File

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
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_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:liftOnScroll="true">
android:keyboardNavigationCluster="false"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/AppTheme.ActionBar"
app:layout_scrollFlags="scroll|enterAlways|snap" />
app:elevation="16dp"
app:layout_scrollFlags="scroll" />
</com.google.android.material.appbar.AppBarLayout>

View 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>

View File

@ -6,15 +6,18 @@
android:icon="@drawable/ic_search"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_share_log"
android:icon="@drawable/ic_share"
android:title="@string/share"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_clear"
android:icon="@drawable/ic_clear"
android:title="@string/clear"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom" />
</menu>

View File

@ -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>

View 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>

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FFFF0000</color>
<color name="colorPrimaryDark">#BFFF0000</color>
<color name="colorOnPrimary">#D7000000</color>
<color name="colorSecondary">#FFFF0000</color>
<color name="colorSecondaryDark">#BFFF0000</color>
<color name="colorOnSecondary">#DF000000</color>
<color name="colorPrimary">#FF424242</color>
<color name="colorPrimaryDark">@android:color/black</color>
</resources>

View 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>

View 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>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FFFF0000</color>
<color name="colorPrimaryDark">#BFFF0000</color>
<color name="colorOnPrimary">#DFFFFFFF</color>
<color name="colorSecondary">#FFFF0000</color>
<color name="colorSecondaryDark">#BFFF0000</color>
<color name="colorOnSecondary">#DFFFFFFF</color>
<color name="colorPrimary">@color/cardview_light_background</color>
<color name="colorPrimaryDark">@android:color/white</color>
<color name="colorAccent">#FFFF0000</color>
</resources>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="app_card_margin">8dp</dimen>
<dimen name="app_card_margin_half">4dp</dimen>
<dimen name="grid_padding">8dp</dimen>
</resources>

View File

@ -1,20 +1,15 @@
<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="colorPrimaryVariant">@color/colorPrimaryDark</item>
<item name="colorOnPrimary">@color/colorOnPrimary</item>
<item name="colorSecondary">@color/colorSecondary</item>
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
<item name="colorOnSecondary">@color/colorOnSecondary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="AppTheme.ActionBar" parent="">
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
<item name="android:textColorSecondary">@color/colorOnPrimary</item>
</style>
<style name="AppTheme" parent="BaseAppTheme" />
<style name="roundedAppImage" parent="">
<style name="roundedAppImage">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">6dp</item>
</style>

View File

@ -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>

0
gradlew vendored Normal file → Executable file
View File