diff options
Diffstat (limited to 'src/android/app')
32 files changed, 1031 insertions, 626 deletions
| diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 552d4a721..d8ef02ac1 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -155,6 +155,9 @@ dependencies {      implementation("org.ini4j:ini4j:0.5.4")      implementation("androidx.constraintlayout:constraintlayout:2.1.4")      implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") +    implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") +    implementation("androidx.navigation:navigation-ui-ktx:2.5.3") +    implementation("info.debatty:java-string-similarity:2.0.0")  }  fun getVersion(): String { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f1f92841c..fd174fd2d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -13,7 +13,6 @@ import android.view.View  import android.view.WindowManager  import android.view.inputmethod.InputMethodManager  import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.FragmentActivity  import androidx.preference.PreferenceManager  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.slider.Slider.OnChangeListener @@ -202,7 +201,7 @@ open class EmulationActivity : AppCompatActivity() {          private const val EMULATION_RUNNING_NOTIFICATION = 0x1000          @JvmStatic -        fun launch(activity: FragmentActivity, game: Game) { +        fun launch(activity: AppCompatActivity, game: Game) {              val launcher = Intent(activity, EmulationActivity::class.java)              launcher.putExtra(EXTRA_SELECTED_GAME, game)              activity.startActivity(launcher) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index af83f05c1..1102b60b1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -3,6 +3,7 @@  package org.yuzu.yuzu_emu.adapters +import android.annotation.SuppressLint  import android.graphics.Bitmap  import android.graphics.BitmapFactory  import android.view.LayoutInflater @@ -11,29 +12,25 @@ import android.view.ViewGroup  import android.widget.ImageView  import androidx.appcompat.app.AppCompatActivity  import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter  import androidx.recyclerview.widget.RecyclerView  import coil.load -import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.CardGameBinding  import org.yuzu.yuzu_emu.activities.EmulationActivity  import org.yuzu.yuzu_emu.model.Game -import kotlin.collections.ArrayList - -/** - * This adapter gets its information from a database Cursor. This fact, paired with the usage of - * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) - * large dataset. - */ -class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) : -    RecyclerView.Adapter<GameAdapter.GameViewHolder>(), +import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder + +class GameAdapter(private val activity: AppCompatActivity) : +    ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),      View.OnClickListener {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {          // Create a new view. -        val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) +        val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)          binding.root.setOnClickListener(this)          // Use that view to create a ViewHolder. @@ -41,12 +38,10 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<      }      override fun onBindViewHolder(holder: GameViewHolder, position: Int) { -        holder.bind(games[position]) +        holder.bind(currentList[position])      } -    override fun getItemCount(): Int { -        return games.size -    } +    override fun getItemCount(): Int = currentList.size      /**       * Launches the game that was clicked on. @@ -55,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<       */      override fun onClick(view: View) {          val holder = view.tag as GameViewHolder -        EmulationActivity.launch((view.context as AppCompatActivity), holder.game) +        EmulationActivity.launch(activity, holder.game)      }      inner class GameViewHolder(val binding: CardGameBinding) : @@ -74,7 +69,6 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<                  val bitmap = decodeGameIcon(game.path)                  binding.imageGameScreen.load(bitmap) {                      error(R.drawable.no_icon) -                    crossfade(true)                  }              } @@ -87,9 +81,15 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<          }      } -    fun swapData(games: ArrayList<Game>) { -        this.games = games -        notifyDataSetChanged() +    private class DiffCallback : DiffUtil.ItemCallback<Game>() { +        override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { +            return oldItem.gameId == newItem.gameId +        } + +        @SuppressLint("DiffUtilEquals") +        override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { +            return oldItem == newItem +        }      }      private fun decodeGameIcon(uri: String): Bitmap? { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt new file mode 100644 index 000000000..2bec2de87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt @@ -0,0 +1,55 @@ +package org.yuzu.yuzu_emu.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding +import org.yuzu.yuzu_emu.model.HomeOption + +class HomeOptionAdapter(private val activity: AppCompatActivity, var options: List<HomeOption>) : +    RecyclerView.Adapter<HomeOptionAdapter.HomeOptionViewHolder>(), +    View.OnClickListener { +    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { +        val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) +        binding.root.setOnClickListener(this) +        return HomeOptionViewHolder(binding) +    } + +    override fun getItemCount(): Int { +        return options.size +    } + +    override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) { +        holder.bind(options[position]) +    } + +    override fun onClick(view: View) { +        val holder = view.tag as HomeOptionViewHolder +        holder.option.onClick.invoke() +    } + +    inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : +        RecyclerView.ViewHolder(binding.root) { +        lateinit var option: HomeOption + +        init { +            itemView.tag = this +        } + +        fun bind(option: HomeOption) { +            this.option = option +            binding.optionTitle.text = activity.resources.getString(option.titleId) +            binding.optionDescription.text = activity.resources.getString(option.descriptionId) +            binding.optionIcon.setImageDrawable( +                ResourcesCompat.getDrawable( +                    activity.resources, +                    option.iconId, +                    activity.theme +                ) +            ) +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 0f2c23827..e4bdcc991 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -15,6 +15,7 @@ import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding +import com.google.android.material.color.MaterialColors  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding @@ -50,6 +51,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {          setSupportActionBar(binding.toolbarSettings)          supportActionBar!!.setDisplayHomeAsUpEnabled(true) +        ThemeHelper.setNavigationBarColor( +            this, +            MaterialColors.getColor(window.decorView, R.attr.colorSurface) +        ) +          setInsets()      } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt new file mode 100644 index 000000000..dac9e67d5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt @@ -0,0 +1,281 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeOption +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GameHelper +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import java.io.IOException + +class OptionsFragment : Fragment() { +    private var _binding: FragmentOptionsBinding? = null +    private val binding get() = _binding!! + +    private val gamesViewModel: GamesViewModel by activityViewModels() + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        _binding = FragmentOptionsBinding.inflate(layoutInflater) +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        val optionsList: List<HomeOption> = listOf( +            HomeOption( +                R.string.add_games, +                R.string.add_games_description, +                R.drawable.ic_add +            ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, +            HomeOption( +                R.string.install_prod_keys, +                R.string.install_prod_keys_description, +                R.drawable.ic_unlock +            ) { getProdKey.launch(arrayOf("*/*")) }, +            HomeOption( +                R.string.install_amiibo_keys, +                R.string.install_amiibo_keys_description, +                R.drawable.ic_nfc +            ) { getAmiiboKey.launch(arrayOf("*/*")) }, +            HomeOption( +                R.string.install_gpu_driver, +                R.string.install_gpu_driver_description, +                R.drawable.ic_input +            ) { driverInstaller() }, +            HomeOption( +                R.string.settings, +                R.string.settings_description, +                R.drawable.ic_settings +            ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } +        ) + +        binding.optionsList.apply { +            layoutManager = LinearLayoutManager(requireContext()) +            adapter = HomeOptionAdapter(requireActivity() as AppCompatActivity, optionsList) +        } + +        requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( +            MaterialColors.getColor( +                binding.root, +                R.attr.colorSurface +            ), ThemeHelper.SYSTEM_BAR_ALPHA +        ) + +        setInsets() +    } + +    override fun onDestroyView() { +        super.onDestroyView() +        _binding = null +    } + +    private fun driverInstaller() { +        // Get the driver name for the dialog message. +        var driverName = GpuDriverHelper.customDriverName +        if (driverName == null) { +            driverName = getString(R.string.system_gpu_driver) +        } + +        MaterialAlertDialogBuilder(requireContext()) +            .setTitle(getString(R.string.select_gpu_driver_title)) +            .setMessage(driverName) +            .setNegativeButton(android.R.string.cancel, null) +            .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> +                GpuDriverHelper.installDefaultDriver(requireContext()) +                Toast.makeText( +                    requireContext(), +                    R.string.select_gpu_driver_use_default, +                    Toast.LENGTH_SHORT +                ).show() +            } +            .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> +                getDriver.launch(arrayOf("application/zip")) +            } +            .show() +    } + +    private fun setInsets() = +        ViewCompat.setOnApplyWindowInsetsListener(binding.scrollViewOptions) { view: View, windowInsets: WindowInsetsCompat -> +            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) +            view.setPadding( +                insets.left, +                insets.top, +                insets.right, +                insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) +            ) +            windowInsets +        } + +    private val getGamesDirectory = +        registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> +            if (result == null) +                return@registerForActivityResult + +            val takeFlags = +                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION +            requireActivity().contentResolver.takePersistableUriPermission( +                result, +                takeFlags +            ) + +            // When a new directory is picked, we currently will reset the existing games +            // database. This effectively means that only one game directory is supported. +            PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() +                .putString(GameHelper.KEY_GAME_PATH, result.toString()) +                .apply() + +            gamesViewModel.reloadGames(true) +        } + +    private val getProdKey = +        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> +            if (result == null) +                return@registerForActivityResult + +            val takeFlags = +                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION +            requireActivity().contentResolver.takePersistableUriPermission( +                result, +                takeFlags +            ) + +            val dstPath = DirectoryInitialization.userDirectory + "/keys/" +            if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) { +                if (NativeLibrary.reloadKeys()) { +                    Toast.makeText( +                        requireContext(), +                        R.string.install_keys_success, +                        Toast.LENGTH_SHORT +                    ).show() +                    gamesViewModel.reloadGames(true) +                } else { +                    Toast.makeText( +                        requireContext(), +                        R.string.install_keys_failure, +                        Toast.LENGTH_LONG +                    ).show() +                } +            } +        } + +    private val getAmiiboKey = +        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> +            if (result == null) +                return@registerForActivityResult + +            val takeFlags = +                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION +            requireActivity().contentResolver.takePersistableUriPermission( +                result, +                takeFlags +            ) + +            val dstPath = DirectoryInitialization.userDirectory + "/keys/" +            if (FileUtil.copyUriToInternalStorage( +                    requireContext(), +                    result, +                    dstPath, +                    "key_retail.bin" +                ) +            ) { +                if (NativeLibrary.reloadKeys()) { +                    Toast.makeText( +                        requireContext(), +                        R.string.install_keys_success, +                        Toast.LENGTH_SHORT +                    ).show() +                } else { +                    Toast.makeText( +                        requireContext(), +                        R.string.install_amiibo_keys_failure, +                        Toast.LENGTH_LONG +                    ).show() +                } +            } +        } + +    private val getDriver = +        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> +            if (result == null) +                return@registerForActivityResult + +            val takeFlags = +                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION +            requireActivity().contentResolver.takePersistableUriPermission( +                result, +                takeFlags +            ) + +            val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) +            progressBinding.progressBar.isIndeterminate = true +            val installationDialog = MaterialAlertDialogBuilder(requireContext()) +                .setTitle(R.string.installing_driver) +                .setView(progressBinding.root) +                .show() + +            lifecycleScope.launch { +                withContext(Dispatchers.IO) { +                    // Ignore file exceptions when a user selects an invalid zip +                    try { +                        GpuDriverHelper.installCustomDriver(requireContext(), result) +                    } catch (_: IOException) { +                    } + +                    withContext(Dispatchers.Main) { +                        installationDialog.dismiss() + +                        val driverName = GpuDriverHelper.customDriverName +                        if (driverName != null) { +                            Toast.makeText( +                                requireContext(), +                                getString( +                                    R.string.select_gpu_driver_install_success, +                                    driverName +                                ), +                                Toast.LENGTH_SHORT +                            ).show() +                        } else { +                            Toast.makeText( +                                requireContext(), +                                R.string.select_gpu_driver_error, +                                Toast.LENGTH_LONG +                            ).show() +                        } +                    } +                } +            } +        } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index fde99f1a2..709a5b976 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -1,18 +1,58 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +  package org.yuzu.yuzu_emu.model  import androidx.lifecycle.LiveData  import androidx.lifecycle.MutableLiveData  import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.utils.GameHelper  class GamesViewModel : ViewModel() { -    private val _games = MutableLiveData<ArrayList<Game>>() -    val games: LiveData<ArrayList<Game>> get() = _games +    private val _games = MutableLiveData<List<Game>>(emptyList()) +    val games: LiveData<List<Game>> get() = _games + +    private val _searchedGames = MutableLiveData<List<Game>>(emptyList()) +    val searchedGames: LiveData<List<Game>> get() = _searchedGames + +    private val _isReloading = MutableLiveData(false) +    val isReloading: LiveData<Boolean> get() = _isReloading + +    private val _shouldSwapData = MutableLiveData(false) +    val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData      init { -        _games.value = ArrayList() +        reloadGames(false) +    } + +    fun setSearchedGames(games: List<Game>) { +        _searchedGames.postValue(games) +    } + +    fun setShouldSwapData(shouldSwap: Boolean) { +        _shouldSwapData.postValue(shouldSwap)      } -    fun setGames(games: ArrayList<Game>) { -        _games.value = games +    fun reloadGames(directoryChanged: Boolean) { +        if (isReloading.value == true) +            return +        _isReloading.postValue(true) + +        viewModelScope.launch { +            withContext(Dispatchers.IO) { +                NativeLibrary.resetRomMetadata() +                _games.postValue(GameHelper.getGames()) +                _isReloading.postValue(false) + +                if (directoryChanged) { +                    setShouldSwapData(true) +                } +            } +        }      }  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt new file mode 100644 index 000000000..c995ff12c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +data class HomeOption( +    val titleId: Int, +    val descriptionId: Int, +    val iconId: Int, +    val onClick: () -> Unit +) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt new file mode 100644 index 000000000..74f12429c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -0,0 +1,17 @@ +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { +    private val _navigationVisible = MutableLiveData(true) +    val navigationVisible: LiveData<Boolean> get() = _navigationVisible + +    fun setNavigationVisible(visible: Boolean) { +        if (_navigationVisible.value == visible) { +            return +        } +        _navigationVisible.value = visible +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt new file mode 100644 index 000000000..0c609798b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.color.MaterialColors +import com.google.android.material.search.SearchView +import com.google.android.material.search.SearchView.TransitionState +import info.debatty.java.stringsimilarity.Jaccard +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.GameAdapter +import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding +import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ThemeHelper +import java.util.Locale + +class GamesFragment : Fragment() { +    private var _binding: FragmentGamesBinding? = null +    private val binding get() = _binding!! + +    private val gamesViewModel: GamesViewModel by activityViewModels() +    private val homeViewModel: HomeViewModel by activityViewModels() + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        _binding = FragmentGamesBinding.inflate(inflater) +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        // Use custom back navigation so the user doesn't back out of the app when trying to back +        // out of the search view +        requireActivity().onBackPressedDispatcher.addCallback( +            viewLifecycleOwner, +            object : OnBackPressedCallback(true) { +                override fun handleOnBackPressed() { +                    if (binding.searchView.currentTransitionState == TransitionState.SHOWN) { +                        binding.searchView.hide() +                    } else { +                        requireActivity().finish() +                    } +                } +            }) + +        binding.gridGames.apply { +            layoutManager = AutofitGridLayoutManager( +                requireContext(), +                requireContext().resources.getDimensionPixelSize(R.dimen.card_width) +            ) +            adapter = GameAdapter(requireActivity() as AppCompatActivity) +        } +        setUpSearch() + +        // Add swipe down to refresh gesture +        binding.swipeRefresh.setOnRefreshListener { +            gamesViewModel.reloadGames(false) +        } + +        // Set theme color to the refresh animation's background +        binding.swipeRefresh.setProgressBackgroundColorSchemeColor( +            MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) +        ) +        binding.swipeRefresh.setColorSchemeColors( +            MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) +        ) + +        // Watch for when we get updates to any of our games lists +        gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading -> +            binding.swipeRefresh.isRefreshing = isReloading + +            if (!isReloading) { +                if (gamesViewModel.games.value!!.isEmpty()) { +                    binding.noticeText.visibility = View.VISIBLE +                } else { +                    binding.noticeText.visibility = View.GONE +                } +            } +        } +        gamesViewModel.games.observe(viewLifecycleOwner) { +            (binding.gridGames.adapter as GameAdapter).submitList(it) +        } +        gamesViewModel.searchedGames.observe(viewLifecycleOwner) { +            (binding.gridSearch.adapter as GameAdapter).submitList(it) +        } +        gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> +            if (shouldSwapData) { +                (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) +                gamesViewModel.setShouldSwapData(false) +            } +        } + +        // Hide bottom navigation and FAB when using the search view +        binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState -> +            when (newState) { +                TransitionState.SHOWING, +                TransitionState.SHOWN -> { +                    (binding.gridSearch.adapter as GameAdapter).submitList(emptyList()) +                    searchShown() +                } +                TransitionState.HIDDEN, +                TransitionState.HIDING -> { +                    gamesViewModel.setSearchedGames(emptyList()) +                    searchHidden() +                } +            } +        } + +        // Ensure that bottom navigation or FAB don't appear upon recreation +        val searchState = binding.searchView.currentTransitionState +        if (searchState == TransitionState.SHOWN) { +            searchShown() +        } else if (searchState == TransitionState.HIDDEN) { +            searchHidden() +        } + +        setInsets() + +        // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn +        binding.swipeRefresh.post { +            binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!! +        } +    } + +    override fun onDestroyView() { +        super.onDestroyView() +        _binding = null +    } + +    private fun searchShown() { +        homeViewModel.setNavigationVisible(false) +        requireActivity().window.statusBarColor = +            ContextCompat.getColor(requireContext(), android.R.color.transparent) +    } + +    private fun searchHidden() { +        homeViewModel.setNavigationVisible(true) +        requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( +            MaterialColors.getColor( +                binding.root, +                R.attr.colorSurface +            ), ThemeHelper.SYSTEM_BAR_ALPHA +        ) +    } + +    private inner class ScoredGame(val score: Double, val item: Game) + +    private fun setUpSearch() { +        binding.gridSearch.apply { +            layoutManager = AutofitGridLayoutManager( +                requireContext(), +                requireContext().resources.getDimensionPixelSize(R.dimen.card_width) +            ) +            adapter = GameAdapter(requireActivity() as AppCompatActivity) +        } + +        binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> +            val searchTerm = text.toString().lowercase(Locale.getDefault()) +            val searchAlgorithm = Jaccard(2) +            val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game -> +                val title = game.title.lowercase(Locale.getDefault()) +                val score = searchAlgorithm.similarity(searchTerm, title) +                if (score > 0.03) { +                    ScoredGame(score, game) +                } else { +                    null +                } +            }.sortedByDescending { it.score }.map { it.item } +            gamesViewModel.setSearchedGames(sortedList) +        } +    } + +    private fun setInsets() = +        ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> +            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) +            val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + +            view.setPadding( +                insets.left, +                insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search), +                insets.right, +                insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing +            ) +            binding.gridSearch.updatePadding( +                left = insets.left, +                top = extraListSpacing, +                right = insets.right, +                bottom = insets.bottom + extraListSpacing +            ) + +            binding.swipeRefresh.setSlingshotDistance( +                resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot) +            ) +            binding.swipeRefresh.setProgressViewOffset( +                false, +                insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start), +                insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) +            ) + +            windowInsets +        } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 69a371947..a16ca8529 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -3,42 +3,31 @@  package org.yuzu.yuzu_emu.ui.main -import android.content.DialogInterface -import android.content.Intent  import android.os.Bundle -import android.view.Menu -import android.view.MenuItem  import android.view.View -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts +import android.view.ViewGroup.MarginLayoutParams +import android.view.animation.PathInterpolator +import androidx.activity.viewModels  import androidx.appcompat.app.AppCompatActivity  import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen  import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.lifecycle.lifecycleScope -import androidx.preference.PreferenceManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.yuzu.yuzu_emu.NativeLibrary +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.color.MaterialColors +import com.google.android.material.elevation.ElevationOverlayProvider  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.activities.EmulationActivity  import org.yuzu.yuzu_emu.databinding.ActivityMainBinding -import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding -import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity -import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment +import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.utils.* -import java.io.IOException - -class MainActivity : AppCompatActivity(), MainView { -    private var platformGamesFragment: PlatformGamesFragment? = null -    private val presenter = MainPresenter(this) +class MainActivity : AppCompatActivity() {      private lateinit var binding: ActivityMainBinding +    private val homeViewModel: HomeViewModel by viewModels() +      override fun onCreate(savedInstanceState: Bundle?) {          val splashScreen = installSplashScreen()          splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -52,19 +41,36 @@ class MainActivity : AppCompatActivity(), MainView {          WindowCompat.setDecorFitsSystemWindows(window, false) -        setSupportActionBar(binding.toolbarMain) -        presenter.onCreate() -        if (savedInstanceState == null) { -            StartupHandler.handleInit(this) -            platformGamesFragment = PlatformGamesFragment() -            supportFragmentManager.beginTransaction() -                .add(R.id.games_platform_frame, platformGamesFragment!!) -                .commit() -        } else { -            platformGamesFragment = supportFragmentManager.getFragment( -                savedInstanceState, -                PlatformGamesFragment.TAG -            ) as PlatformGamesFragment? +        ThemeHelper.setNavigationBarColor( +            this, +            ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay( +                MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface), +                binding.navigationBar.elevation +            ) +        ) + +        // Set up a central host fragment that is controlled via bottom navigation with xml navigation +        val navHostFragment = +            supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment +        binding.navigationBar.setupWithNavController(navHostFragment.navController) + +        binding.statusBarShade.setBackgroundColor( +            ThemeHelper.getColorWithOpacity( +                MaterialColors.getColor( +                    binding.root, +                    R.attr.colorSurface +                ), ThemeHelper.SYSTEM_BAR_ALPHA +            ) +        ) + +        // Prevents navigation from being drawn for a short time on recreation if set to hidden +        if (homeViewModel.navigationVisible.value == false) { +            binding.navigationBar.visibility = View.INVISIBLE +            binding.statusBarShade.visibility = View.INVISIBLE +        } + +        homeViewModel.navigationVisible.observe(this) { visible -> +            showNavigation(visible)          }          // Dismiss previous notifications (should not happen unless a crash occurred) @@ -73,78 +79,24 @@ class MainActivity : AppCompatActivity(), MainView {          setInsets()      } -    override fun onSaveInstanceState(outState: Bundle) { -        super.onSaveInstanceState(outState) -        supportFragmentManager.putFragment( -            outState, -            PlatformGamesFragment.TAG, -            platformGamesFragment!! -        ) -    } - -    override fun onCreateOptionsMenu(menu: Menu): Boolean { -        menuInflater.inflate(R.menu.menu_game_grid, menu) -        return true -    } - -    /** -     * MainView -     */ -    override fun setVersionString(version: String) { -        binding.toolbarMain.subtitle = version -    } - -    override fun launchSettingsActivity(menuTag: String) { -        SettingsActivity.launch(this, menuTag, "") -    } - -    override fun launchFileListActivity(request: Int) { -        when (request) { -            MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) -            MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*")) -            MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*")) -            MainPresenter.REQUEST_SELECT_GPU_DRIVER -> { -                // Get the driver name for the dialog message. -                var driverName = GpuDriverHelper.customDriverName -                if (driverName == null) { -                    driverName = getString(R.string.system_gpu_driver) -                } - -                MaterialAlertDialogBuilder(this) -                    .setTitle(getString(R.string.select_gpu_driver_title)) -                    .setMessage(driverName) -                    .setNegativeButton(android.R.string.cancel, null) -                    .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> -                        GpuDriverHelper.installDefaultDriver(this) -                        Toast.makeText( -                            this, -                            R.string.select_gpu_driver_use_default, -                            Toast.LENGTH_SHORT -                        ).show() -                    } -                    .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> -                        getDriver.launch(arrayOf("application/zip")) -                    } -                    .show() +    private fun showNavigation(visible: Boolean) { +        binding.navigationBar.animate().apply { +            if (visible) { +                binding.navigationBar.visibility = View.VISIBLE +                binding.navigationBar.translationY = binding.navigationBar.height.toFloat() * 2 +                duration = 300 +                translationY(0f) +                interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) +            } else { +                duration = 300 +                translationY(binding.navigationBar.height.toFloat() * 2) +                interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)              } -        } -    } - -    /** -     * Called by the framework whenever any actionbar/toolbar icon is clicked. -     * -     * @param item The icon that was clicked on. -     * @return True if the event was handled, false to bubble it up to the OS. -     */ -    override fun onOptionsItemSelected(item: MenuItem): Boolean { -        return presenter.handleOptionSelection(item.itemId) -    } - -    private fun refreshFragment() { -        if (platformGamesFragment != null) { -            NativeLibrary.resetRomMetadata() -            platformGamesFragment!!.refresh() -        } +        }.withEndAction { +            if (!visible) { +                binding.navigationBar.visibility = View.INVISIBLE +            } +        }.start()      }      override fun onDestroy() { @@ -152,145 +104,12 @@ class MainActivity : AppCompatActivity(), MainView {          super.onDestroy()      } -    private fun setInsets() { -        ViewCompat.setOnApplyWindowInsetsListener(binding.gamesPlatformFrame) { view: View, windowInsets: WindowInsetsCompat -> +    private fun setInsets() = +        ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarShade) { view: View, windowInsets: WindowInsetsCompat ->              val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) -            view.updatePadding(left = insets.left, right = insets.right) -            InsetsHelper.insetAppBar(insets, binding.appbarMain) +            val mlpShade = view.layoutParams as MarginLayoutParams +            mlpShade.height = insets.top +            binding.statusBarShade.layoutParams = mlpShade              windowInsets          } -    } - -    private val getGamesDirectory = -        registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> -            if (result == null) -                return@registerForActivityResult - -            val takeFlags = -                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION -            contentResolver.takePersistableUriPermission( -                result, -                takeFlags -            ) - -            // When a new directory is picked, we currently will reset the existing games -            // database. This effectively means that only one game directory is supported. -            PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() -                .putString(GameHelper.KEY_GAME_PATH, result.toString()) -                .apply() -        } - -    private val getProdKey = -        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> -            if (result == null) -                return@registerForActivityResult - -            val takeFlags = -                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION -            contentResolver.takePersistableUriPermission( -                result, -                takeFlags -            ) - -            val dstPath = DirectoryInitialization.userDirectory + "/keys/" -            if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "prod.keys")) { -                if (NativeLibrary.reloadKeys()) { -                    Toast.makeText( -                        this, -                        R.string.install_keys_success, -                        Toast.LENGTH_SHORT -                    ).show() -                    refreshFragment() -                } else { -                    Toast.makeText( -                        this, -                        R.string.install_keys_failure, -                        Toast.LENGTH_LONG -                    ).show() -                } -            } -        } - -    private val getAmiiboKey = -        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> -            if (result == null) -                return@registerForActivityResult - -            val takeFlags = -                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION -            contentResolver.takePersistableUriPermission( -                result, -                takeFlags -            ) - -            val dstPath = DirectoryInitialization.userDirectory + "/keys/" -            if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) { -                if (NativeLibrary.reloadKeys()) { -                    Toast.makeText( -                        this, -                        R.string.install_keys_success, -                        Toast.LENGTH_SHORT -                    ).show() -                    refreshFragment() -                } else { -                    Toast.makeText( -                        this, -                        R.string.install_amiibo_keys_failure, -                        Toast.LENGTH_LONG -                    ).show() -                } -            } -        } - -    private val getDriver = -        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> -            if (result == null) -                return@registerForActivityResult - -            val takeFlags = -                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION -            contentResolver.takePersistableUriPermission( -                result, -                takeFlags -            ) - -            val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) -            progressBinding.progressBar.isIndeterminate = true -            val installationDialog = MaterialAlertDialogBuilder(this) -                .setTitle(R.string.installing_driver) -                .setView(progressBinding.root) -                .show() - -            lifecycleScope.launch { -                withContext(Dispatchers.IO) { -                    // Ignore file exceptions when a user selects an invalid zip -                    try { -                        GpuDriverHelper.installCustomDriver(applicationContext, result) -                    } catch (_: IOException) { -                    } - -                    withContext(Dispatchers.Main) { -                        installationDialog.dismiss() - -                        val driverName = GpuDriverHelper.customDriverName -                        if (driverName != null) { -                            Toast.makeText( -                                applicationContext, -                                getString( -                                    R.string.select_gpu_driver_install_success, -                                    driverName -                                ), -                                Toast.LENGTH_SHORT -                            ).show() -                        } else { -                            Toast.makeText( -                                applicationContext, -                                R.string.select_gpu_driver_error, -                                Toast.LENGTH_LONG -                            ).show() -                        } -                    } -                } -            } -        }  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt deleted file mode 100644 index a7ddc333f..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.main - -import org.yuzu.yuzu_emu.BuildConfig -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile - -class MainPresenter(private val view: MainView) { -    fun onCreate() { -        val versionName = BuildConfig.VERSION_NAME -        view.setVersionString(versionName) -    } - -    private fun launchFileListActivity(request: Int) { -        view.launchFileListActivity(request) -    } - -    fun handleOptionSelection(itemId: Int): Boolean { -        when (itemId) { -            R.id.menu_settings_core -> { -                view.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG) -                return true -            } -            R.id.button_add_directory -> { -                launchFileListActivity(REQUEST_ADD_DIRECTORY) -                return true -            } -            R.id.button_install_keys -> { -                launchFileListActivity(REQUEST_INSTALL_KEYS) -                return true -            } -            R.id.button_install_amiibo_keys -> { -                launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS) -                return true -            } -            R.id.button_select_gpu_driver -> { -                launchFileListActivity(REQUEST_SELECT_GPU_DRIVER) -                return true -            } -        } -        return false -    } - -    companion object { -        const val REQUEST_ADD_DIRECTORY = 1 -        const val REQUEST_INSTALL_KEYS = 2 -        const val REQUEST_INSTALL_AMIIBO_KEYS = 3 -        const val REQUEST_SELECT_GPU_DRIVER = 4 -    } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt deleted file mode 100644 index 4dc9f0706..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.main - -/** - * Abstraction for the screen that shows on application launch. - * Implementations will differ primarily to target touch-screen - * or non-touch screen devices. - */ -interface MainView { -    /** -     * Pass the view the native library's version string. Displaying -     * it is optional. -     * -     * @param version A string pulled from native code. -     */ -    fun setVersionString(version: String) - -    fun launchSettingsActivity(menuTag: String) - -    fun launchFileListActivity(request: Int) -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt deleted file mode 100644 index 443a37cd2..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.platform - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.color.MaterialColors -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.adapters.GameAdapter -import org.yuzu.yuzu_emu.databinding.FragmentGridBinding -import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager -import org.yuzu.yuzu_emu.model.GamesViewModel -import org.yuzu.yuzu_emu.utils.GameHelper - -class PlatformGamesFragment : Fragment() { -    private var _binding: FragmentGridBinding? = null -    private val binding get() = _binding!! - -    private lateinit var gamesViewModel: GamesViewModel - -    override fun onCreateView( -        inflater: LayoutInflater, -        container: ViewGroup?, -        savedInstanceState: Bundle? -    ): View { -        _binding = FragmentGridBinding.inflate(inflater) -        return binding.root -    } - -    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { -        gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java] - -        binding.gridGames.apply { -            layoutManager = AutofitGridLayoutManager( -                requireContext(), -                requireContext().resources.getDimensionPixelSize(R.dimen.card_width) -            ) -            adapter = -                GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!) -        } - -        // Add swipe down to refresh gesture -        binding.swipeRefresh.setOnRefreshListener { -            refresh() -            binding.swipeRefresh.isRefreshing = false -        } - -        // Set theme color to the refresh animation's background -        binding.swipeRefresh.setProgressBackgroundColorSchemeColor( -            MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) -        ) -        binding.swipeRefresh.setColorSchemeColors( -            MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) -        ) - -        gamesViewModel.games.observe(viewLifecycleOwner) { -            (binding.gridGames.adapter as GameAdapter).swapData(it) -            updateTextView() -        } - -        setInsets() - -        refresh() -    } - -    override fun onResume() { -        super.onResume() -        refresh() -    } - -    override fun onDestroyView() { -        super.onDestroyView() -        _binding = null -    } - -    fun refresh() { -        gamesViewModel.setGames(GameHelper.getGames()) -        updateTextView() -    } - -    private fun updateTextView() { -        if (_binding == null) -            return - -        binding.gamelistEmptyText.visibility = -            if ((binding.gridGames.adapter as GameAdapter).itemCount == 0) View.VISIBLE else View.GONE -    } - -    private fun setInsets() { -        ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> -            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) -            view.updatePadding(bottom = insets.bottom) -            windowInsets -        } -    } - -    companion object { -        const val TAG = "PlatformGamesFragment" -    } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt deleted file mode 100644 index e2e56eb06..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.utils - -import androidx.preference.PreferenceManager -import android.text.Html -import android.text.method.LinkMovementMethod -import android.view.View -import android.widget.TextView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.ui.main.MainActivity -import org.yuzu.yuzu_emu.ui.main.MainPresenter - -object StartupHandler { -    private val preferences = -        PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - -    private fun handleStartupPromptDismiss(parent: MainActivity) { -        parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS) -    } - -    private fun markFirstBoot() { -        preferences.edit() -            .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) -            .apply() -    } - -    fun handleInit(parent: MainActivity) { -        if (preferences.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)) { -            markFirstBoot() -            val alert = MaterialAlertDialogBuilder(parent) -                .setMessage(Html.fromHtml(parent.resources.getString(R.string.app_disclaimer))) -                .setTitle(R.string.app_name) -                .setIcon(R.drawable.ic_launcher) -                .setPositiveButton(android.R.string.ok, null) -                .setOnDismissListener { -                    handleStartupPromptDismiss(parent) -                } -                .show() -            (alert.findViewById<View>(android.R.id.message) as TextView?)!!.movementMethod = -                LinkMovementMethod.getInstance() -        } -    } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt index ce6396e91..481498f7b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt @@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.R  import kotlin.math.roundToInt  object ThemeHelper { -    private const val NAV_BAR_ALPHA = 0.9f +    const val SYSTEM_BAR_ALPHA = 0.9f      @JvmStatic      fun setTheme(activity: AppCompatActivity) { @@ -29,10 +29,6 @@ object ThemeHelper {          windowController.isAppearanceLightNavigationBars = isLightMode          activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent) - -        val navigationBarColor = -            MaterialColors.getColor(activity.window.decorView, R.attr.colorSurface) -        setNavigationBarColor(activity, navigationBarColor)      }      @JvmStatic @@ -48,7 +44,7 @@ object ThemeHelper {          } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||              gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION          ) { -            activity.window.navigationBarColor = getColorWithOpacity(color, NAV_BAR_ALPHA) +            activity.window.navigationBarColor = getColorWithOpacity(color, SYSTEM_BAR_ALPHA)          } else {              activity.window.navigationBarColor = ContextCompat.getColor(                  activity.applicationContext, @@ -58,7 +54,7 @@ object ThemeHelper {      }      @ColorInt -    private fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { +    fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {          return Color.argb(              (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color),              Color.green(color), Color.blue(color) diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 000000000..f7deb2532 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_input.xml b/src/android/app/src/main/res/drawable/ic_input.xml new file mode 100644 index 000000000..c170865ef --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_input.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:autoMirrored="true" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M21,3.01H3c-1.1,0 -2,0.9 -2,2V9h2V4.99h18v14.03H3V15H1v4.01c0,1.1 0.9,1.98 2,1.98h18c1.1,0 2,-0.88 2,-1.98v-14c0,-1.11 -0.9,-2 -2,-2zM11,16l4,-4 -4,-4v3H1v2h10v3z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_nfc.xml b/src/android/app/src/main/res/drawable/ic_nfc.xml new file mode 100644 index 000000000..3dacf798b --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_nfc.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_options.xml b/src/android/app/src/main/res/drawable/ic_options.xml new file mode 100644 index 000000000..91d52f1b8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_options.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_unlock.xml b/src/android/app/src/main/res/drawable/ic_unlock.xml new file mode 100644 index 000000000..40952cbc5 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_unlock.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml new file mode 100644 index 000000000..4400e9eaf --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="614.697dp" +    android:height="683dp" +    android:viewportWidth="614.4" +    android:viewportHeight="682.67"> +    <group> +        <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> +        <path +            android:fillColor="?attr/colorPrimary" +            android:pathData="M340.81,138V682.08c150.26,0 272.06,-121.81 272.06,-272.06S491.07,138 340.81,138M394,197.55a219.06,219.06 0,0 1,0 424.94V197.55" /> +    </group> +    <group> +        <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> +        <path +            android:fillColor="?attr/colorPrimary" +            android:pathData="M272.79,1.92C122.53,1.92 0.73,123.73 0.73,274s121.8,272.07 272.06,272.07ZM219.65,61.51v425A219,219 0,0 1,118 119.18,217.51 217.51,0 0,1 219.65,61.51" /> +    </group> +</vector> diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml index 059aaa9b4..9002b0642 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml @@ -1,28 +1,32 @@  <?xml version="1.0" encoding="utf-8"?> -<androidx.coordinatorlayout.widget.CoordinatorLayout 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"      android:id="@+id/coordinator_main"      android:layout_width="match_parent"      android:layout_height="match_parent"> -    <com.google.android.material.appbar.AppBarLayout -        android:id="@+id/appbar_main" -        android:layout_width="match_parent" -        android:layout_height="wrap_content" -        android:fitsSystemWindows="true" -        app:liftOnScrollTargetViewId="@id/grid_games"> - -        <androidx.appcompat.widget.Toolbar -            android:id="@+id/toolbar_main" -            android:layout_width="match_parent" -            android:layout_height="?attr/actionBarSize" /> +    <androidx.fragment.app.FragmentContainerView +        android:id="@+id/fragment_container" +        android:name="androidx.navigation.fragment.NavHostFragment" +        android:layout_width="0dp" +        android:layout_height="0dp" +        app:defaultNavHost="true" +        app:layout_constraintBottom_toBottomOf="parent" +        app:layout_constraintLeft_toLeftOf="parent" +        app:layout_constraintRight_toRightOf="parent" +        app:layout_constraintTop_toTopOf="parent" +        app:navGraph="@navigation/home_navigation" +        tools:layout="@layout/fragment_games" /> -    </com.google.android.material.appbar.AppBarLayout> - -    <FrameLayout -        android:id="@+id/games_platform_frame" +    <com.google.android.material.bottomnavigation.BottomNavigationView +        android:id="@+id/navigation_bar"          android:layout_width="match_parent" -        android:layout_height="match_parent" -        app:layout_behavior="@string/appbar_scrolling_view_behavior" /> +        android:layout_height="wrap_content" +        app:layout_constraintBottom_toBottomOf="parent" +        app:layout_constraintLeft_toLeftOf="parent" +        app:layout_constraintRight_toRightOf="parent" +        app:menu="@menu/menu_navigation" /> -</androidx.coordinatorlayout.widget.CoordinatorLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml new file mode 100644 index 000000000..aea354783 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_home_option.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<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" +    style="?attr/materialCardViewFilledStyle" +    android:id="@+id/option_card" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:layout_marginVertical="8dp" +    android:layout_marginHorizontal="16dp" +    android:background="?attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content"> + +        <ImageView +            android:id="@+id/option_icon" +            android:layout_width="24dp" +            android:layout_height="24dp" +            android:layout_marginStart="28dp" +            android:layout_gravity="center_vertical" +            app:tint="?attr/colorPrimary" /> + +        <LinearLayout +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_margin="16dp" +            android:orientation="vertical"> + +            <com.google.android.material.textview.MaterialTextView +                style="@style/TextAppearance.Material3.BodyMedium" +                android:id="@+id/option_title" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:textAlignment="viewStart" +                tools:text="@string/install_prod_keys" /> + +            <com.google.android.material.textview.MaterialTextView +                style="@style/TextAppearance.Material3.BodySmall" +                android:id="@+id/option_description" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:textAlignment="viewStart" +                tools:text="@string/install_prod_keys_description" /> + +        </LinearLayout> + +    </LinearLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml new file mode 100644 index 000000000..5cfe76de3 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_games.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<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/coordinator_main" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface"> + +    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout +        android:id="@+id/swipe_refresh" +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:clipToPadding="false" +        app:layout_behavior="@string/searchbar_scrolling_view_behavior"> + +        <RelativeLayout +            android:layout_width="match_parent" +            android:layout_height="match_parent"> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/notice_text" +                style="@style/TextAppearance.Material3.BodyLarge" +                android:layout_width="match_parent" +                android:layout_height="match_parent" +                android:gravity="center" +                android:padding="@dimen/spacing_large" +                android:text="@string/empty_gamelist" +                tools:visibility="gone" /> + +            <androidx.recyclerview.widget.RecyclerView +                android:id="@+id/grid_games" +                android:layout_width="match_parent" +                android:layout_height="match_parent" +                android:clipToPadding="false" +                tools:listitem="@layout/card_game" /> + +        </RelativeLayout> + +    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> + +    <com.google.android.material.appbar.AppBarLayout +        android:id="@+id/app_bar_search" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        app:liftOnScrollTargetViewId="@id/grid_games"> + +        <FrameLayout +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:fitsSystemWindows="true"> + +            <com.google.android.material.search.SearchBar +                android:id="@+id/search_bar" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:hint="@string/home_search_games" /> + +        </FrameLayout> + +    </com.google.android.material.appbar.AppBarLayout> + +    <com.google.android.material.search.SearchView +        android:id="@+id/search_view" +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:hint="@string/home_search_games" +        app:layout_anchor="@id/search_bar"> + +        <androidx.recyclerview.widget.RecyclerView +            android:id="@+id/grid_search" +            android:layout_width="match_parent" +            android:layout_height="match_parent" +            android:clipToPadding="false" +            tools:listitem="@layout/card_game" /> + +    </com.google.android.material.search.SearchView> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml deleted file mode 100644 index bfb670b6d..000000000 --- a/src/android/app/src/main/res/layout/fragment_grid.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<FrameLayout -    xmlns:android="http://schemas.android.com/apk/res/android" -    xmlns:tools="http://schemas.android.com/tools" -    android:layout_width="match_parent" -    android:layout_height="match_parent"> - -    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout -        android:id="@+id/swipe_refresh" -        android:layout_width="match_parent" -        android:layout_height="match_parent"> - -        <RelativeLayout -            android:layout_width="match_parent" -            android:layout_height="match_parent"> - -            <TextView -                android:id="@+id/gamelist_empty_text" -                android:layout_width="match_parent" -                android:layout_height="match_parent" -                android:gravity="center" -                android:text="@string/empty_gamelist" -                android:textSize="18sp" -                android:visibility="gone" /> - -            <androidx.recyclerview.widget.RecyclerView -                android:id="@+id/grid_games" -                android:layout_width="match_parent" -                android:layout_height="match_parent" -                android:clipToPadding="false" -                tools:listitem="@layout/card_game" /> - -        </RelativeLayout> - -    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> - -</FrameLayout> diff --git a/src/android/app/src/main/res/layout/fragment_options.xml b/src/android/app/src/main/res/layout/fragment_options.xml new file mode 100644 index 000000000..ec6e7c205 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_options.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView +    xmlns:android="http://schemas.android.com/apk/res/android" +    android:id="@+id/scroll_view_options" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface" +    android:clipToPadding="false"> + +    <androidx.appcompat.widget.LinearLayoutCompat +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:orientation="vertical" +        android:background="?attr/colorSurface"> + +        <ImageView +            android:layout_width="128dp" +            android:layout_height="128dp" +            android:layout_margin="64dp" +            android:layout_gravity="center_horizontal" +            android:src="@drawable/ic_yuzu_themed" /> + +        <androidx.recyclerview.widget.RecyclerView +            android:id="@+id/options_list" +            android:layout_width="match_parent" +            android:layout_height="match_parent" /> + +    </androidx.appcompat.widget.LinearLayoutCompat> + +</androidx.core.widget.NestedScrollView> diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml deleted file mode 100644 index 73046de0e..000000000 --- a/src/android/app/src/main/res/menu/menu_game_grid.xml +++ /dev/null @@ -1,47 +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/button_file_menu" -        android:icon="@drawable/ic_folder" -        android:title="@string/select_game_folder" -        app:showAsAction="ifRoom"> - -        <menu> - -            <item -                android:id="@+id/button_add_directory" -                android:icon="@drawable/ic_folder" -                android:title="@string/select_game_folder" -                app:showAsAction="ifRoom" /> - -            <item -                android:id="@+id/button_install_keys" -                android:icon="@drawable/ic_install" -                android:title="@string/install_keys" -                app:showAsAction="ifRoom" /> - -            <item -                android:id="@+id/button_install_amiibo_keys" -                android:icon="@drawable/ic_install" -                android:title="@string/install_amiibo_keys" -                app:showAsAction="ifRoom" /> - -            <item -                android:id="@+id/button_select_gpu_driver" -                android:icon="@drawable/ic_settings" -                android:title="@string/select_gpu_driver" -                app:showAsAction="ifRoom" /> - -        </menu> - -    </item> - -    <item -        android:id="@+id/menu_settings_core" -        android:icon="@drawable/ic_settings" -        android:title="@string/grid_menu_core_settings" -        app:showAsAction="ifRoom" /> - -</menu> diff --git a/src/android/app/src/main/res/menu/menu_navigation.xml b/src/android/app/src/main/res/menu/menu_navigation.xml new file mode 100644 index 000000000..ca5a656a6 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_navigation.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + +    <item +        android:id="@+id/gamesFragment" +        android:icon="@drawable/ic_controller" +        android:title="@string/home_games" /> + +    <item +        android:id="@+id/optionsFragment" +        android:icon="@drawable/ic_options" +        android:title="@string/home_options" /> + +</menu> diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml new file mode 100644 index 000000000..e85e24a85 --- /dev/null +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<navigation xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    android:id="@+id/home_navigation" +    app:startDestination="@id/gamesFragment"> + +    <fragment +        android:id="@+id/gamesFragment" +        android:name="org.yuzu.yuzu_emu.ui.GamesFragment" +        android:label="PlatformGamesFragment" /> + +    <fragment +        android:id="@+id/optionsFragment" +        android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" +        android:label="OptionsFragment" /> + +</navigation> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index db0a8f7e5..23977c9f1 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -1,10 +1,15 @@  <resources>      <dimen name="spacing_small">4dp</dimen> +    <dimen name="spacing_med">8dp</dimen>      <dimen name="spacing_medlarge">12dp</dimen>      <dimen name="spacing_large">16dp</dimen>      <dimen name="spacing_xtralarge">32dp</dimen>      <dimen name="spacing_list">64dp</dimen> -    <dimen name="spacing_fab">72dp</dimen> +    <dimen name="spacing_navigation">80dp</dimen> +    <dimen name="spacing_search">88dp</dimen> +    <dimen name="spacing_refresh_slingshot">80dp</dimen> +    <dimen name="spacing_refresh_start">32dp</dimen> +    <dimen name="spacing_refresh_end">96dp</dimen>      <dimen name="menu_width">256dp</dimen>      <dimen name="card_width">160dp</dimen> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 75d1f2293..564bad081 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -9,6 +9,24 @@      <string name="app_notification_channel_description">yuzu Switch emulator notifications</string>      <string name="app_notification_running">yuzu is running</string> +    <!-- Home strings --> +    <string name="home_games">Games</string> +    <string name="home_options">Options</string> +    <string name="add_games">Add Games</string> +    <string name="add_games_description">Select your games folder</string> +    <string name="home_search_games">Search Games</string> +    <string name="install_prod_keys">Install Prod.keys</string> +    <string name="install_prod_keys_description">Required to decrypt retail games</string> +    <string name="install_amiibo_keys">Install Amiibo Keys</string> +    <string name="install_amiibo_keys_description">Required to use Amiibo in game</string> +    <string name="install_keys_success">Keys successfully installed</string> +    <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> +    <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> +    <string name="install_gpu_driver">Install GPU Driver</string> +    <string name="install_gpu_driver_description">Use a different driver for potentially better performance or accuracy</string> +    <string name="settings">Settings</string> +    <string name="settings_description">Configure emulator settings</string> +      <!-- General settings strings -->      <string name="frame_limit_enable">Enable limit speed</string>      <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string> @@ -51,17 +69,6 @@      <string name="error_saving">Error saving %1$s.ini: %2$s</string>      <string name="loading">Loading...</string> -    <!-- Game Grid Screen--> -    <string name="grid_menu_core_settings">Settings</string> - -    <!-- Add Directory Screen--> -    <string name="select_game_folder">Select game folder</string> -    <string name="install_keys">Install keys</string> -    <string name="install_amiibo_keys">Install amiibo keys</string> -    <string name="install_keys_success">Keys successfully installed</string> -    <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> -    <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> -      <!-- GPU driver installation -->      <string name="select_gpu_driver">Select GPU driver</string>      <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> | 
