diff options
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt')
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt new file mode 100644 index 000000000..0dce8ad8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.AddonAdapter +import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding +import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.AddonUtil +import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo +import java.io.File + +class AddonsFragment : Fragment() { + private var _binding: FragmentAddonsBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private val addonViewModel: AddonViewModel by activityViewModels() + + private val args by navArgs<AddonsFragmentArgs>() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + addonViewModel.onOpenAddons(args.game) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddonsBinding.inflate(inflater) + return binding.root + } + + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = false) + homeViewModel.setStatusBarShadeVisibility(false) + + binding.toolbarAddons.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title) + + binding.listAddons.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = AddonAdapter() + } + + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.addonList.collect { + (binding.listAddons.adapter as AddonAdapter).submitList(it) + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.showModInstallPicker.collect { + if (it) { + installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) + addonViewModel.showModInstallPicker(false) + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.showModNoticeDialog.collect { + if (it) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.addon_notice, + descriptionId = R.string.addon_notice_description, + positiveAction = { addonViewModel.showModInstallPicker(true) } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + addonViewModel.showModNoticeDialog(false) + } + } + } + } + } + + binding.buttonInstall.setOnClickListener { + ContentTypeSelectionDialogFragment().show( + parentFragmentManager, + ContentTypeSelectionDialogFragment.TAG + ) + } + + setInsets() + } + + override fun onResume() { + super.onResume() + addonViewModel.refreshAddons() + } + + override fun onDestroy() { + super.onDestroy() + addonViewModel.onCloseAddons() + } + + val installAddon = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result) + if (externalAddonDirectory == null) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.invalid_directory, + descriptionId = R.string.invalid_directory_description + ).show(parentFragmentManager, MessageDialogFragment.TAG) + return@registerForActivityResult + } + + val isValid = externalAddonDirectory.listFiles() + .any { AddonUtil.validAddonDirectories.contains(it.name) } + val errorMessage = MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.invalid_directory, + descriptionId = R.string.invalid_directory_description + ) + if (isValid) { + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.installing_game_content, + false + ) { + val parentDirectoryName = externalAddonDirectory.name + val internalAddonDirectory = + File(args.game.addonDir + parentDirectoryName) + try { + externalAddonDirectory.copyFilesTo(internalAddonDirectory) + } catch (_: Exception) { + return@newInstance errorMessage + } + addonViewModel.refreshAddons() + return@newInstance getString(R.string.addon_installed_successfully) + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } else { + errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams + mlpToolbar.leftMargin = leftInsets + mlpToolbar.rightMargin = rightInsets + binding.toolbarAddons.layoutParams = mlpToolbar + + val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams + mlpAddonsList.leftMargin = leftInsets + mlpAddonsList.rightMargin = rightInsets + binding.listAddons.layoutParams = mlpAddonsList + binding.listAddons.updatePadding( + bottom = barInsets.bottom + + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) + + val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + val mlpFab = + binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams + mlpFab.leftMargin = leftInsets + fabSpacing + mlpFab.rightMargin = rightInsets + fabSpacing + mlpFab.bottomMargin = barInsets.bottom + fabSpacing + binding.buttonInstall.layoutParams = mlpFab + + windowInsets + } +} |