Browse Source
Merge pull request #11603 from t895/consolidate-installs
Merge pull request #11603 from t895/consolidate-installs
android: Consolidate installers to one fragmentnce_cpp
committed by
GitHub
33 changed files with 616 additions and 421 deletions
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
-
49src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
-
7src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
-
48src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
-
214src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
-
18src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
-
138src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
-
13src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
-
6src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
-
248src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
67src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
-
22src/android/app/src/main/jni/native.cpp
-
71src/android/app/src/main/res/layout/card_installable.xml
-
61src/android/app/src/main/res/layout/fragment_about.xml
-
31src/android/app/src/main/res/layout/fragment_installables.xml
-
7src/android/app/src/main/res/navigation/home_navigation.xml
-
1src/android/app/src/main/res/values-de/strings.xml
-
1src/android/app/src/main/res/values-es/strings.xml
-
1src/android/app/src/main/res/values-fr/strings.xml
-
1src/android/app/src/main/res/values-it/strings.xml
-
1src/android/app/src/main/res/values-ja/strings.xml
-
1src/android/app/src/main/res/values-ko/strings.xml
-
1src/android/app/src/main/res/values-nb/strings.xml
-
1src/android/app/src/main/res/values-pl/strings.xml
-
1src/android/app/src/main/res/values-pt-rBR/strings.xml
-
1src/android/app/src/main/res/values-pt-rPT/strings.xml
-
1src/android/app/src/main/res/values-ru/strings.xml
-
1src/android/app/src/main/res/values-uk/strings.xml
-
6src/android/app/src/main/res/values-w600dp/integers.xml
-
1src/android/app/src/main/res/values-zh-rCN/strings.xml
-
1src/android/app/src/main/res/values-zh-rTW/strings.xml
-
2src/android/app/src/main/res/values/integers.xml
-
10src/android/app/src/main/res/values/strings.xml
@ -0,0 +1,49 @@ |
|||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.adapters |
||||
|
|
||||
|
import android.view.LayoutInflater |
||||
|
import android.view.View |
||||
|
import android.view.ViewGroup |
||||
|
import androidx.recyclerview.widget.RecyclerView |
||||
|
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding |
||||
|
import org.yuzu.yuzu_emu.model.Installable |
||||
|
|
||||
|
class InstallableAdapter(private val installables: List<Installable>) : |
||||
|
RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { |
||||
|
override fun onCreateViewHolder( |
||||
|
parent: ViewGroup, |
||||
|
viewType: Int |
||||
|
): InstallableAdapter.InstallableViewHolder { |
||||
|
val binding = |
||||
|
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
||||
|
return InstallableViewHolder(binding) |
||||
|
} |
||||
|
|
||||
|
override fun getItemCount(): Int = installables.size |
||||
|
|
||||
|
override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = |
||||
|
holder.bind(installables[position]) |
||||
|
|
||||
|
inner class InstallableViewHolder(val binding: CardInstallableBinding) : |
||||
|
RecyclerView.ViewHolder(binding.root) { |
||||
|
lateinit var installable: Installable |
||||
|
|
||||
|
fun bind(installable: Installable) { |
||||
|
this.installable = installable |
||||
|
|
||||
|
binding.title.setText(installable.titleId) |
||||
|
binding.description.setText(installable.descriptionId) |
||||
|
|
||||
|
if (installable.install != null) { |
||||
|
binding.buttonInstall.visibility = View.VISIBLE |
||||
|
binding.buttonInstall.setOnClickListener { installable.install.invoke() } |
||||
|
} |
||||
|
if (installable.export != null) { |
||||
|
binding.buttonExport.visibility = View.VISIBLE |
||||
|
binding.buttonExport.setOnClickListener { installable.export.invoke() } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,214 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.fragments |
|
||||
|
|
||||
import android.app.Dialog |
|
||||
import android.content.Intent |
|
||||
import android.net.Uri |
|
||||
import android.os.Bundle |
|
||||
import android.provider.DocumentsContract |
|
||||
import android.widget.Toast |
|
||||
import androidx.activity.result.ActivityResultLauncher |
|
||||
import androidx.activity.result.contract.ActivityResultContracts |
|
||||
import androidx.appcompat.app.AppCompatActivity |
|
||||
import androidx.documentfile.provider.DocumentFile |
|
||||
import androidx.fragment.app.DialogFragment |
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
|
||||
import java.io.BufferedOutputStream |
|
||||
import java.io.File |
|
||||
import java.io.FileOutputStream |
|
||||
import java.io.FilenameFilter |
|
||||
import java.time.LocalDateTime |
|
||||
import java.time.format.DateTimeFormatter |
|
||||
import java.util.zip.ZipEntry |
|
||||
import java.util.zip.ZipOutputStream |
|
||||
import kotlinx.coroutines.CoroutineScope |
|
||||
import kotlinx.coroutines.Dispatchers |
|
||||
import kotlinx.coroutines.launch |
|
||||
import kotlinx.coroutines.withContext |
|
||||
import org.yuzu.yuzu_emu.R |
|
||||
import org.yuzu.yuzu_emu.YuzuApplication |
|
||||
import org.yuzu.yuzu_emu.features.DocumentProvider |
|
||||
import org.yuzu.yuzu_emu.getPublicFilesDir |
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil |
|
||||
|
|
||||
class ImportExportSavesFragment : DialogFragment() { |
|
||||
private val context = YuzuApplication.appContext |
|
||||
private val savesFolder = |
|
||||
"${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" |
|
||||
|
|
||||
// Get first subfolder in saves folder (should be the user folder) |
|
||||
private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" |
|
||||
private var lastZipCreated: File? = null |
|
||||
|
|
||||
private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> |
|
||||
private lateinit var documentPicker: ActivityResultLauncher<Array<String>> |
|
||||
|
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
val activity = requireActivity() as AppCompatActivity |
|
||||
|
|
||||
val activityResultRegistry = requireActivity().activityResultRegistry |
|
||||
startForResultExportSave = activityResultRegistry.register( |
|
||||
"startForResultExportSaveKey", |
|
||||
ActivityResultContracts.StartActivityForResult() |
|
||||
) { |
|
||||
File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() |
|
||||
} |
|
||||
documentPicker = activityResultRegistry.register( |
|
||||
"documentPickerKey", |
|
||||
ActivityResultContracts.OpenDocument() |
|
||||
) { |
|
||||
it?.let { uri -> importSave(uri, activity) } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
|
||||
return if (savesFolderRoot == "") { |
|
||||
MaterialAlertDialogBuilder(requireContext()) |
|
||||
.setTitle(R.string.manage_save_data) |
|
||||
.setMessage(R.string.import_export_saves_no_profile) |
|
||||
.setPositiveButton(android.R.string.ok, null) |
|
||||
.show() |
|
||||
} else { |
|
||||
MaterialAlertDialogBuilder(requireContext()) |
|
||||
.setTitle(R.string.manage_save_data) |
|
||||
.setMessage(R.string.manage_save_data_description) |
|
||||
.setNegativeButton(R.string.export_saves) { _, _ -> |
|
||||
exportSave() |
|
||||
} |
|
||||
.setPositiveButton(R.string.import_saves) { _, _ -> |
|
||||
documentPicker.launch(arrayOf("application/zip")) |
|
||||
} |
|
||||
.setNeutralButton(android.R.string.cancel, null) |
|
||||
.show() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Zips the save files located in the given folder path and creates a new zip file with the current date and time. |
|
||||
* @return true if the zip file is successfully created, false otherwise. |
|
||||
*/ |
|
||||
private fun zipSave(): Boolean { |
|
||||
try { |
|
||||
val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") |
|
||||
tempFolder.mkdirs() |
|
||||
val saveFolder = File(savesFolderRoot) |
|
||||
val outputZipFile = File( |
|
||||
tempFolder, |
|
||||
"yuzu saves - ${ |
|
||||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) |
|
||||
}.zip" |
|
||||
) |
|
||||
outputZipFile.createNewFile() |
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> |
|
||||
saveFolder.walkTopDown().forEach { file -> |
|
||||
val zipFileName = |
|
||||
file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") |
|
||||
if (zipFileName == "") { |
|
||||
return@forEach |
|
||||
} |
|
||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") |
|
||||
zos.putNextEntry(entry) |
|
||||
if (file.isFile) { |
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
lastZipCreated = outputZipFile |
|
||||
} catch (e: Exception) { |
|
||||
return false |
|
||||
} |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent. |
|
||||
*/ |
|
||||
private fun exportSave() { |
|
||||
CoroutineScope(Dispatchers.IO).launch { |
|
||||
val wasZipCreated = zipSave() |
|
||||
val lastZipFile = lastZipCreated |
|
||||
if (!wasZipCreated || lastZipFile == null) { |
|
||||
withContext(Dispatchers.Main) { |
|
||||
Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() |
|
||||
} |
|
||||
return@launch |
|
||||
} |
|
||||
|
|
||||
withContext(Dispatchers.Main) { |
|
||||
val file = DocumentFile.fromSingleUri( |
|
||||
context, |
|
||||
DocumentsContract.buildDocumentUri( |
|
||||
DocumentProvider.AUTHORITY, |
|
||||
"${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" |
|
||||
) |
|
||||
)!! |
|
||||
val intent = Intent(Intent.ACTION_SEND) |
|
||||
.setDataAndType(file.uri, "application/zip") |
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
|
||||
.putExtra(Intent.EXTRA_STREAM, file.uri) |
|
||||
startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Imports the save files contained in the zip file, and replaces any existing ones with the new save file. |
|
||||
* @param zipUri The Uri of the zip file containing the save file(s) to import. |
|
||||
*/ |
|
||||
private fun importSave(zipUri: Uri, activity: AppCompatActivity) { |
|
||||
val inputZip = context.contentResolver.openInputStream(zipUri) |
|
||||
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. |
|
||||
var validZip = false |
|
||||
val savesFolder = File(savesFolderRoot) |
|
||||
val cacheSaveDir = File("${context.cacheDir.path}/saves/") |
|
||||
cacheSaveDir.mkdir() |
|
||||
|
|
||||
if (inputZip == null) { |
|
||||
Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) |
|
||||
.show() |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
val filterTitleId = |
|
||||
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } |
|
||||
|
|
||||
try { |
|
||||
CoroutineScope(Dispatchers.IO).launch { |
|
||||
FileUtil.unzip(inputZip, cacheSaveDir) |
|
||||
cacheSaveDir.list(filterTitleId)?.forEach { savePath -> |
|
||||
File(savesFolder, savePath).deleteRecursively() |
|
||||
File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) |
|
||||
validZip = true |
|
||||
} |
|
||||
|
|
||||
withContext(Dispatchers.Main) { |
|
||||
if (!validZip) { |
|
||||
MessageDialogFragment.newInstance( |
|
||||
requireActivity(), |
|
||||
titleId = R.string.save_file_invalid_zip_structure, |
|
||||
descriptionId = R.string.save_file_invalid_zip_structure_description |
|
||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) |
|
||||
return@withContext |
|
||||
} |
|
||||
Toast.makeText( |
|
||||
context, |
|
||||
context.getString(R.string.save_file_imported_success), |
|
||||
Toast.LENGTH_LONG |
|
||||
).show() |
|
||||
} |
|
||||
|
|
||||
cacheSaveDir.deleteRecursively() |
|
||||
} |
|
||||
} catch (e: Exception) { |
|
||||
Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) |
|
||||
.show() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
companion object { |
|
||||
const val TAG = "ImportExportSavesFragment" |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,138 @@ |
|||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.fragments |
||||
|
|
||||
|
import android.os.Bundle |
||||
|
import android.view.LayoutInflater |
||||
|
import android.view.View |
||||
|
import android.view.ViewGroup |
||||
|
import androidx.core.view.ViewCompat |
||||
|
import androidx.core.view.WindowInsetsCompat |
||||
|
import androidx.core.view.updatePadding |
||||
|
import androidx.fragment.app.Fragment |
||||
|
import androidx.fragment.app.activityViewModels |
||||
|
import androidx.navigation.findNavController |
||||
|
import androidx.recyclerview.widget.GridLayoutManager |
||||
|
import com.google.android.material.transition.MaterialSharedAxis |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.adapters.InstallableAdapter |
||||
|
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding |
||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel |
||||
|
import org.yuzu.yuzu_emu.model.Installable |
||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity |
||||
|
|
||||
|
class InstallableFragment : Fragment() { |
||||
|
private var _binding: FragmentInstallablesBinding? = null |
||||
|
private val binding get() = _binding!! |
||||
|
|
||||
|
private val homeViewModel: HomeViewModel by activityViewModels() |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
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 = FragmentInstallablesBinding.inflate(layoutInflater) |
||||
|
return binding.root |
||||
|
} |
||||
|
|
||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||
|
super.onViewCreated(view, savedInstanceState) |
||||
|
|
||||
|
val mainActivity = requireActivity() as MainActivity |
||||
|
|
||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true) |
||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false) |
||||
|
|
||||
|
binding.toolbarInstallables.setNavigationOnClickListener { |
||||
|
binding.root.findNavController().popBackStack() |
||||
|
} |
||||
|
|
||||
|
val installables = listOf( |
||||
|
Installable( |
||||
|
R.string.user_data, |
||||
|
R.string.user_data_description, |
||||
|
install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, |
||||
|
export = { mainActivity.exportUserData.launch("export.zip") } |
||||
|
), |
||||
|
Installable( |
||||
|
R.string.install_game_content, |
||||
|
R.string.install_game_content_description, |
||||
|
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } |
||||
|
), |
||||
|
Installable( |
||||
|
R.string.install_firmware, |
||||
|
R.string.install_firmware_description, |
||||
|
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } |
||||
|
), |
||||
|
if (mainActivity.savesFolderRoot != "") { |
||||
|
Installable( |
||||
|
R.string.manage_save_data, |
||||
|
R.string.import_export_saves_description, |
||||
|
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, |
||||
|
export = { mainActivity.exportSave() } |
||||
|
) |
||||
|
} else { |
||||
|
Installable( |
||||
|
R.string.manage_save_data, |
||||
|
R.string.import_export_saves_description, |
||||
|
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } |
||||
|
) |
||||
|
}, |
||||
|
Installable( |
||||
|
R.string.install_prod_keys, |
||||
|
R.string.install_prod_keys_description, |
||||
|
install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } |
||||
|
), |
||||
|
Installable( |
||||
|
R.string.install_amiibo_keys, |
||||
|
R.string.install_amiibo_keys_description, |
||||
|
install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
binding.listInstallables.apply { |
||||
|
layoutManager = GridLayoutManager( |
||||
|
requireContext(), |
||||
|
resources.getInteger(R.integer.grid_columns) |
||||
|
) |
||||
|
adapter = InstallableAdapter(installables) |
||||
|
} |
||||
|
|
||||
|
setInsets() |
||||
|
} |
||||
|
|
||||
|
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 mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams |
||||
|
mlpAppBar.leftMargin = leftInsets |
||||
|
mlpAppBar.rightMargin = rightInsets |
||||
|
binding.toolbarInstallables.layoutParams = mlpAppBar |
||||
|
|
||||
|
val mlpScrollAbout = |
||||
|
binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams |
||||
|
mlpScrollAbout.leftMargin = leftInsets |
||||
|
mlpScrollAbout.rightMargin = rightInsets |
||||
|
binding.listInstallables.layoutParams = mlpScrollAbout |
||||
|
|
||||
|
binding.listInstallables.updatePadding(bottom = barInsets.bottom) |
||||
|
|
||||
|
windowInsets |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.model |
||||
|
|
||||
|
import androidx.annotation.StringRes |
||||
|
|
||||
|
data class Installable( |
||||
|
@StringRes val titleId: Int, |
||||
|
@StringRes val descriptionId: Int, |
||||
|
val install: (() -> Unit)? = null, |
||||
|
val export: (() -> Unit)? = null |
||||
|
) |
||||
@ -0,0 +1,71 @@ |
|||||
|
<?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/materialCardViewOutlinedStyle" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:layout_marginVertical="12dp"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_margin="16dp" |
||||
|
android:orientation="horizontal" |
||||
|
android:layout_gravity="center"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginEnd="16dp" |
||||
|
android:layout_weight="1" |
||||
|
android:orientation="vertical"> |
||||
|
|
||||
|
<com.google.android.material.textview.MaterialTextView |
||||
|
android:id="@+id/title" |
||||
|
style="@style/TextAppearance.Material3.TitleMedium" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:text="@string/user_data" |
||||
|
android:textAlignment="viewStart" /> |
||||
|
|
||||
|
<com.google.android.material.textview.MaterialTextView |
||||
|
android:id="@+id/description" |
||||
|
style="@style/TextAppearance.Material3.BodyMedium" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginTop="6dp" |
||||
|
android:text="@string/user_data_description" |
||||
|
android:textAlignment="viewStart" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
|
||||
|
<Button |
||||
|
android:id="@+id/button_export" |
||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_gravity="center_vertical" |
||||
|
android:contentDescription="@string/export" |
||||
|
android:tooltipText="@string/export" |
||||
|
android:visibility="gone" |
||||
|
app:icon="@drawable/ic_export" |
||||
|
tools:visibility="visible" /> |
||||
|
|
||||
|
<Button |
||||
|
android:id="@+id/button_install" |
||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_gravity="center_vertical" |
||||
|
android:layout_marginStart="12dp" |
||||
|
android:contentDescription="@string/string_import" |
||||
|
android:tooltipText="@string/string_import" |
||||
|
android:visibility="gone" |
||||
|
app:icon="@drawable/ic_import" |
||||
|
tools:visibility="visible" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
|
||||
|
</com.google.android.material.card.MaterialCardView> |
||||
@ -0,0 +1,31 @@ |
|||||
|
<?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" |
||||
|
android:id="@+id/coordinator_licenses" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
android:background="?attr/colorSurface"> |
||||
|
|
||||
|
<com.google.android.material.appbar.AppBarLayout |
||||
|
android:id="@+id/appbar_installables" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:fitsSystemWindows="true"> |
||||
|
|
||||
|
<com.google.android.material.appbar.MaterialToolbar |
||||
|
android:id="@+id/toolbar_installables" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="?attr/actionBarSize" |
||||
|
app:title="@string/manage_yuzu_data" |
||||
|
app:navigationIcon="@drawable/ic_back" /> |
||||
|
|
||||
|
</com.google.android.material.appbar.AppBarLayout> |
||||
|
|
||||
|
<androidx.recyclerview.widget.RecyclerView |
||||
|
android:id="@+id/list_installables" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
android:clipToPadding="false" |
||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> |
||||
|
|
||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
|
||||
|
<integer name="grid_columns">2</integer> |
||||
|
|
||||
|
</resources> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue