mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-28 07:55:29 +03:00
Redesign cards in grid view
* Refactor some classes and clean up * Refresh style on the fly
This commit is contained in:
parent
b23779bda1
commit
c3e54d1abf
@ -15,10 +15,11 @@ import android.view.KeyEvent
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import emu.skyline.adapter.AppItem
|
import emu.skyline.data.AppItem
|
||||||
import kotlinx.android.synthetic.main.app_dialog.*
|
import kotlinx.android.synthetic.main.app_dialog.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +27,7 @@ import kotlinx.android.synthetic.main.app_dialog.*
|
|||||||
*
|
*
|
||||||
* @param item This is used to hold the [AppItem] between instances
|
* @param item This is used to hold the [AppItem] between instances
|
||||||
*/
|
*/
|
||||||
class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
class AppDialog(val item : AppItem) : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This inflates the layout of the dialog after initial view creation
|
* This inflates the layout of the dialog after initial view creation
|
||||||
@ -47,10 +48,9 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
|||||||
dialog?.setOnKeyListener { _, keyCode, event ->
|
dialog?.setOnKeyListener { _, keyCode, event ->
|
||||||
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_DOWN) {
|
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_DOWN) {
|
||||||
dialog?.onBackPressed()
|
dialog?.onBackPressed()
|
||||||
true
|
return@setOnKeyListener true
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,38 +60,35 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() {
|
|||||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
if (item is AppItem) {
|
val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256)
|
||||||
val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context?.theme)?.toBitmap(256, 256)
|
|
||||||
|
|
||||||
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
||||||
game_title.text = item.title
|
game_title.text = item.title
|
||||||
game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing)
|
game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing)
|
||||||
|
|
||||||
game_play.setOnClickListener {
|
game_play.setOnClickListener {
|
||||||
val intent = Intent(activity, EmulationActivity::class.java)
|
val intent = Intent(activity, EmulationActivity::class.java)
|
||||||
intent.data = item.uri
|
intent.data = item.uri
|
||||||
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
val shortcutManager = activity?.getSystemService(ShortcutManager::class.java)!!
|
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
|
||||||
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||||
|
|
||||||
game_pin.setOnClickListener {
|
game_pin.setOnClickListener {
|
||||||
val info = ShortcutInfo.Builder(context, item.title)
|
val info = ShortcutInfo.Builder(context, item.title)
|
||||||
info.setShortLabel(item.meta.name)
|
info.setShortLabel(item.meta.name)
|
||||||
info.setActivity(ComponentName(context!!, EmulationActivity::class.java))
|
info.setActivity(ComponentName(requireActivity(), EmulationActivity::class.java))
|
||||||
info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon))
|
info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon))
|
||||||
|
|
||||||
val intent = Intent(context, EmulationActivity::class.java)
|
val intent = Intent(context, EmulationActivity::class.java)
|
||||||
intent.data = item.uri
|
intent.data = item.uri
|
||||||
intent.action = Intent.ACTION_VIEW
|
intent.action = Intent.ACTION_VIEW
|
||||||
|
|
||||||
info.setIntent(intent)
|
info.setIntent(intent)
|
||||||
|
|
||||||
shortcutManager.requestPinShortcut(info.build(), null)
|
shortcutManager.requestPinShortcut(info.build(), null)
|
||||||
}
|
}
|
||||||
} else
|
|
||||||
activity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import emu.skyline.adapter.AppAdapter
|
import emu.skyline.adapter.AppAdapter
|
||||||
import emu.skyline.adapter.AppItem
|
|
||||||
import emu.skyline.adapter.GridLayoutSpan
|
import emu.skyline.adapter.GridLayoutSpan
|
||||||
import emu.skyline.adapter.LayoutType
|
import emu.skyline.adapter.LayoutType
|
||||||
|
import emu.skyline.data.AppItem
|
||||||
import emu.skyline.loader.RomFile
|
import emu.skyline.loader.RomFile
|
||||||
import emu.skyline.loader.RomFormat
|
import emu.skyline.loader.RomFormat
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
@ -37,7 +37,7 @@ import java.io.IOException
|
|||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClickListener {
|
class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
/**
|
/**
|
||||||
* This is used to get/set shared preferences
|
* This is used to get/set shared preferences
|
||||||
*/
|
*/
|
||||||
@ -51,14 +51,14 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
/**
|
/**
|
||||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata
|
* This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata
|
||||||
*/
|
*/
|
||||||
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
private fun addEntries(romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
||||||
var foundCurrent = found
|
var foundCurrent = found
|
||||||
|
|
||||||
directory.listFiles().forEach { file ->
|
directory.listFiles().forEach { file ->
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
|
foundCurrent = addEntries(romFormat, file, foundCurrent)
|
||||||
} else {
|
} else {
|
||||||
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
if (romFormat.extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||||
val romFd = contentResolver.openFileDescriptor(file.uri, "r")!!
|
val romFd = contentResolver.openFileDescriptor(file.uri, "r")!!
|
||||||
val romFile = RomFile(this, romFormat, romFd)
|
val romFile = RomFile(this, romFormat, romFd)
|
||||||
|
|
||||||
@ -111,14 +111,17 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
try {
|
try {
|
||||||
runOnUiThread { adapter.clear() }
|
runOnUiThread { adapter.clear() }
|
||||||
|
|
||||||
var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!
|
||||||
foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
|
||||||
foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
var foundRoms = addEntries(RomFormat.NRO, searchLocation)
|
||||||
foundRoms = foundRoms or addEntries("nsp", RomFormat.NSP, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
foundRoms = foundRoms or addEntries(RomFormat.NSO, searchLocation)
|
||||||
|
foundRoms = foundRoms or addEntries(RomFormat.NCA, searchLocation)
|
||||||
|
foundRoms = foundRoms or addEntries(RomFormat.NSP, searchLocation)
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (!foundRoms)
|
if (!foundRoms) {
|
||||||
adapter.addHeader(getString(R.string.no_rom))
|
adapter.addHeader(getString(R.string.no_rom))
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
adapter.save(File("${applicationInfo.dataDir}/roms.bin"))
|
adapter.save(File("${applicationInfo.dataDir}/roms.bin"))
|
||||||
@ -172,28 +175,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
open_fab.setOnClickListener(this)
|
open_fab.setOnClickListener(this)
|
||||||
log_fab.setOnClickListener(this)
|
log_fab.setOnClickListener(this)
|
||||||
|
|
||||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
setupAppList()
|
||||||
|
|
||||||
adapter = AppAdapter(this, layoutType)
|
|
||||||
app_list.adapter = adapter
|
|
||||||
|
|
||||||
when (layoutType) {
|
|
||||||
LayoutType.List -> {
|
|
||||||
app_list.layoutManager = LinearLayoutManager(this)
|
|
||||||
app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
|
||||||
}
|
|
||||||
|
|
||||||
LayoutType.Grid -> {
|
|
||||||
val itemWidth = 225
|
|
||||||
val metrics = resources.displayMetrics
|
|
||||||
val span = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
|
||||||
|
|
||||||
val layoutManager = GridLayoutManager(this, span)
|
|
||||||
layoutManager.spanSizeLookup = GridLayoutSpan(adapter, span)
|
|
||||||
|
|
||||||
app_list.layoutManager = layoutManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
var y : Int = 0
|
var y : Int = 0
|
||||||
@ -202,23 +184,43 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
y += dy
|
y += dy
|
||||||
|
|
||||||
if (!app_list.isInTouchMode) {
|
if (!app_list.isInTouchMode) {
|
||||||
if (y == 0)
|
toolbar_layout.setExpanded(y == 0)
|
||||||
toolbar_layout.setExpanded(true)
|
|
||||||
else
|
|
||||||
toolbar_layout.setExpanded(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAppList() {
|
||||||
|
val itemWidth = 225
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||||
|
|
||||||
|
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||||
|
|
||||||
|
adapter = AppAdapter(layoutType = layoutType, gridSpan = gridSpan, onClick = selectStartGame, onLongClick = selectShowGameDialog)
|
||||||
|
app_list.adapter = adapter
|
||||||
|
|
||||||
|
app_list.layoutManager = when (layoutType) {
|
||||||
|
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||||
|
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
||||||
|
spanSizeLookup = GridLayoutSpan(adapter, gridSpan).also {
|
||||||
|
if (app_list.itemDecorationCount > 0) {
|
||||||
|
app_list.removeItemDecorationAt(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sharedPreferences.getString("search_location", "") == "") {
|
if (sharedPreferences.getString("search_location", "") == "") {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
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
|
} else {
|
||||||
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false))
|
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,12 +247,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handles on-click interaction with [R.id.log_fab], [R.id.open_fab], [R.id.app_item_linear] and [R.id.app_item_grid]
|
* This handles on-click interaction with [R.id.log_fab], [R.id.open_fab]
|
||||||
*/
|
*/
|
||||||
override fun onClick(view : View) {
|
override fun onClick(view : View) {
|
||||||
when (view.id) {
|
when (view.id) {
|
||||||
R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java))
|
R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java))
|
||||||
|
|
||||||
R.id.open_fab -> {
|
R.id.open_fab -> {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
@ -258,40 +259,19 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
|
|
||||||
startActivityForResult(intent, 2)
|
startActivityForResult(intent, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.app_item_linear, R.id.app_item_grid -> {
|
|
||||||
val tag = view.tag
|
|
||||||
if (tag is AppItem) {
|
|
||||||
if (sharedPreferences.getBoolean("select_action", false)) {
|
|
||||||
val dialog = AppDialog(tag)
|
|
||||||
dialog.show(supportFragmentManager, "game")
|
|
||||||
} else {
|
|
||||||
val intent = Intent(this, EmulationActivity::class.java)
|
|
||||||
intent.data = tag.uri
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val selectStartGame : (appItem : AppItem) -> Unit = {
|
||||||
* This handles long-click interaction with [R.id.app_item_linear] and [R.id.app_item_grid]
|
if (sharedPreferences.getBoolean("select_action", false)) {
|
||||||
*/
|
AppDialog(it).show(supportFragmentManager, "game")
|
||||||
override fun onLongClick(view : View?) : Boolean {
|
} else {
|
||||||
when (view?.id) {
|
startActivity(Intent(this, EmulationActivity::class.java).apply { data = it.uri })
|
||||||
R.id.app_item_linear, R.id.app_item_grid -> {
|
|
||||||
val tag = view.tag
|
|
||||||
if (tag is AppItem) {
|
|
||||||
val dialog = AppDialog(tag)
|
|
||||||
dialog.show(supportFragmentManager, "game")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
private val selectShowGameDialog : (appItem : AppItem) -> Unit = {
|
||||||
|
AppDialog(it).show(supportFragmentManager, "game")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,4 +330,13 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||||
|
if (layoutType != adapter.layoutType) {
|
||||||
|
setupAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,80 +7,42 @@ package emu.skyline.adapter
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.adapter.ElementType.Header
|
import emu.skyline.data.AppItem
|
||||||
import emu.skyline.adapter.ElementType.Item
|
|
||||||
import emu.skyline.loader.AppEntry
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
|
||||||
*/
|
|
||||||
class AppItem(val meta : AppEntry) : BaseItem() {
|
|
||||||
/**
|
|
||||||
* The icon of the application
|
|
||||||
*/
|
|
||||||
val icon : Bitmap?
|
|
||||||
get() = meta.icon
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The title of the application
|
|
||||||
*/
|
|
||||||
val title : String
|
|
||||||
get() = meta.name
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The string used as the sub-title, we currently use the author
|
|
||||||
*/
|
|
||||||
val subTitle : String?
|
|
||||||
get() = meta.author
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URI of the application's image file
|
|
||||||
*/
|
|
||||||
val uri : Uri
|
|
||||||
get() = meta.uri
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The format of the application ROM as a string
|
|
||||||
*/
|
|
||||||
private val type : String
|
|
||||||
get() = meta.format.name
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name and author is used as the key
|
|
||||||
*/
|
|
||||||
override fun key() : String? {
|
|
||||||
return if (meta.author != null) meta.name + " " + meta.author else meta.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This enumerates the type of layouts the menu can be in
|
* This enumerates the type of layouts the menu can be in
|
||||||
*/
|
*/
|
||||||
enum class LayoutType {
|
enum class LayoutType(val layoutRes : Int) {
|
||||||
List,
|
List(R.layout.app_item_linear),
|
||||||
Grid,
|
Grid(R.layout.app_item_grid),
|
||||||
|
GridCompact(R.layout.app_item_grid_compact)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private typealias AppInteractionFunc = (appItem : AppItem) -> Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This adapter is used to display all found applications using their metadata
|
* This adapter is used to display all found applications using their metadata
|
||||||
*/
|
*/
|
||||||
internal class AppAdapter(val context : Context?, private val layoutType : LayoutType) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>(), View.OnClickListener {
|
internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : Int, private val onClick : AppInteractionFunc, private val onLongClick : AppInteractionFunc) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||||
private val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context.theme)?.toBitmap(256, 256)
|
|
||||||
private val missingString = context?.getString(R.string.metadata_missing)
|
private lateinit var context : Context
|
||||||
|
private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||||
|
private val missingString by lazy { context.getString(R.string.metadata_missing) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This adds a header to the view with the contents of [string]
|
* This adds a header to the view with the contents of [string]
|
||||||
@ -89,29 +51,6 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
|||||||
super.addHeader(BaseHeader(string))
|
super.addHeader(BaseHeader(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The onClick handler for the supplied [view], used for the icon preview
|
|
||||||
*/
|
|
||||||
override fun onClick(view : View) {
|
|
||||||
val position = view.tag as Int
|
|
||||||
|
|
||||||
if (getItem(position) is AppItem) {
|
|
||||||
val item = getItem(position) as AppItem
|
|
||||||
|
|
||||||
if (view.id == R.id.icon) {
|
|
||||||
val builder = Dialog(context!!)
|
|
||||||
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
||||||
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
|
||||||
|
|
||||||
val imageView = ImageView(context)
|
|
||||||
imageView.setImageBitmap(item.icon ?: missingIcon)
|
|
||||||
|
|
||||||
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ViewHolder used by items is used to hold the views associated with an item
|
* The ViewHolder used by items is used to hold the views associated with an item
|
||||||
*
|
*
|
||||||
@ -134,38 +73,49 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
|||||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||||
*/
|
*/
|
||||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||||
|
context = parent.context
|
||||||
|
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
var holder : RecyclerView.ViewHolder? = null
|
val view = when (ElementType.values()[viewType]) {
|
||||||
|
ElementType.Item -> inflater.inflate(layoutType.layoutRes, parent, false)
|
||||||
if (viewType == Item.ordinal) {
|
ElementType.Header -> inflater.inflate(R.layout.section_item, parent, false)
|
||||||
val view = inflater.inflate(if (layoutType == LayoutType.List) R.layout.app_item_linear else R.layout.app_item_grid, parent, false)
|
|
||||||
holder = ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
|
||||||
|
|
||||||
if (layoutType == LayoutType.List) {
|
|
||||||
if (context is View.OnClickListener)
|
|
||||||
view.setOnClickListener(context as View.OnClickListener)
|
|
||||||
|
|
||||||
if (context is View.OnLongClickListener)
|
|
||||||
view.setOnLongClickListener(context as View.OnLongClickListener)
|
|
||||||
} else {
|
|
||||||
holder.card = view.findViewById(R.id.app_item_grid)
|
|
||||||
|
|
||||||
if (context is View.OnClickListener)
|
|
||||||
holder.card!!.setOnClickListener(context as View.OnClickListener)
|
|
||||||
|
|
||||||
if (context is View.OnLongClickListener)
|
|
||||||
holder.card!!.setOnLongClickListener(context as View.OnLongClickListener)
|
|
||||||
|
|
||||||
holder.title.isSelected = true
|
|
||||||
}
|
|
||||||
} else if (viewType == Header.ordinal) {
|
|
||||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
|
||||||
holder = HeaderViewHolder(view)
|
|
||||||
|
|
||||||
holder.header = view.findViewById(R.id.text_title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return holder!!
|
Log.i("blaa", "onCreateViewHolder")
|
||||||
|
|
||||||
|
return when (ElementType.values()[viewType]) {
|
||||||
|
ElementType.Item -> {
|
||||||
|
ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)).apply {
|
||||||
|
if (layoutType == LayoutType.List) {
|
||||||
|
view.apply {
|
||||||
|
if (context is View.OnClickListener) {
|
||||||
|
setOnClickListener(context as View.OnClickListener)
|
||||||
|
}
|
||||||
|
if (context is View.OnLongClickListener) {
|
||||||
|
setOnLongClickListener(context as View.OnLongClickListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
card = view.findViewById(R.id.app_item_grid)
|
||||||
|
card!!.apply {
|
||||||
|
if (context is View.OnClickListener) {
|
||||||
|
setOnClickListener(context as View.OnClickListener)
|
||||||
|
}
|
||||||
|
if (context is View.OnLongClickListener) {
|
||||||
|
setOnLongClickListener(context as View.OnLongClickListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title.isSelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElementType.Header -> {
|
||||||
|
HeaderViewHolder(view).apply {
|
||||||
|
header = view.findViewById(R.id.text_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,16 +133,45 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou
|
|||||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||||
|
|
||||||
if (layoutType == LayoutType.List) {
|
if (layoutType == LayoutType.List) {
|
||||||
holder.icon.setOnClickListener(this)
|
holder.icon.setOnClickListener { showIconDialog(item) }
|
||||||
holder.icon.tag = position
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.card?.tag = item
|
when (layoutType) {
|
||||||
holder.parent.tag = item
|
LayoutType.List -> holder.itemView
|
||||||
|
LayoutType.Grid, LayoutType.GridCompact -> holder.card!!
|
||||||
|
}.apply {
|
||||||
|
setOnClickListener { onClick.invoke(item) }
|
||||||
|
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase margin of edges to avoid huge gap in between items
|
||||||
|
if (layoutType == LayoutType.Grid || layoutType == LayoutType.GridCompact) {
|
||||||
|
|
||||||
|
holder.itemView.layoutParams = LinearLayout.LayoutParams(holder.itemView.layoutParams.width, holder.itemView.layoutParams.height).apply {
|
||||||
|
if (position % gridSpan == 0) {
|
||||||
|
marginStart = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2
|
||||||
|
} else if (position % gridSpan == gridSpan - 1) {
|
||||||
|
marginEnd = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.itemView.requestLayout()
|
||||||
|
}
|
||||||
} else if (item is BaseHeader) {
|
} else if (item is BaseHeader) {
|
||||||
val holder = viewHolder as HeaderViewHolder
|
val holder = viewHolder as HeaderViewHolder
|
||||||
|
|
||||||
holder.header!!.text = item.title
|
holder.header!!.text = item.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showIconDialog(appItem : AppItem) {
|
||||||
|
val builder = Dialog(context)
|
||||||
|
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
|
||||||
|
val imageView = ImageView(context)
|
||||||
|
imageView.setImageBitmap(appItem.icon ?: missingIcon)
|
||||||
|
|
||||||
|
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import android.widget.Filter
|
|||||||
import android.widget.Filterable
|
import android.widget.Filterable
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import emu.skyline.data.BaseItem
|
||||||
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.io.*
|
import java.io.*
|
||||||
@ -35,16 +36,6 @@ abstract class BaseElement constructor(val elementType : ElementType) : Serializ
|
|||||||
*/
|
*/
|
||||||
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
|
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an abstract class that all adapter item classes inherit from
|
|
||||||
*/
|
|
||||||
abstract class BaseItem : BaseElement(ElementType.Item) {
|
|
||||||
/**
|
|
||||||
* This function returns a string used for searching
|
|
||||||
*/
|
|
||||||
abstract fun key() : String?
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This adapter has the ability to have 2 types of elements specifically headers and items
|
* This adapter has the ability to have 2 types of elements specifically headers and items
|
||||||
*/
|
*/
|
||||||
@ -69,9 +60,9 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
|||||||
*/
|
*/
|
||||||
fun addItem(item : ItemType) {
|
fun addItem(item : ItemType) {
|
||||||
elementArray.add(item)
|
elementArray.add(item)
|
||||||
if (searchTerm.isNotEmpty())
|
if (searchTerm.isNotEmpty()) {
|
||||||
filter.filter(searchTerm)
|
filter.filter(searchTerm)
|
||||||
else {
|
} else {
|
||||||
visibleArray.add(elementArray.size - 1)
|
visibleArray.add(elementArray.size - 1)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
@ -146,93 +137,91 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
|||||||
/**
|
/**
|
||||||
* This returns an instance of the filter object which is used to search for items in the view
|
* This returns an instance of the filter object which is used to search for items in the view
|
||||||
*/
|
*/
|
||||||
override fun getFilter() : Filter {
|
override fun getFilter() : Filter = object : Filter() {
|
||||||
return object : Filter() {
|
/**
|
||||||
/**
|
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
|
||||||
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
|
*/
|
||||||
*/
|
private val jw = JaroWinkler()
|
||||||
private val jw = JaroWinkler()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
|
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
|
||||||
*/
|
*/
|
||||||
private val cos = Cosine()
|
private val cos = Cosine()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to store the results of the item sorting
|
* This class is used to store the results of the item sorting
|
||||||
*
|
*
|
||||||
* @param score The score of this result
|
* @param score The score of this result
|
||||||
* @param index The index of this item
|
* @param index The index of this item
|
||||||
*/
|
*/
|
||||||
inner class ScoredItem(val score : Double, val index : Int) {}
|
inner class ScoredItem(val score : Double, val index : Int) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This sorts the items in [keyArray] in relation to how similar they are to [term]
|
* This sorts the items in [keyArray] in relation to how similar they are to [term]
|
||||||
*/
|
*/
|
||||||
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
|
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
|
||||||
val scoredItems : MutableList<ScoredItem> = ArrayList()
|
val scoredItems : MutableList<ScoredItem> = ArrayList()
|
||||||
|
|
||||||
keyArray.forEachIndexed { index, item ->
|
keyArray.forEachIndexed { index, item ->
|
||||||
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
|
||||||
|
|
||||||
if (similarity != 0.0)
|
if (similarity != 0.0)
|
||||||
scoredItems.add(ScoredItem(similarity, index))
|
scoredItems.add(ScoredItem(similarity, index))
|
||||||
}
|
|
||||||
|
|
||||||
scoredItems.sortWith(compareByDescending { it.score })
|
|
||||||
|
|
||||||
return scoredItems.toTypedArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
scoredItems.sortWith(compareByDescending { it.score })
|
||||||
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
|
||||||
*/
|
|
||||||
override fun performFiltering(term : CharSequence) : FilterResults {
|
|
||||||
val results = FilterResults()
|
|
||||||
searchTerm = (term as String).toLowerCase(Locale.getDefault())
|
|
||||||
|
|
||||||
if (term.isEmpty()) {
|
return scoredItems.toTypedArray()
|
||||||
results.values = elementArray.indices.toMutableList()
|
}
|
||||||
results.count = elementArray.size
|
|
||||||
} else {
|
|
||||||
val filterData = ArrayList<Int>()
|
|
||||||
|
|
||||||
val keyArray = ArrayList<String>()
|
/**
|
||||||
val keyIndex = SparseIntArray()
|
* This performs filtering on the items in [elementArray] based on similarity to [term]
|
||||||
|
*/
|
||||||
|
override fun performFiltering(term : CharSequence) : FilterResults {
|
||||||
|
val results = FilterResults()
|
||||||
|
searchTerm = (term as String).toLowerCase(Locale.getDefault())
|
||||||
|
|
||||||
for (index in elementArray.indices) {
|
if (term.isEmpty()) {
|
||||||
val item = elementArray[index]!!
|
results.values = elementArray.indices.toMutableList()
|
||||||
|
results.count = elementArray.size
|
||||||
|
} else {
|
||||||
|
val filterData = ArrayList<Int>()
|
||||||
|
|
||||||
if (item is BaseItem) {
|
val keyArray = ArrayList<String>()
|
||||||
keyIndex.append(keyArray.size, index)
|
val keyIndex = SparseIntArray()
|
||||||
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
|
||||||
}
|
for (index in elementArray.indices) {
|
||||||
|
val item = elementArray[index]!!
|
||||||
|
|
||||||
|
if (item is BaseItem) {
|
||||||
|
keyIndex.append(keyArray.size, index)
|
||||||
|
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val topResults = extractSorted(searchTerm, keyArray)
|
|
||||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
|
||||||
|
|
||||||
for (result in topResults)
|
|
||||||
if (result.score > avgScore)
|
|
||||||
filterData.add(keyIndex[result.index])
|
|
||||||
|
|
||||||
results.values = filterData
|
|
||||||
results.count = filterData.size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
val topResults = extractSorted(searchTerm, keyArray)
|
||||||
|
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||||
|
|
||||||
|
for (result in topResults)
|
||||||
|
if (result.score > avgScore)
|
||||||
|
filterData.add(keyIndex[result.index])
|
||||||
|
|
||||||
|
results.values = filterData
|
||||||
|
results.count = filterData.size
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return results
|
||||||
* This publishes the results that were calculated in [performFiltering] to the view
|
}
|
||||||
*/
|
|
||||||
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
|
||||||
if (results.values is ArrayList<*>) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
visibleArray = results.values as ArrayList<Int>
|
|
||||||
|
|
||||||
notifyDataSetChanged()
|
/**
|
||||||
}
|
* This publishes the results that were calculated in [performFiltering] to the view
|
||||||
|
*/
|
||||||
|
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
||||||
|
if (results.values is ArrayList<*>) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
visibleArray = results.values as ArrayList<Int>
|
||||||
|
|
||||||
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,7 +233,7 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
|
|||||||
* @param adapter The adapter which is used to deduce the type of the item based on the position
|
* @param adapter The adapter which is used to deduce the type of the item based on the position
|
||||||
* @param headerSpan The span size to return for headers
|
* @param headerSpan The span size to return for headers
|
||||||
*/
|
*/
|
||||||
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, var headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
|
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
|
||||||
/**
|
/**
|
||||||
* This returns the size of the span based on the type of the element at [position]
|
* This returns the size of the span based on the type of the element at [position]
|
||||||
*/
|
*/
|
||||||
|
@ -10,12 +10,12 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.OnLongClickListener
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
|
import emu.skyline.data.BaseItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to hold all data about a log entry
|
* This class is used to hold all data about a log entry
|
||||||
@ -32,7 +32,7 @@ internal class LogItem(val message : String, val level : String) : BaseItem() {
|
|||||||
/**
|
/**
|
||||||
* This adapter is used for displaying logs outputted by the application
|
* This adapter is used for displaying logs outputted by the application
|
||||||
*/
|
*/
|
||||||
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>(), OnLongClickListener {
|
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||||
private val clipboard : ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
private val clipboard : ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,41 +72,33 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
|||||||
*/
|
*/
|
||||||
private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent)
|
private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent)
|
||||||
|
|
||||||
/**
|
|
||||||
* The onLongClick handler for the supplied [view], used to copy a log into the clipboard
|
|
||||||
*/
|
|
||||||
override fun onLongClick(view : View) : Boolean {
|
|
||||||
val item = view.tag as LogItem
|
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
|
||||||
Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||||
*/
|
*/
|
||||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
var holder : RecyclerView.ViewHolder? = null
|
|
||||||
|
|
||||||
if (viewType == ElementType.Item.ordinal) {
|
val view = when (ElementType.values()[viewType]) {
|
||||||
if (compact) {
|
ElementType.Item -> {
|
||||||
val view = inflater.inflate(R.layout.log_item_compact, parent, false)
|
inflater.inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
|
||||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title))
|
}
|
||||||
|
ElementType.Header -> {
|
||||||
view.setOnLongClickListener(this)
|
inflater.inflate(R.layout.log_item, parent, false)
|
||||||
} else {
|
|
||||||
val view = inflater.inflate(R.layout.log_item, parent, false)
|
|
||||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
|
||||||
|
|
||||||
view.setOnLongClickListener(this)
|
|
||||||
}
|
}
|
||||||
} else if (viewType == ElementType.Header.ordinal) {
|
|
||||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
|
||||||
holder = HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return holder!!
|
return when (ElementType.values()[viewType]) {
|
||||||
|
ElementType.Item -> {
|
||||||
|
if (compact) {
|
||||||
|
ItemViewHolder(view, view.findViewById(R.id.text_title))
|
||||||
|
} else {
|
||||||
|
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElementType.Header -> {
|
||||||
|
HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +113,10 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
|||||||
holder.title.text = item.message
|
holder.title.text = item.message
|
||||||
holder.subtitle?.text = item.level
|
holder.subtitle?.text = item.level
|
||||||
|
|
||||||
holder.parent.tag = item
|
holder.parent.setOnClickListener {
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||||
|
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
} else if (item is BaseHeader) {
|
} else if (item is BaseHeader) {
|
||||||
val holder = viewHolder as HeaderViewHolder
|
val holder = viewHolder as HeaderViewHolder
|
||||||
|
|
||||||
|
52
app/src/main/java/emu/skyline/data/AppItem.kt
Normal file
52
app/src/main/java/emu/skyline/data/AppItem.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.data
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import emu.skyline.loader.AppEntry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
||||||
|
*/
|
||||||
|
class AppItem(val meta : AppEntry) : BaseItem() {
|
||||||
|
/**
|
||||||
|
* The icon of the application
|
||||||
|
*/
|
||||||
|
val icon : Bitmap?
|
||||||
|
get() = meta.icon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of the application
|
||||||
|
*/
|
||||||
|
val title : String
|
||||||
|
get() = meta.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string used as the sub-title, we currently use the author
|
||||||
|
*/
|
||||||
|
val subTitle : String?
|
||||||
|
get() = meta.author
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI of the application's image file
|
||||||
|
*/
|
||||||
|
val uri : Uri
|
||||||
|
get() = meta.uri
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format of the application ROM as a string
|
||||||
|
*/
|
||||||
|
private val type : String
|
||||||
|
get() = meta.format.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and author is used as the key
|
||||||
|
*/
|
||||||
|
override fun key() : String? {
|
||||||
|
return if (meta.author != null) meta.name + " " + meta.author else meta.name
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/java/emu/skyline/data/BaseItem.kt
Normal file
19
app/src/main/java/emu/skyline/data/BaseItem.kt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.data
|
||||||
|
|
||||||
|
import emu.skyline.adapter.BaseElement
|
||||||
|
import emu.skyline.adapter.ElementType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an abstract class that all adapter item classes inherit from
|
||||||
|
*/
|
||||||
|
abstract class BaseItem : BaseElement(ElementType.Item) {
|
||||||
|
/**
|
||||||
|
* This function returns a string used for searching
|
||||||
|
*/
|
||||||
|
abstract fun key() : String?
|
||||||
|
}
|
@ -13,7 +13,6 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.view.Surface
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
import java.io.ObjectOutputStream
|
import java.io.ObjectOutputStream
|
||||||
@ -23,12 +22,12 @@ import java.util.*
|
|||||||
/**
|
/**
|
||||||
* An enumeration of all supported ROM formats
|
* An enumeration of all supported ROM formats
|
||||||
*/
|
*/
|
||||||
enum class RomFormat(val format: Int){
|
enum class RomFormat(val extension : String) {
|
||||||
NRO(0),
|
NRO("nro"),
|
||||||
NSO(1),
|
NSO("nso"),
|
||||||
NCA(2),
|
NCA("nca"),
|
||||||
XCI(3),
|
XCI("xci"),
|
||||||
NSP(4),
|
NSP("nsp"),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,7 +184,7 @@ internal class RomFile(val context : Context, val format : RomFormat, val file :
|
|||||||
fun getAppEntry(uri : Uri) : AppEntry {
|
fun getAppEntry(uri : Uri) : AppEntry {
|
||||||
return if (hasAssets(instance)) {
|
return if (hasAssets(instance)) {
|
||||||
val rawIcon = getIcon(instance)
|
val rawIcon = getIcon(instance)
|
||||||
val icon = if (rawIcon.size != 0) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null
|
val icon = if (rawIcon.isNotEmpty()) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null
|
||||||
|
|
||||||
AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon)
|
AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon)
|
||||||
} else {
|
} else {
|
||||||
|
9
app/src/main/res/drawable/background_gradient.xml
Normal file
9
app/src/main/res/drawable/background_gradient.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:endColor="#80000000"
|
||||||
|
android:centerColor="#00000000"
|
||||||
|
android:startColor="#00000000"
|
||||||
|
android:type="linear" />
|
||||||
|
</shape>
|
@ -1,21 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout 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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
<androidx.cardview.widget.CardView
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/app_item_grid"
|
android:id="@+id/app_item_grid"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_margin="15dp"
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="@dimen/app_card_margin_half"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
android:foreground="?attr/selectableItemBackground"
|
||||||
card_view:cardCornerRadius="4dp">
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="@dimen/app_card_margin"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
|
||||||
<RelativeLayout
|
<LinearLayout
|
||||||
android:layout_width="155dp"
|
android:layout_width="155dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@ -25,16 +28,13 @@
|
|||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="155dp"
|
android:layout_height="155dp"
|
||||||
android:layout_alignParentTop="false"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:contentDescription="@string/icon"
|
android:contentDescription="@string/icon"
|
||||||
android:scaleType="centerCrop" />
|
tools:src="@drawable/default_icon" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/icon"
|
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:paddingStart="15dp"
|
android:paddingStart="15dp"
|
||||||
@ -43,14 +43,12 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
tools:ignore="RelativeOverlap" />
|
tools:text="Title" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_subtitle"
|
android:id="@+id/text_subtitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/text_title"
|
|
||||||
android:layout_alignStart="@id/text_title"
|
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:fadingEdge="horizontal"
|
android:fadingEdge="horizontal"
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
@ -58,7 +56,8 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
android:textColor="@android:color/tertiary_text_light" />
|
android:textColor="@android:color/tertiary_text_light"
|
||||||
</RelativeLayout>
|
tools:text="Subtitle" />
|
||||||
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
</LinearLayout>
|
</FrameLayout>
|
||||||
|
81
app/src/main/res/layout/app_item_grid_compact.xml
Normal file
81
app/src/main/res/layout/app_item_grid_compact.xml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout 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">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/app_item_grid"
|
||||||
|
android:layout_width="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"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="@dimen/app_card_margin"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="155dp"
|
||||||
|
android:layout_height="155dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignParentTop="false"
|
||||||
|
android:layout_centerHorizontal="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="wrap_content"
|
||||||
|
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="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="Title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
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_constrainedWidth="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="Subtitle" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</FrameLayout>
|
@ -1,40 +1,43 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout 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:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/app_item_linear"
|
|
||||||
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"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:orientation="vertical"
|
android:padding="16dp">
|
||||||
android:padding="15dp">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_alignParentStart="true"
|
android:contentDescription="@string/icon"
|
||||||
android:layout_alignParentTop="false"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_centerVertical="true"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginEnd="10dp"
|
tools:src="@drawable/default_icon" />
|
||||||
android:contentDescription="@string/icon" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignTop="@id/icon"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_toEndOf="@id/icon"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
tools:ignore="RelativeOverlap" />
|
app:layout_constraintBottom_toTopOf="@+id/text_subtitle"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Title" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_subtitle"
|
android:id="@+id/text_subtitle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/text_title"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_alignStart="@id/text_title"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
android:textColor="@android:color/tertiary_text_light" />
|
android:textColor="@android:color/tertiary_text_light"
|
||||||
</RelativeLayout>
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/text_title"
|
||||||
|
tools:text="SubTitle" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
<string-array name="layout_type">
|
<string-array name="layout_type">
|
||||||
<item>List</item>
|
<item>List</item>
|
||||||
<item>Grid</item>
|
<item>Grid</item>
|
||||||
|
<item>Grid Compact</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="layout_type_val">
|
<string-array name="layout_type_val">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="app_theme">
|
<string-array name="app_theme">
|
||||||
<item>Light</item>
|
<item>Light</item>
|
||||||
|
5
app/src/main/res/values/dimens.xml
Normal file
5
app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="app_card_margin">8dp</dimen>
|
||||||
|
<dimen name="app_card_margin_half">4dp</dimen>
|
||||||
|
</resources>
|
Loading…
Reference in New Issue
Block a user