hltb example: backlink hltb result to game list
This commit is contained in:
parent
5b5810c35d
commit
adde1e8e70
|
@ -4,15 +4,26 @@ import android.app.Activity
|
|||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import java.net.URL
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Uri.toBitmap(activity: Activity): Bitmap {
|
||||
return BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
|
||||
}
|
||||
fun Uri.toBitmap(activity: Activity): Bitmap
|
||||
= BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
|
||||
|
||||
fun URL.downloadAsImage(): Bitmap =
|
||||
BitmapFactory.decodeStream(this.openConnection().getInputStream())
|
||||
|
||||
fun Bitmap.scaleToWidth(width: Int): Bitmap {
|
||||
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||
val height = (width / aspectRatio).roundToInt()
|
||||
|
||||
return Bitmap.createScaledBitmap(this, width, height, false)
|
||||
}
|
||||
|
||||
fun View.ensureVisible() {
|
||||
if(!this.isVisible) {
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import be.kuleuven.howlongtobeat.databinding.FragmentGamelistBinding
|
||||
import be.kuleuven.howlongtobeat.model.Game
|
||||
import be.kuleuven.howlongtobeat.model.GameRepository
|
||||
import be.kuleuven.howlongtobeat.model.room.GameRepositoryRoomImpl
|
||||
|
||||
class GameListFragment : Fragment(R.layout.fragment_gamelist) {
|
||||
|
||||
|
@ -28,7 +27,7 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) {
|
|||
): View? {
|
||||
binding = FragmentGamelistBinding.inflate(layoutInflater)
|
||||
main = activity as MainActivity
|
||||
gameRepository = GameRepositoryRoomImpl(main.applicationContext)
|
||||
gameRepository = GameRepository.defaultImpl(main.applicationContext)
|
||||
loadGames()
|
||||
|
||||
adapter = GameListAdapter(gameList)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package be.kuleuven.howlongtobeat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -19,8 +19,11 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
|
|||
|
||||
inner class HltbResultsViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
|
||||
|
||||
private lateinit var parentFragment: HltbResultsFragment
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HltbResultsViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_hltbresult, parent, false)
|
||||
parentFragment = parent.findFragment()
|
||||
return HltbResultsViewHolder(view)
|
||||
}
|
||||
|
||||
|
@ -28,6 +31,10 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
|
|||
val itm = items[position]
|
||||
|
||||
holder.itemView.apply {
|
||||
setOnClickListener {
|
||||
parentFragment.addResultToGameLibrary(itm)
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.txtHltbItemResult).text = itm.toString()
|
||||
val boxArtView = findViewById<ImageView>(R.id.imgHltbItemResult)
|
||||
|
||||
|
@ -35,7 +42,7 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
|
|||
MainScope().launch{
|
||||
var art: Bitmap? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
art = BitmapFactory.decodeStream(itm.boxartUrl().openConnection().getInputStream())
|
||||
art = itm.boxartUrl().downloadAsImage()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
boxArtView.setImageBitmap(art)
|
||||
|
|
|
@ -5,13 +5,18 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import be.kuleuven.howlongtobeat.databinding.FragmentHltbresultsBinding
|
||||
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
||||
import be.kuleuven.howlongtobeat.model.Game
|
||||
import be.kuleuven.howlongtobeat.model.GameRepository
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
|
||||
private lateinit var binding: FragmentHltbresultsBinding
|
||||
private lateinit var adapter: HltbResultsAdapter
|
||||
private lateinit var gameRepository: GameRepository
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -22,10 +27,18 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
|
|||
|
||||
val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List<HowLongToBeatResult>
|
||||
|
||||
gameRepository = GameRepository.defaultImpl(requireContext())
|
||||
|
||||
adapter = HltbResultsAdapter(resultFromLoadingFragment)
|
||||
binding.rvHltbResult.adapter = adapter
|
||||
binding.rvHltbResult.layoutManager = LinearLayoutManager(this.context)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun addResultToGameLibrary(hltbResult: HowLongToBeatResult) {
|
||||
gameRepository.save(Game(hltbResult))
|
||||
Snackbar.make(requireView(), "Added ${hltbResult.title} to library!", Snackbar.LENGTH_LONG).show()
|
||||
findNavController().navigate(R.id.action_hltbResultsFragment_to_gameListFragment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package be.kuleuven.howlongtobeat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
interface ImageRecognizer {
|
||||
suspend fun recognizeCartCode(image: Bitmap): String?
|
||||
}
|
|
@ -12,7 +12,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import be.kuleuven.howlongtobeat.cartridges.Cartridge
|
||||
|
@ -26,12 +25,11 @@ import kotlinx.coroutines.MainScope
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
|
||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||
|
||||
private lateinit var hltbClient: HLTBClient
|
||||
private lateinit var cartRepo: CartridgesRepository
|
||||
private lateinit var visionClient: GoogleVisionClient
|
||||
private lateinit var imageRecognizer: ImageRecognizer
|
||||
|
||||
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
|
||||
private lateinit var cameraActivityResult: ActivityResultLauncher<Uri>
|
||||
|
@ -49,7 +47,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
main = activity as MainActivity
|
||||
|
||||
cartRepo = CartridgesRepository.fromAsset(main.applicationContext)
|
||||
visionClient = GoogleVisionClient()
|
||||
imageRecognizer = GoogleVisionClient()
|
||||
hltbClient = HLTBClient(main.applicationContext)
|
||||
|
||||
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicture(), this::cameraSnapTaken)
|
||||
|
@ -77,11 +75,12 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
}
|
||||
|
||||
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
|
||||
progress("Unleashing Google Vision on the pic...")
|
||||
val cartCode = visionClient.findCartCodeViaGoogleVision(pic)
|
||||
/*
|
||||
progress("Recognizing game cart from picture...")
|
||||
val cartCode = imageRecognizer.recognizeCartCode(pic)
|
||||
|
||||
if (cartCode == null) {
|
||||
errorInProgress("Unable to find a code in your pic. Retry?")
|
||||
errorInProgress("No cart code in your pic found. Retry?")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -89,16 +88,19 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
val foundCart = cartRepo.find(cartCode)
|
||||
|
||||
if (foundCart == Cartridge.UNKNOWN_CART) {
|
||||
errorInProgress("$cartCode is an unknown game cartridge. Retry?")
|
||||
errorInProgress("$cartCode is an unknown game cart. Retry?")
|
||||
return
|
||||
}
|
||||
|
||||
progress("Valid cart code $cartCode, looking in HLTB...")
|
||||
|
||||
*/
|
||||
val foundCart = Cartridge("DMG", "Super Mario Land", "DMG-something")
|
||||
hltbClient.find(foundCart.title) {
|
||||
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart $cartCode", Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show()
|
||||
|
||||
// TODO wat als geen hltb results gevonden?
|
||||
val bundle = bundleOf(HowLongToBeatResult.RESULT to it, HowLongToBeatResult.CODE to cartCode)
|
||||
val bundle = bundleOf(HowLongToBeatResult.RESULT to it)
|
||||
findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +125,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
}
|
||||
|
||||
private fun createNewTempCameraFile() {
|
||||
// a <Provider/> should be present in the manifest file.
|
||||
val tempFile = File.createTempFile("hltbCameraSnap", ".png", main.cacheDir).apply {
|
||||
createNewFile()
|
||||
deleteOnExit()
|
||||
|
@ -136,9 +139,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
}
|
||||
|
||||
private fun progress(msg: String) {
|
||||
if(!binding.indeterminateBar.isVisible) {
|
||||
binding.indeterminateBar.visibility = View.VISIBLE
|
||||
}
|
||||
binding.indeterminateBar.ensureVisible()
|
||||
binding.txtLoading.text = msg
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package be.kuleuven.howlongtobeat.google
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import be.kuleuven.howlongtobeat.ImageRecognizer
|
||||
import be.kuleuven.howlongtobeat.asEncodedGoogleVisionImage
|
||||
import be.kuleuven.howlongtobeat.cartridges.Cartridge
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
|
@ -14,7 +15,7 @@ import com.google.api.services.vision.v1.model.Feature
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class GoogleVisionClient {
|
||||
class GoogleVisionClient : ImageRecognizer {
|
||||
|
||||
// TODO encrypt and store externally: https://cloud.google.com/docs/authentication/api-keys?hl=en&visit_id=637642790375688006-1838986332&rd=1
|
||||
private val vision = Vision.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance(), null)
|
||||
|
@ -22,7 +23,7 @@ class GoogleVisionClient {
|
|||
.setApplicationName("How Long To Beat")
|
||||
.build()
|
||||
|
||||
suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
|
||||
private suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
|
||||
var response: BatchAnnotateImagesResponse
|
||||
withContext(Dispatchers.IO) {
|
||||
val sml2Data = cameraSnap.asEncodedGoogleVisionImage()
|
||||
|
@ -51,4 +52,6 @@ class GoogleVisionClient {
|
|||
}.firstOrNull()
|
||||
return gbId?.description ?: null
|
||||
}
|
||||
|
||||
override suspend fun recognizeCartCode(image: Bitmap): String? = findCartCodeViaGoogleVision(image)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import java.net.URL
|
|||
data class HowLongToBeatResult(val title: String, val howlong: Double, val boxart: String = "") : java.io.Serializable {
|
||||
companion object {
|
||||
const val RESULT = "HowLongToBeatResult"
|
||||
const val CODE = "CartCode"
|
||||
}
|
||||
|
||||
fun hasBoxart(): Boolean = boxart.startsWith(HLTBClient.DOMAIN)
|
||||
|
|
|
@ -3,6 +3,7 @@ package be.kuleuven.howlongtobeat.model
|
|||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
||||
|
||||
@Entity
|
||||
data class Game(
|
||||
|
@ -10,6 +11,10 @@ data class Game(
|
|||
@ColumnInfo(name = "is_done") var isDone: Boolean = false,
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable {
|
||||
|
||||
constructor(result: HowLongToBeatResult) : this(result.title)
|
||||
|
||||
// TODO more columns (platform? hours, paths of images?)
|
||||
|
||||
fun check() {
|
||||
isDone = true
|
||||
}
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
package be.kuleuven.howlongtobeat.model
|
||||
|
||||
import android.content.Context
|
||||
import be.kuleuven.howlongtobeat.model.room.GameRepositoryRoomImpl
|
||||
|
||||
interface GameRepository {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* This makes it easier to switch between implementations if needed and does not expose the RoomImpl
|
||||
* Dependency Injection is the better alternative.
|
||||
*/
|
||||
fun defaultImpl(appContext: Context): GameRepository = GameRepositoryRoomImpl(appContext)
|
||||
}
|
||||
|
||||
fun load(): List<Game>
|
||||
|
||||
fun save(items: List<Game>)
|
||||
fun save(game: Game)
|
||||
|
||||
fun overwrite(items: List<Game>)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,13 @@ class GameRepositoryRoomImpl(appContext: Context) :
|
|||
|
||||
override fun load(): List<Game> = dao.query()
|
||||
|
||||
override fun save(items: List<Game>) {
|
||||
override fun save(game: Game) {
|
||||
db.runInTransaction {
|
||||
dao.insert(listOf(game))
|
||||
}
|
||||
}
|
||||
|
||||
override fun overwrite(items: List<Game>) {
|
||||
// You'll learn more about transactions in the database course in the 3rd academic year.
|
||||
db.runInTransaction {
|
||||
dao.deleteAll()
|
||||
|
|
|
@ -23,5 +23,9 @@
|
|||
<fragment
|
||||
android:id="@+id/hltbResultsFragment"
|
||||
android:name="be.kuleuven.howlongtobeat.HltbResultsFragment"
|
||||
android:label="HltbResultsFragment" />
|
||||
android:label="HltbResultsFragment" >
|
||||
<action
|
||||
android:id="@+id/action_hltbResultsFragment_to_gameListFragment"
|
||||
app:destination="@id/gameListFragment" />
|
||||
</fragment>
|
||||
</navigation>
|
Loading…
Reference in New Issue