This commit is contained in:
lynxnb 2022-02-28 21:58:39 +01:00
parent 8360cdf82e
commit b443287007
10 changed files with 381 additions and 114 deletions

View File

@ -2,6 +2,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'kotlinx-serialization'
id 'dagger.hilt.android.plugin' id 'dagger.hilt.android.plugin'
} }
@ -92,6 +93,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.preference:preference-ktx:1.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 'com.google.android.material:material:1.4.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
@ -104,9 +106,12 @@ dependencies {
/* JetBrains */ /* JetBrains */
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
/* Other Java */ /* Other Java */
implementation 'info.debatty:java-string-similarity:2.0.0' 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 { kapt {

View File

@ -4,3 +4,31 @@
# Retain all classes within Skyline for traces + JNI access + Serializable classes # Retain all classes within Skyline for traces + JNI access + Serializable classes
-keep class emu.skyline.** { *; } -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

View File

@ -5,23 +5,32 @@
package emu.skyline package emu.skyline
import android.content.ComponentName import android.graphics.Rect
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap import androidx.recyclerview.widget.*
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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.data.AppItem
import emu.skyline.databinding.AppDialogBinding 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 * 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 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 } private val item by lazy { requireArguments().getSerializable("item") as AppItem }
/** /**
@ -56,9 +71,6 @@ class AppDialog : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog?.setOnKeyListener { _, keyCode, event -> dialog?.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) {
dialog?.onBackPressed() dialog?.onBackPressed()
@ -71,33 +83,121 @@ class AppDialog : BottomSheetDialogFragment() {
override fun onViewCreated(view : View, savedInstanceState : Bundle?) { override fun onViewCreated(view : View, savedInstanceState : Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256) baseAdapter.apply {
addAdapter(gameInfoAdapter)
binding.gameIcon.setImageBitmap(item.icon ?: missingIcon) addAdapter(updatesAdapter)
binding.gameTitle.text = item.title addAdapter(dlcsAdapter)
binding.gameSubtitle.text = item.subTitle ?: item.loaderResultString(requireContext()) addAdapter(tlmdAdapter)
binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success
binding.gamePlay.setOnClickListener {
startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri })
} }
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java) val retrofit = Retrofit.Builder()
binding.gamePin.isEnabled = shortcutManager.isRequestPinShortcutSupported .baseUrl("https://raw.githubusercontent.com/skyline-emu/title-meta/")
.addConverterFactory(Json.asConverterFactory(MediaType.get("application/json")))
.build()
binding.gamePin.setOnClickListener { val service : TitleMetaService = retrofit.create(TitleMetaService::class.java)
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 intent = Intent(context, EmulationActivity::class.java) service.getData(item.title).enqueue(object : Callback<TitleMetaData> {
intent.data = item.uri override fun onResponse(call : Call<TitleMetaData>, response : Response<TitleMetaData>) {
intent.action = Intent.ACTION_VIEW //response.body()?.let { populateAdaptersAfterData(it) }
TODO("NOT")
}
info.setIntent(intent) override fun onFailure(call : Call<TitleMetaData>, 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<GenericListItem<out ViewBinding>> = 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<GenericListItem<out ViewBinding>> = 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<GenericListItem<out ViewBinding>> = ArrayList()
entries.apply {
add(SectionHeaderViewItem(requireContext().getString(R.string.dlcs)))
}
dlcsAdapter.setItems(entries)
}
private fun populateTlmdAdapter(data : TitleMetaData) {
val entries : MutableList<GenericListItem<out ViewBinding>> = 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)
} }

View File

@ -23,12 +23,13 @@ import emu.skyline.adapter.inflater
import emu.skyline.data.AppItem import emu.skyline.data.AppItem
import emu.skyline.databinding.AppDialogGameInfoBinding import emu.skyline.databinding.AppDialogGameInfoBinding
import emu.skyline.loader.LoaderResult import emu.skyline.loader.LoaderResult
import emu.skyline.network.TitleRating
object ControllerBindingFactory : ViewBindingFactory { object ControllerBindingFactory : ViewBindingFactory {
override fun createBinding(parent : ViewGroup) = AppDialogGameInfoBinding.inflate(parent.inflater(), parent, false) 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<AppDialogGameInfoBinding>() { class GameInfoViewItem(private val context : Context, private val item : AppItem, private val testedVersion : String?, private val rating : TitleRating?) : GenericListItem<AppDialogGameInfoBinding>() {
override fun getViewBindingFactory() = ControllerBindingFactory override fun getViewBindingFactory() = ControllerBindingFactory
override fun bind(binding : AppDialogGameInfoBinding, position : Int) { 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.gameSubtitle.isSelected = true
binding.flex.visibility = if (rating == null && testedVersion == null) View.INVISIBLE else View.VISIBLE 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.testedVersion.text = testedVersion?.let { context.getString(R.string.tested_on, it) } ?: context.getString(R.string.not_tested)
binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success

View File

@ -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<Issue>? = null,
val notes : List<String>? = null,
val cheats : Map<String, Cheat>? = 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<List<String>>, // TODO
)
@Serializable
data class Cheat(
val title : String,
val description : String? = null,
val author : String? = null,
val code : String,
)

View File

@ -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<TitleMetaData>
}

View File

@ -1,82 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:nextFocusRight="@id/game_play"
android:padding="16dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/game_icon"
android:layout_width="150dp"
android:layout_height="150dp"
android:contentDescription="@string/icon"
android:focusable="false"
app:shapeAppearance="?attr/shapeAppearanceSmallComponent"
tools:src="@drawable/default_icon" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/game_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="18sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="The Legend of Zelda: Breath of the Wild" />
<TextView
android:id="@+id/game_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/game_title"
app:layout_constraintStart_toStartOf="@id/game_title"
tools:text="Nintendo" />
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:clipChildren="false"
android:layout_marginTop="12dp" android:clipToPadding="false"
app:layout_constraintStart_toStartOf="@id/game_title" android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/game_subtitle" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
app:flexWrap="wrap">
<Button
android:id="@+id/game_play"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:focusedByDefault="true"
android:text="@string/play"
android:textColor="?attr/colorAccent"
app:layout_minWidth="146dp"
app:icon="@drawable/ic_play"
app:iconTint="?attr/colorAccent" />
<Button
android:id="@+id/game_pin"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
app:layout_maxWidth="55dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:icon="@drawable/ic_add_home"
android:textColor="?attr/colorAccent" />
</com.google.android.flexbox.FlexboxLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?><!-- This is a dummy layout which serves as a preview of AppDialog -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp">
<include
layout="@layout/app_dialog_drag_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
layout="@layout/app_dialog_game_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/updates_title"
layout="@layout/app_dialog_section_header" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_updates_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_updates_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/dlcs_title"
layout="@layout/app_dialog_section_header" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_dlcs_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_dlcs_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/issues_title"
layout="@layout/app_dialog_section_header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_issues_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_issues_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/notes_title"
layout="@layout/app_dialog_section_header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_notes_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/cheats_title"
layout="@layout/app_dialog_section_header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_cheats_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include
android:id="@+id/saves_title"
layout="@layout/app_dialog_section_header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
<include layout="@layout/app_dialog_saves_item" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/section_spacing" />
</LinearLayout>
</ScrollView>

View File

@ -5,7 +5,7 @@
<dimen name="corner_radius_large">12dp</dimen> <dimen name="corner_radius_large">12dp</dimen>
<!-- Main - AppDialog --> <!-- Main - AppDialog -->
<dimen name="section_spacing">10dp</dimen> <dimen name="section_spacing">6dp</dimen>
<dimen name="section_title_padding">6dp</dimen> <dimen name="section_title_padding">6dp</dimen>
<dimen name="section_item_card_padding">12dp</dimen> <dimen name="section_item_card_padding">12dp</dimen>
</resources> </resources>

View File

@ -15,6 +15,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.0-beta02' classpath 'com.android.tools.build:gradle:7.1.0-beta02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong