From 4db75022d26798ffc0be25e468aae10559c08a52 Mon Sep 17 00:00:00 2001 From: Willi Ye Date: Mon, 8 Feb 2021 18:41:49 +0100 Subject: [PATCH] Add filter and move search bar down * Fix crashes --- app/src/main/java/emu/skyline/MainActivity.kt | 94 ++++++--- .../main/java/emu/skyline/MainViewModel.kt | 24 ++- .../emu/skyline/adapter/GenericAdapter.kt | 3 +- .../java/emu/skyline/views/SearchBarView.kt | 35 ---- app/src/main/res/layout/app_dialog.xml | 2 +- app/src/main/res/layout/app_item_grid.xml | 84 ++++---- app/src/main/res/layout/button_dialog.xml | 125 ++++++----- app/src/main/res/layout/main_activity.xml | 147 +++++++++++-- app/src/main/res/layout/stick_dialog.xml | 197 +++++++++--------- app/src/main/res/layout/view_search_bar.xml | 143 +++++-------- app/src/main/res/values-night-v27/styles.xml | 8 - app/src/main/res/values-night/colors.xml | 2 +- app/src/main/res/values-night/styles.xml | 9 - app/src/main/res/values-night/themes.xml | 8 + app/src/main/res/values-v27/styles.xml | 8 - app/src/main/res/values/attrs.xml | 3 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 23 +- app/src/main/res/values/themes.xml | 24 +++ ..._search_bar_xml_constraintlayout_scene.xml | 8 +- build.gradle | 2 +- 22 files changed, 520 insertions(+), 434 deletions(-) delete mode 100644 app/src/main/res/values-night-v27/styles.xml delete mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values-night/themes.xml delete mode 100644 app/src/main/res/values-v27/styles.xml create mode 100644 app/src/main/res/values/themes.xml diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index d8d913024..a0c05abbf 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -17,12 +17,15 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import androidx.core.content.res.use import androidx.core.graphics.drawable.toBitmap +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.core.view.size import androidx.lifecycle.observe import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import emu.skyline.adapter.AppViewItem @@ -33,6 +36,7 @@ import emu.skyline.data.AppItem import emu.skyline.data.DataItem import emu.skyline.data.HeaderItem import emu.skyline.databinding.MainActivityBinding +import emu.skyline.loader.AppEntry import emu.skyline.loader.LoaderResult import emu.skyline.loader.RomFormat import emu.skyline.utils.Settings @@ -41,6 +45,10 @@ import kotlin.math.ceil @AndroidEntryPoint class MainActivity : AppCompatActivity() { + companion object { + private val formatOrder = arrayOf(RomFormat.NSP, RomFormat.XCI, RomFormat.NRO, RomFormat.NSO, RomFormat.NCA) + } + private val binding by lazy { MainActivityBinding.inflate(layoutInflater) } @Inject @@ -54,16 +62,32 @@ class MainActivity : AppCompatActivity() { private val viewModel by viewModels() + private var formatFilter : RomFormat? = null + private var appEntries : Map>? = null + + private var refreshIconVisible = false + set(visible) { + field = visible + binding.refreshIcon.apply { + if (visible != isVisible) { + binding.refreshIcon.alpha = if (visible) 0f else 1f + animate().alpha(if (visible) 1f else 0f).withStartAction { isVisible = true }.withEndAction { isInvisible = !visible }.apply { duration = 500 }.start() + } + } + } + private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog) override fun onCreate(savedInstanceState : Bundle?) { // Need to create new instance of settings, dependency injection happens - AppCompatDelegate.setDefaultNightMode(when ((Settings(this).appTheme.toInt())) { - 0 -> AppCompatDelegate.MODE_NIGHT_NO - 1 -> AppCompatDelegate.MODE_NIGHT_YES - 2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED - }) + AppCompatDelegate.setDefaultNightMode( + when ((Settings(this).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(binding.root) @@ -79,13 +103,26 @@ class MainActivity : AppCompatActivity() { setOnRefreshListener { loadRoms(false) } } + for (format in formatOrder) { + binding.chipGroup.addView(Chip(this, null, R.attr.chipChoiceStyle).apply { text = format.name }) + } + binding.chipGroup.setOnCheckedChangeListener { group, checkedId -> + for (i in 0 until group.childCount) { + if (group.getChildAt(i).id == checkedId) { + formatFilter = if (i == 0) null else formatOrder[i - 1] + populateAdapter() + break + } + } + } + viewModel.stateData.observe(owner = this, onChanged = ::handleState) loadRoms(!settings.refreshRequired) binding.searchBar.apply { - setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) } - setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) } - setRefreshIconListener { loadRoms(false) } + binding.logIcon.setOnClickListener { startActivity(Intent(context, LogActivity::class.java)) } + binding.settingsIcon.setOnClickListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) } + binding.refreshIcon.setOnClickListener { loadRoms(false) } addTextChangedListener(afterTextChanged = { editable -> editable?.let { text -> adapter.filter.filter(text.toString()) } }) @@ -95,7 +132,7 @@ class MainActivity : AppCompatActivity() { } } window.decorView.findViewById(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode -> - binding.searchBar.refreshIconVisible = !isInTouchMode + refreshIconVisible = !isInTouchMode } } @@ -184,32 +221,38 @@ class MainActivity : AppCompatActivity() { binding.appList.layoutManager = CustomLayoutManager(gridSpan) setAppListDecoration() - if (settings.searchLocation.isEmpty()) { - 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 + if (settings.searchLocation.isEmpty()) startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION + }, 1) + } - startActivityForResult(intent, 1) + private fun getDataItems() = mutableListOf().apply { + appEntries?.let { entries -> + val formats = formatFilter?.let { arrayOf(it) } ?: formatOrder + for (format in formats) { + entries[format]?.let { + add(HeaderItem(format.name)) + it.forEach { entry -> add(AppItem(entry)) } + } + } } } private fun handleState(state : MainState) = when (state) { MainState.Loading -> { - binding.searchBar.animateRefreshIcon() + binding.refreshIcon.animate().rotationBy(-180f) binding.swipeRefreshLayout.isRefreshing = true } + is MainState.Loaded -> { binding.swipeRefreshLayout.isRefreshing = false - val formatOrder = arrayOf(RomFormat.NSP, RomFormat.NRO, RomFormat.NSO, RomFormat.NCA) - val items = mutableListOf() - for (format in formatOrder) { - state.items[format]?.let { - items.add(HeaderItem(format.name)) - it.forEach { entry -> items.add(AppItem(entry)) } - } - } - populateAdapter(items) + appEntries = state.items + populateAdapter() } + is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show() } @@ -233,7 +276,8 @@ class MainActivity : AppCompatActivity() { settings.refreshRequired = false } - private fun populateAdapter(items : List) { + private fun populateAdapter() { + val items = getDataItems() adapter.setItems(items.map { when (it) { is HeaderItem -> HeaderViewItem(it.title) diff --git a/app/src/main/java/emu/skyline/MainViewModel.kt b/app/src/main/java/emu/skyline/MainViewModel.kt index 544471851..3652ee2fe 100644 --- a/app/src/main/java/emu/skyline/MainViewModel.kt +++ b/app/src/main/java/emu/skyline/MainViewModel.kt @@ -15,8 +15,9 @@ import emu.skyline.utils.toFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File -import java.io.IOException +import java.util.* import javax.inject.Inject +import kotlin.collections.HashMap sealed class MainState { object Loading : MainState() @@ -50,7 +51,7 @@ class MainViewModel @Inject constructor(private val romProvider : RomProvider) : val romsFile = File(context.filesDir.canonicalPath + "/roms.bin") viewModelScope.launch(Dispatchers.IO) { - if (loadFromFile) { + if (loadFromFile && romsFile.exists()) { try { state = MainState.Loaded(fromFile(romsFile)) return@launch @@ -59,14 +60,19 @@ class MainViewModel @Inject constructor(private val romProvider : RomProvider) : } } - val romElements = romProvider.loadRoms(searchLocation) - try { - romElements.toFile(romsFile) - } catch (e : IOException) { - Log.w(TAG, "Ran into exception while saving: ${e.message}") + state = if (searchLocation.toString().isEmpty()) { + @Suppress("ReplaceWithEnumMap") + MainState.Loaded(HashMap()) + } else { + try { + val romElements = romProvider.loadRoms(searchLocation) + romElements.toFile(romsFile) + MainState.Loaded(romElements) + } catch (e : Exception) { + Log.w(TAG, "Ran into exception while saving: ${e.message}") + MainState.Error(e) + } } - - state = MainState.Loaded(romElements) } } } diff --git a/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt b/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt index f6eba6a47..0ee8aedb1 100644 --- a/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt @@ -5,7 +5,6 @@ package emu.skyline.adapter -import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Filter import android.widget.Filterable @@ -101,7 +100,7 @@ class GenericAdapter : RecyclerView.Adapter>(), F val avgScore = topResults.sumByDouble { it.score } / topResults.size for (result in topResults) - if (result.score > avgScore) filterData.add(result.item) + if (result.score >= avgScore) filterData.add(result.item) results.values = filterData results.count = filterData.size diff --git a/app/src/main/java/emu/skyline/views/SearchBarView.kt b/app/src/main/java/emu/skyline/views/SearchBarView.kt index baaaa6151..588c8cc5b 100644 --- a/app/src/main/java/emu/skyline/views/SearchBarView.kt +++ b/app/src/main/java/emu/skyline/views/SearchBarView.kt @@ -4,15 +4,10 @@ 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.databinding.ViewSearchBarBinding -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) { private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this) @@ -21,32 +16,6 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu 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) = binding.refreshIcon.setOnClickListener(listener) - fun setLogIconListener(listener : OnClickListener) = binding.logIcon.setOnClickListener(listener) - fun setSettingsIconListener(listener : OnClickListener) = binding.settingsIcon.setOnClickListener(listener) - - var refreshIconVisible = false - set(visible) { - field = visible - binding.refreshIcon.apply { - if (visible != isVisible) { - binding.refreshIcon.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() = binding.searchField.text set(value) = binding.searchField.setText(value) @@ -65,10 +34,6 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu } } - fun animateRefreshIcon() { - binding.refreshIcon.animate().rotationBy(-180f) - } - fun addTextChangedListener( beforeTextChanged : ( text : CharSequence?, diff --git a/app/src/main/res/layout/app_dialog.xml b/app/src/main/res/layout/app_dialog.xml index 59cccc889..ca81c1e7b 100644 --- a/app/src/main/res/layout/app_dialog.xml +++ b/app/src/main/res/layout/app_dialog.xml @@ -14,7 +14,7 @@ android:layout_height="150dp" android:contentDescription="@string/icon" android:focusable="false" - app:shapeAppearanceOverlay="@style/roundedAppImage" + app:shapeAppearanceOverlay="@style/RoundedAppImage" tools:src="@drawable/default_icon" /> + + + android:layout_marginStart="14dp" + android:layout_marginTop="14dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="14dp" + app:cardCornerRadius="16dp" + app:cardElevation="2dp"> - - - + android:adjustViewBounds="true" + android:contentDescription="@string/icon" + tools:src="@drawable/default_icon" /> + 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" /> + 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" /> diff --git a/app/src/main/res/layout/button_dialog.xml b/app/src/main/res/layout/button_dialog.xml index f03bd16aa..984bc26ad 100644 --- a/app/src/main/res/layout/button_dialog.xml +++ b/app/src/main/res/layout/button_dialog.xml @@ -1,81 +1,80 @@ + + - - + android:text="@string/use_button_axis" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.AppCompat.Display1" + android:textSize="16sp" /> + android:id="@+id/button_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:animateLayoutChanges="true" + android:gravity="center"> + android:id="@+id/button_icon" + android:layout_width="50dp" + android:layout_height="50dp" + android:contentDescription="@string/buttons" + android:outlineProvider="bounds" + android:src="@drawable/ic_button" + app:tint="?android:attr/textColorPrimary" /> + android:id="@+id/button_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignStart="@id/button_icon" + android:layout_alignTop="@id/button_icon" + android:layout_alignEnd="@id/button_icon" + android:layout_alignBottom="@id/button_icon" + android:alpha="0.25" + android:fontFamily="monospace" + android:gravity="center" + android:includeFontPadding="false" + android:textSize="27sp" /> + android:id="@+id/button_seekbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:max="100" + android:progress="25" + android:secondaryProgressTintMode="screen" + android:visibility="gone" />