diff --git a/app/src/main/java/com/raofflineproxy/ui/CachedGamesFragment.kt b/app/src/main/java/com/raofflineproxy/ui/CachedGamesFragment.kt index 654e5b46..e75ba805 100644 --- a/app/src/main/java/com/raofflineproxy/ui/CachedGamesFragment.kt +++ b/app/src/main/java/com/raofflineproxy/ui/CachedGamesFragment.kt @@ -41,6 +41,10 @@ class CachedGamesFragment : Fragment() { private val collapsedConsoleIds = mutableSetOf() private var currentGames: List = emptyList() private var gamesAdapter: CachedGamesAdapter? = null + private var headerAdapter: CachedGamesHeaderAdapter? = null + private var headerState = CachedGamesHeaderAdapter.HeaderState() + private var searchOpen = false + private var searchQuery = "" private var queueDialog: AlertDialog? = null private val romFolderPickerLauncher = registerForActivityResult( @@ -65,7 +69,7 @@ class CachedGamesFragment : Fragment() { onHeaderClick = { consoleId -> if (!collapsedConsoleIds.remove(consoleId)) collapsedConsoleIds.add(consoleId) saveCollapsedState() - gamesAdapter?.submitList(buildGroupedList(currentGames, collapsedConsoleIds)) + submitGames() }, onDelete = viewModel::deleteCachedGame, onDeleteConsole = { header -> @@ -108,8 +112,20 @@ class CachedGamesFragment : Fragment() { .create() .also { it.setCanceledOnTouchOutside(false) } .show() + }, + onSearchChanged = { query -> + searchQuery = query + publishHeaderState(headerState) + submitGames() + }, + onSearchToggled = { open -> + searchOpen = open + if (!open) searchQuery = "" + publishHeaderState(headerState) + submitGames() } ) + this.headerAdapter = headerAdapter view.findViewById(R.id.rv_cached_games).apply { layoutManager = LinearLayoutManager(requireContext()) @@ -121,7 +137,7 @@ class CachedGamesFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.cachedGames.collect { games -> currentGames = games - gamesAdapter?.submitList(buildGroupedList(games, collapsedConsoleIds)) + submitGames() } } @@ -146,7 +162,7 @@ class CachedGamesFragment : Fragment() { ) else -> queuedStatusText(state) } - headerAdapter.update( + publishHeaderState( CachedGamesHeaderAdapter.HeaderState( smartCacheEnabled = smartCacheEnabled, showSmartCache = showSmartCache, @@ -166,6 +182,16 @@ class CachedGamesFragment : Fragment() { queueDialog?.dismiss() queueDialog = null gamesAdapter = null + headerAdapter = null + } + + private fun publishHeaderState(base: CachedGamesHeaderAdapter.HeaderState) { + headerState = base.copy(searchOpen = searchOpen, searchQuery = searchQuery) + headerAdapter?.update(headerState) + } + + private fun submitGames() { + gamesAdapter?.submitList(buildGroupedList(currentGames, collapsedConsoleIds, searchQuery)) } private fun updateQueueDialog(estimate: QueueEstimate?) { @@ -289,6 +315,17 @@ class CachedGamesFragment : Fragment() { } private fun buildGroupedList( + games: List, + collapsedConsoleIds: Set, + query: String = "" +): List { + val searching = query.isNotBlank() + val visibleGames = if (searching) games.filter { it.title.contains(query.trim(), ignoreCase = true) } else games + val collapsed = if (searching) emptySet() else collapsedConsoleIds + return groupByConsole(visibleGames, collapsed) +} + +private fun groupByConsole( games: List, collapsedConsoleIds: Set ): List { diff --git a/app/src/main/java/com/raofflineproxy/ui/CachedGamesHeaderAdapter.kt b/app/src/main/java/com/raofflineproxy/ui/CachedGamesHeaderAdapter.kt index db257b32..c7a7c40a 100644 --- a/app/src/main/java/com/raofflineproxy/ui/CachedGamesHeaderAdapter.kt +++ b/app/src/main/java/com/raofflineproxy/ui/CachedGamesHeaderAdapter.kt @@ -2,7 +2,12 @@ package com.raofflineproxy.ui import android.view.LayoutInflater import android.view.View +import android.text.Editable +import android.text.TextWatcher import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton @@ -11,7 +16,9 @@ import com.raofflineproxy.R class CachedGamesHeaderAdapter( private val onSmartCache: () -> Unit, private val onScan: () -> Unit, - private val onClear: () -> Unit + private val onClear: () -> Unit, + private val onSearchChanged: (String) -> Unit, + private val onSearchToggled: (Boolean) -> Unit ) : RecyclerView.Adapter() { private var state: HeaderState = HeaderState() @@ -22,11 +29,15 @@ class CachedGamesHeaderAdapter( val scanEnabled: Boolean = false, val clearEnabled: Boolean = true, val showNoCachedGames: Boolean = false, - val statusText: String? = null + val statusText: String? = null, + val searchOpen: Boolean = false, + val searchQuery: String = "" ) inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val btnSmartCache: MaterialButton = view.findViewById(R.id.btn_smart_cache) + val btnSearch: MaterialButton = view.findViewById(R.id.btn_search) + val etSearch: EditText = view.findViewById(R.id.et_search) val btnScan: MaterialButton = view.findViewById(R.id.btn_scan_roms) val btnClear: MaterialButton = view.findViewById(R.id.btn_clear_cache) val tvScanHint: TextView = view.findViewById(R.id.tv_scan_hint) @@ -36,9 +47,42 @@ class CachedGamesHeaderAdapter( btnSmartCache.setOnClickListener { onSmartCache() } btnScan.setOnClickListener { onScan() } btnClear.setOnClickListener { onClear() } + btnSearch.setOnClickListener { onSearchToggled(!state.searchOpen) } + etSearch.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + override fun afterTextChanged(s: Editable?) { + val query = s?.toString().orEmpty() + if (query != state.searchQuery) onSearchChanged(query) + } + }) + etSearch.setOnEditorActionListener { v, actionId, _ -> + if (actionId != EditorInfo.IME_ACTION_SEARCH) return@setOnEditorActionListener false + hideKeyboard(v) + true + } + } + + private fun hideKeyboard(target: View) { + val imm = target.context.getSystemService(InputMethodManager::class.java) + imm?.hideSoftInputFromWindow(target.windowToken, 0) + } + + private fun bindSearch(s: HeaderState) { + val wasOpen = etSearch.visibility == View.VISIBLE + etSearch.visibility = if (s.searchOpen) View.VISIBLE else View.GONE + btnSearch.setIconResource(if (s.searchOpen) R.drawable.ic_close else R.drawable.ic_search) + if (etSearch.text.toString() != s.searchQuery) etSearch.setText(s.searchQuery) + if (s.searchOpen && !wasOpen) { + etSearch.requestFocus() + etSearch.context.getSystemService(InputMethodManager::class.java) + ?.showSoftInput(etSearch, InputMethodManager.SHOW_IMPLICIT) + } + if (!s.searchOpen && wasOpen) hideKeyboard(etSearch) } fun bind(s: HeaderState) { + bindSearch(s) btnSmartCache.visibility = if (s.showSmartCache) View.VISIBLE else View.GONE btnSmartCache.isEnabled = s.smartCacheEnabled btnSmartCache.alpha = if (s.smartCacheEnabled) 1f else 0.38f @@ -50,7 +94,8 @@ class CachedGamesHeaderAdapter( btnClear.alpha = if (s.clearEnabled) 1f else 0.38f tvScanHint.text = s.statusText - tvScanHint.visibility = if (s.statusText.isNullOrEmpty()) View.GONE else View.VISIBLE + tvScanHint.visibility = + if (s.statusText.isNullOrEmpty() || s.searchOpen) View.GONE else View.VISIBLE tvNoCachedGames.visibility = if (s.showNoCachedGames) View.VISIBLE else View.GONE } diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..246d4f34 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..26b55f11 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_cached_games_header.xml b/app/src/main/res/layout/item_cached_games_header.xml index 9f5cb0ba..bc360336 100644 --- a/app/src/main/res/layout/item_cached_games_header.xml +++ b/app/src/main/res/layout/item_cached_games_header.xml @@ -29,8 +29,33 @@ android:textSize="12sp" android:visibility="gone" /> + + + + %1$d minutes %1$d h %2$d min Start Smart Cache + Search games + Search games Scan ROM folder Clear all cache No games cached yet. Add a ROM or scan your ROM folder while online.