From b443287007e2fb6401a19fa4ad8e420de9a780f1 Mon Sep 17 00:00:00 2001 From: lynxnb Date: Mon, 28 Feb 2022 21:58:39 +0100 Subject: [PATCH] WIP --- app/build.gradle | 5 + app/proguard-rules.pro | 28 +++ app/src/main/java/emu/skyline/AppDialog.kt | 164 ++++++++++++++---- .../adapter/appdialog/GameInfoViewItem.kt | 5 +- .../java/emu/skyline/network/TitleMetaData.kt | 47 +++++ .../emu/skyline/network/TitleMetaService.kt | 15 ++ app/src/main/res/layout/app_dialog.xml | 86 +-------- app/src/main/res/layout/app_dialog_dummy.xml | 142 +++++++++++++++ app/src/main/res/values/dimens.xml | 2 +- build.gradle | 1 + 10 files changed, 381 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/emu/skyline/network/TitleMetaData.kt create mode 100644 app/src/main/java/emu/skyline/network/TitleMetaService.kt create mode 100644 app/src/main/res/layout/app_dialog_dummy.xml diff --git a/app/build.gradle b/app/build.gradle index e3db3172..7c90a669 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' + id 'kotlinx-serialization' id 'dagger.hilt.android.plugin' } @@ -92,6 +93,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'androidx.preference:preference-ktx:1.1.1' + implementation "androidx.recyclerview:recyclerview:1.2.1" implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' @@ -104,9 +106,12 @@ dependencies { /* JetBrains */ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" /* Other Java */ implementation 'info.debatty:java-string-similarity:2.0.0' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" } kapt { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b3df0d00..85c17225 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -4,3 +4,31 @@ # Retain all classes within Skyline for traces + JNI access + Serializable classes -keep class emu.skyline.** { *; } + +# Kotlinx Serialization rules +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault diff --git a/app/src/main/java/emu/skyline/AppDialog.kt b/app/src/main/java/emu/skyline/AppDialog.kt index 869e7e9b..8aa15996 100644 --- a/app/src/main/java/emu/skyline/AppDialog.kt +++ b/app/src/main/java/emu/skyline/AppDialog.kt @@ -5,23 +5,32 @@ package emu.skyline -import android.content.ComponentName -import android.content.Intent -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon +import android.graphics.Rect import android.os.Bundle import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap +import androidx.annotation.StringRes +import androidx.recyclerview.widget.* +import androidx.viewbinding.ViewBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import emu.skyline.adapter.GenericAdapter +import emu.skyline.adapter.GenericListItem +import emu.skyline.adapter.appdialog.* import emu.skyline.data.AppItem import emu.skyline.databinding.AppDialogBinding -import emu.skyline.loader.LoaderResult +import emu.skyline.network.TitleMetaData +import emu.skyline.network.TitleMetaService +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit /** * This dialog is used to show extra game metadata and provide extra options such as pinning the game to the home screen @@ -43,6 +52,12 @@ class AppDialog : BottomSheetDialogFragment() { private lateinit var binding : AppDialogBinding + private val baseAdapter = ConcatAdapter() + private val gameInfoAdapter = GenericAdapter() + private val updatesAdapter = GenericAdapter() + private val dlcsAdapter = GenericAdapter() + private val tlmdAdapter = GenericAdapter() + private val item by lazy { requireArguments().getSerializable("item") as AppItem } /** @@ -56,9 +71,6 @@ class AppDialog : BottomSheetDialogFragment() { override fun onStart() { super.onStart() - val behavior = BottomSheetBehavior.from(requireView().parent as View) - behavior.state = BottomSheetBehavior.STATE_EXPANDED - dialog?.setOnKeyListener { _, keyCode, event -> if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) { dialog?.onBackPressed() @@ -71,33 +83,121 @@ class AppDialog : BottomSheetDialogFragment() { override fun onViewCreated(view : View, savedInstanceState : Bundle?) { super.onViewCreated(view, savedInstanceState) - val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256) - - binding.gameIcon.setImageBitmap(item.icon ?: missingIcon) - binding.gameTitle.text = item.title - binding.gameSubtitle.text = item.subTitle ?: item.loaderResultString(requireContext()) - - binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success - binding.gamePlay.setOnClickListener { - startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri }) + baseAdapter.apply { + addAdapter(gameInfoAdapter) + addAdapter(updatesAdapter) + addAdapter(dlcsAdapter) + addAdapter(tlmdAdapter) } - val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java) - binding.gamePin.isEnabled = shortcutManager.isRequestPinShortcutSupported + val retrofit = Retrofit.Builder() + .baseUrl("https://raw.githubusercontent.com/skyline-emu/title-meta/") + .addConverterFactory(Json.asConverterFactory(MediaType.get("application/json"))) + .build() - binding.gamePin.setOnClickListener { - val info = ShortcutInfo.Builder(context, item.title) - info.setShortLabel(item.title) - info.setActivity(ComponentName(requireContext(), EmulationActivity::class.java)) - info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon)) + val service : TitleMetaService = retrofit.create(TitleMetaService::class.java) - val intent = Intent(context, EmulationActivity::class.java) - intent.data = item.uri - intent.action = Intent.ACTION_VIEW + service.getData(item.title).enqueue(object : Callback { + override fun onResponse(call : Call, response : Response) { + //response.body()?.let { populateAdaptersAfterData(it) } + TODO("NOT") + } - info.setIntent(intent) + override fun onFailure(call : Call, t : Throwable) { + TODO("Not yet implemented") + } + }) - shortcutManager.requestPinShortcut(info.build(), null) + populateAdaptersBeforeData() + + binding.content.apply { + addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.section_spacing))) + adapter = baseAdapter } } + + private fun populateAdaptersBeforeData() { + populateGameInfoAdapter(null) + //populateUpdatesAdapter() + //populateDlcsAdapter() + } + + private fun populateAdaptersAfterData(data : TitleMetaData) { + populateGameInfoAdapter(data) + populateTlmdAdapter(data) + } + + private fun populateGameInfoAdapter(data : TitleMetaData?) { + val entries : MutableList> = ArrayList() + + entries.apply { + add(DragIndicatorViewItem(BottomSheetBehavior.from(requireView().parent as View))) + add(GameInfoViewItem(requireActivity(), item, data?.version, data?.rating)) + } + + gameInfoAdapter.setItems(entries) + } + + private fun populateUpdatesAdapter() { + val entries : MutableList> = ArrayList() + + entries.apply { + add(SectionHeaderViewItem(requireContext().getString(R.string.updates))) + add(UpdatesViewItem("TODO base_version")) + } + + updatesAdapter.apply { + selectedPosition = 0 + 1 + setItems(entries) + } + } + + private fun populateDlcsAdapter() { + val entries : MutableList> = ArrayList() + + entries.apply { + add(SectionHeaderViewItem(requireContext().getString(R.string.dlcs))) + } + + dlcsAdapter.setItems(entries) + } + + private fun populateTlmdAdapter(data : TitleMetaData) { + val entries : MutableList> = ArrayList() + + entries.apply { + data.issues?.let { issues -> + add(SectionHeaderViewItem(requireContext().getString(R.string.issues)) { _, _ -> + Snackbar.make(this@AppDialog.requireView(), data.discussion, Snackbar.LENGTH_SHORT).show() + }) + issues.forEach { issue -> + add(IssuesViewItem(issue.title, issue.description)) + } + } + + data.notes?.let { notes -> + add(SectionHeaderViewItem(requireContext().getString(R.string.notes))) + notes.forEach { note -> + add(NotesViewItem(note)) + } + } + + data.cheats?.let { cheats -> + add(SectionHeaderViewItem(requireContext().getString(R.string.cheats))) + cheats.forEach { (key, cheat) -> + add(CheatsViewItem(cheat.title, cheat.author, cheat.description, cheat.code, false)) + } + } + } + tlmdAdapter.setItems(entries) + } + + private inner class SpacingItemDecoration(private val padding : Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + outRect.set(0, 0, 0, padding) + } + } + + private fun getResString(@StringRes resId : Int) = requireContext().getString(resId) } diff --git a/app/src/main/java/emu/skyline/adapter/appdialog/GameInfoViewItem.kt b/app/src/main/java/emu/skyline/adapter/appdialog/GameInfoViewItem.kt index f1c5ad47..8928bad5 100644 --- a/app/src/main/java/emu/skyline/adapter/appdialog/GameInfoViewItem.kt +++ b/app/src/main/java/emu/skyline/adapter/appdialog/GameInfoViewItem.kt @@ -23,12 +23,13 @@ import emu.skyline.adapter.inflater import emu.skyline.data.AppItem import emu.skyline.databinding.AppDialogGameInfoBinding import emu.skyline.loader.LoaderResult +import emu.skyline.network.TitleRating object ControllerBindingFactory : ViewBindingFactory { override fun createBinding(parent : ViewGroup) = AppDialogGameInfoBinding.inflate(parent.inflater(), parent, false) } -class GameInfoViewItem(private val context : Context, private val item : AppItem, private val testedVersion : String?, private val rating : Int?) : GenericListItem() { +class GameInfoViewItem(private val context : Context, private val item : AppItem, private val testedVersion : String?, private val rating : TitleRating?) : GenericListItem() { override fun getViewBindingFactory() = ControllerBindingFactory override fun bind(binding : AppDialogGameInfoBinding, position : Int) { @@ -42,7 +43,7 @@ class GameInfoViewItem(private val context : Context, private val item : AppItem binding.gameSubtitle.isSelected = true binding.flex.visibility = if (rating == null && testedVersion == null) View.INVISIBLE else View.VISIBLE - binding.ratingBar.rating = (rating ?: 0).toFloat() + binding.ratingBar.rating = (rating ?: TitleRating.None).ordinal.toFloat() binding.testedVersion.text = testedVersion?.let { context.getString(R.string.tested_on, it) } ?: context.getString(R.string.not_tested) binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success diff --git a/app/src/main/java/emu/skyline/network/TitleMetaData.kt b/app/src/main/java/emu/skyline/network/TitleMetaData.kt new file mode 100644 index 00000000..19ce08b4 --- /dev/null +++ b/app/src/main/java/emu/skyline/network/TitleMetaData.kt @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TitleMetaData( + val name : String, + val id : String, + val version : String, + val rating : TitleRating, + val discussion : String, + val issues : List? = null, + val notes : List? = null, + val cheats : Map? = null, +) + +@Serializable +enum class TitleRating { + None, + @SerialName("crash") Crash, + @SerialName("intro") Intro, + @SerialName("major-bugs") MajorBugs, + @SerialName("minor-bugs") MinorBugs, + @SerialName("perfect") Perfect, +} + +@Serializable +data class Issue( + val title : String, + val description : String? = null, + val url : String, + //val workarounds : List>, // TODO +) + +@Serializable +data class Cheat( + val title : String, + val description : String? = null, + val author : String? = null, + val code : String, +) diff --git a/app/src/main/java/emu/skyline/network/TitleMetaService.kt b/app/src/main/java/emu/skyline/network/TitleMetaService.kt new file mode 100644 index 00000000..457b9cff --- /dev/null +++ b/app/src/main/java/emu/skyline/network/TitleMetaService.kt @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.network + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface TitleMetaService { + @GET("main/{id}/title.json") + fun getData(@Path("id") titleId : String) : Call +} diff --git a/app/src/main/res/layout/app_dialog.xml b/app/src/main/res/layout/app_dialog.xml index 0039790a..1ffc14cc 100644 --- a/app/src/main/res/layout/app_dialog.xml +++ b/app/src/main/res/layout/app_dialog.xml @@ -1,82 +1,10 @@ - - - - - - - - - - - - -