hltb example: update snapshot-cart-to-services flow
This commit is contained in:
parent
ec81c2a24f
commit
5b5810c35d
|
@ -43,6 +43,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
// --- defaults
|
// --- defaults
|
||||||
implementation("androidx.core:core-ktx:1.6.0")
|
implementation("androidx.core:core-ktx:1.6.0")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.3.1")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||||
implementation("com.google.android.material:material:1.4.0")
|
implementation("com.google.android.material:material:1.4.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
package="be.kuleuven.howlongtobeat">
|
package="be.kuleuven.howlongtobeat">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-permission android:name="android.permission.CAMERA" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -21,6 +21,16 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- WHY? See https://medium.com/codex/how-to-use-the-android-activity-result-api-for-selecting-and-taking-images-5dbcc3e6324b -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -0,0 +1,18 @@
|
||||||
|
package be.kuleuven.howlongtobeat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
fun Uri.toBitmap(activity: Activity): Bitmap {
|
||||||
|
return BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -35,7 +35,9 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) {
|
||||||
binding.rvGameList.adapter = adapter
|
binding.rvGameList.adapter = adapter
|
||||||
binding.rvGameList.layoutManager = LinearLayoutManager(this.context)
|
binding.rvGameList.layoutManager = LinearLayoutManager(this.context)
|
||||||
|
|
||||||
binding.btnAddTodo.setOnClickListener(this::addNewTotoItem)
|
binding.btnAddTodo.setOnClickListener {
|
||||||
|
findNavController().navigate(R.id.action_gameListFragment_to_loadingFragment)
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,10 +48,6 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addNewTotoItem(it: View) {
|
|
||||||
findNavController().navigate(R.id.action_gameListFragment_to_loadingFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fun onHltbGamesRetrieved(games: List<HowLongToBeatResult>) {
|
fun onHltbGamesRetrieved(games: List<HowLongToBeatResult>) {
|
||||||
gameList.clear()
|
gameList.clear()
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
package be.kuleuven.howlongtobeat
|
package be.kuleuven.howlongtobeat
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
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 android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
||||||
class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : RecyclerView.Adapter<HltbResultsAdapter.HltbResultsViewHolder>() {
|
class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : RecyclerView.Adapter<HltbResultsAdapter.HltbResultsViewHolder>() {
|
||||||
|
|
||||||
|
@ -17,10 +25,25 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: HltbResultsViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: HltbResultsViewHolder, position: Int) {
|
||||||
val currentResult = items[position]
|
val itm = items[position]
|
||||||
|
|
||||||
holder.itemView.apply {
|
holder.itemView.apply {
|
||||||
val txtHltbItemResult = findViewById<TextView>(R.id.txtHltbItemResult)
|
findViewById<TextView>(R.id.txtHltbItemResult).text = itm.toString()
|
||||||
txtHltbItemResult.text = currentResult.title
|
val boxArtView = findViewById<ImageView>(R.id.imgHltbItemResult)
|
||||||
|
|
||||||
|
if(itm.hasBoxart()) {
|
||||||
|
MainScope().launch{
|
||||||
|
var art: Bitmap? = null
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
art = BitmapFactory.decodeStream(itm.boxartUrl().openConnection().getInputStream())
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
boxArtView.setImageBitmap(art)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boxArtView.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
package be.kuleuven.howlongtobeat
|
package be.kuleuven.howlongtobeat
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.content.PermissionChecker
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import be.kuleuven.howlongtobeat.cartridges.Cartridge
|
import be.kuleuven.howlongtobeat.cartridges.Cartridge
|
||||||
|
@ -23,6 +24,8 @@ import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
|
|
||||||
|
@ -31,10 +34,12 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
private lateinit var visionClient: GoogleVisionClient
|
private lateinit var visionClient: GoogleVisionClient
|
||||||
|
|
||||||
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
|
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
|
||||||
private lateinit var cameraActivityResult: ActivityResultLauncher<Void>
|
private lateinit var cameraActivityResult: ActivityResultLauncher<Uri>
|
||||||
private lateinit var main: MainActivity
|
private lateinit var main: MainActivity
|
||||||
private lateinit var binding: FragmentLoadingBinding
|
private lateinit var binding: FragmentLoadingBinding
|
||||||
|
|
||||||
|
private var snapshot: Uri? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -47,24 +52,33 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
visionClient = GoogleVisionClient()
|
visionClient = GoogleVisionClient()
|
||||||
hltbClient = HLTBClient(main.applicationContext)
|
hltbClient = HLTBClient(main.applicationContext)
|
||||||
|
|
||||||
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicturePreview(), this::cameraSnapTaken)
|
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicture(), this::cameraSnapTaken)
|
||||||
cameraPermissionActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission(), this::cameraPermissionAcceptedOrDenied)
|
cameraPermissionActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission(), this::cameraPermissionAcceptedOrDenied)
|
||||||
|
binding.btnRetryAfterLoading.setOnClickListener {
|
||||||
|
tryToMakeCameraSnap()
|
||||||
|
}
|
||||||
tryToMakeCameraSnap()
|
tryToMakeCameraSnap()
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cameraSnapTaken(pic: Bitmap) {
|
private fun cameraSnapTaken(succeeded: Boolean) {
|
||||||
|
if(!succeeded || snapshot == null) {
|
||||||
|
errorInProgress("Photo could not be saved, try again?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progress("Scaling image for upload...")
|
||||||
|
val bitmap = snapshot!!.toBitmap(main).scaleToWidth(1600)
|
||||||
|
|
||||||
MainScope().launch{
|
MainScope().launch{
|
||||||
findGameBasedOnCameraSnap(pic)
|
findGameBasedOnCameraSnap(bitmap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
|
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
|
||||||
progress("Unleashing Google Vision on the pic...")
|
progress("Unleashing Google Vision on the pic...")
|
||||||
// TODO remove in future
|
val cartCode = visionClient.findCartCodeViaGoogleVision(pic)
|
||||||
val dummypic = BitmapFactory.decodeResource(resources, R.drawable.supermarioland2)
|
|
||||||
val cartCode = visionClient.findCartCodeViaGoogleVision(dummypic)
|
|
||||||
|
|
||||||
if (cartCode == null) {
|
if (cartCode == null) {
|
||||||
errorInProgress("Unable to find a code in your pic. Retry?")
|
errorInProgress("Unable to find a code in your pic. Retry?")
|
||||||
|
@ -93,35 +107,46 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
if(succeeded) {
|
if(succeeded) {
|
||||||
makeCameraSnap()
|
makeCameraSnap()
|
||||||
} else {
|
} else {
|
||||||
progress("Camera permission required!")
|
errorInProgress("Camera permission required!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryToMakeCameraSnap() {
|
private fun tryToMakeCameraSnap() {
|
||||||
|
binding.btnRetryAfterLoading.hide()
|
||||||
progress("Making snapshot with camera...")
|
progress("Making snapshot with camera...")
|
||||||
if(ContextCompat.checkSelfPermission(main, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
|
if(PermissionChecker.checkSelfPermission(main, Manifest.permission.CAMERA) != PermissionChecker.PERMISSION_GRANTED) {
|
||||||
cameraPermissionActivityResult.launch(Manifest.permission.CAMERA)
|
cameraPermissionActivityResult.launch(Manifest.permission.CAMERA)
|
||||||
|
} else {
|
||||||
|
makeCameraSnap()
|
||||||
}
|
}
|
||||||
makeCameraSnap()
|
}
|
||||||
|
|
||||||
|
private fun createNewTempCameraFile() {
|
||||||
|
val tempFile = File.createTempFile("hltbCameraSnap", ".png", main.cacheDir).apply {
|
||||||
|
createNewFile()
|
||||||
|
deleteOnExit()
|
||||||
|
}
|
||||||
|
snapshot = FileProvider.getUriForFile(main.applicationContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeCameraSnap() {
|
private fun makeCameraSnap() {
|
||||||
cameraActivityResult.launch(null)
|
createNewTempCameraFile()
|
||||||
|
cameraActivityResult.launch(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun progress(msg: String) {
|
private fun progress(msg: String) {
|
||||||
if(!binding.indeterminateBar.isAnimating) {
|
if(!binding.indeterminateBar.isVisible) {
|
||||||
binding.indeterminateBar.animate()
|
binding.indeterminateBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
binding.txtLoading.text = msg
|
binding.txtLoading.text = msg
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun errorInProgress(msg: String) {
|
private fun errorInProgress(msg: String) {
|
||||||
progress(msg)
|
progress(msg)
|
||||||
binding.indeterminateBar.clearAnimation()
|
binding.indeterminateBar.visibility = View.GONE
|
||||||
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
|
||||||
// todo show retry button or something?
|
binding.btnRetryAfterLoading.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -19,6 +19,7 @@ class GoogleVisionClient {
|
||||||
// TODO encrypt and store externally: https://cloud.google.com/docs/authentication/api-keys?hl=en&visit_id=637642790375688006-1838986332&rd=1
|
// 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)
|
private val vision = Vision.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance(), null)
|
||||||
.setVisionRequestInitializer(VisionRequestInitializer("AIzaSyCaMjQQOY7508y95riDhr25fsrqe3m2JW0"))
|
.setVisionRequestInitializer(VisionRequestInitializer("AIzaSyCaMjQQOY7508y95riDhr25fsrqe3m2JW0"))
|
||||||
|
.setApplicationName("How Long To Beat")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
|
suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
|
||||||
|
@ -39,7 +40,9 @@ class GoogleVisionClient {
|
||||||
}
|
}
|
||||||
response = vision.images().annotate(batch).execute()
|
response = vision.images().annotate(batch).execute()
|
||||||
}
|
}
|
||||||
if(response.responses.isEmpty()) {
|
if(response.responses.isEmpty()
|
||||||
|
|| response.responses.get(0).textAnnotations == null
|
||||||
|
|| response.responses.get(0).textAnnotations.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,4 +51,4 @@ class GoogleVisionClient {
|
||||||
}.firstOrNull()
|
}.firstOrNull()
|
||||||
return gbId?.description ?: null
|
return gbId?.description ?: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,14 @@ import com.android.volley.toolbox.Volley
|
||||||
|
|
||||||
class HLTBClient(val context: Context) {
|
class HLTBClient(val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DOMAIN = "https://howlongtobeat.com"
|
||||||
|
}
|
||||||
|
|
||||||
// Inspired by https://www.npmjs.com/package/howlongtobeat
|
// Inspired by https://www.npmjs.com/package/howlongtobeat
|
||||||
// The API is abysmal, but hey, it works...
|
// The API is abysmal, but hey, it works...
|
||||||
class HLTBRequest(val query: String, responseListener: Response.Listener<String>) :
|
class HLTBRequest(val query: String, responseListener: Response.Listener<String>) :
|
||||||
StringRequest(Method.POST, "https://howlongtobeat.com/search_results.php?page=1", responseListener,
|
StringRequest(Method.POST, "$DOMAIN/search_results.php?page=1", responseListener,
|
||||||
Response.ErrorListener {
|
Response.ErrorListener {
|
||||||
println("Something went wrong: ${it.message}")
|
println("Something went wrong: ${it.message}")
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
package be.kuleuven.howlongtobeat.hltb
|
package be.kuleuven.howlongtobeat.hltb
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HowLongToBeatResult(val title: String, val howlong: Double, val finished: Boolean = false) : java.io.Serializable {
|
data class HowLongToBeatResult(val title: String, val howlong: Double, val boxart: String = "") : java.io.Serializable {
|
||||||
companion object {
|
companion object {
|
||||||
const val RESULT = "HowLongToBeatResult"
|
const val RESULT = "HowLongToBeatResult"
|
||||||
const val CODE = "CartCode"
|
const val CODE = "CartCode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasBoxart(): Boolean = boxart.startsWith(HLTBClient.DOMAIN)
|
||||||
|
fun boxartUrl(): URL = URL(boxart)
|
||||||
|
override fun toString(): String = "$title ($howlong hrs)"
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ object HowLongToBeatResultParser {
|
||||||
|
|
||||||
private val titleMatcher = """<a class=".+" title="(.+)" href=""".toRegex()
|
private val titleMatcher = """<a class=".+" title="(.+)" href=""".toRegex()
|
||||||
private val hourMatcher = """<div class=".+">(.+) Hours""".toRegex()
|
private val hourMatcher = """<div class=".+">(.+) Hours""".toRegex()
|
||||||
|
private val boxArtMatcher = """<img alt=".+" src="(.+)"""".toRegex()
|
||||||
|
|
||||||
fun parse(html: String): List<HowLongToBeatResult> {
|
fun parse(html: String): List<HowLongToBeatResult> {
|
||||||
val result = arrayListOf<HowLongToBeatResult>()
|
val result = arrayListOf<HowLongToBeatResult>()
|
||||||
|
@ -13,14 +14,26 @@ object HowLongToBeatResultParser {
|
||||||
if(matched != null) {
|
if(matched != null) {
|
||||||
val (title) = matched.destructured
|
val (title) = matched.destructured
|
||||||
val hour = parseHoursFromRow(i, rows)
|
val hour = parseHoursFromRow(i, rows)
|
||||||
|
val boxart = parseBoxArtFromRow(i, rows)
|
||||||
|
|
||||||
result.add(HowLongToBeatResult(title, hour))
|
result.add(HowLongToBeatResult(title, hour, boxart))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseBoxArtFromRow(row: Int, rows: List<String>): String {
|
||||||
|
// three rows up, there should be an image tag with the box art
|
||||||
|
if(row - 3 >= 0) {
|
||||||
|
val matchedBoxArt = boxArtMatcher.find(rows[row - 3])
|
||||||
|
if(matchedBoxArt != null) {
|
||||||
|
return HLTBClient.DOMAIN + matchedBoxArt.groupValues[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseHoursFromRow(row: Int, rows: List<String>): Double {
|
private fun parseHoursFromRow(row: Int, rows: List<String>): Double {
|
||||||
var hour = -1.0
|
var hour = -1.0
|
||||||
// two rows down, there should be a <div class="search_list_tidbit center time_100">6½ Hours </div>
|
// two rows down, there should be a <div class="search_list_tidbit center time_100">6½ Hours </div>
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:indeterminate="true" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/txtLoading"
|
android:id="@+id/txtLoading"
|
||||||
|
@ -25,4 +26,17 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/btnRetryAfterLoading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:contentDescription="Retry"
|
||||||
|
android:src="@android:drawable/ic_menu_rotate"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
@ -12,9 +13,19 @@
|
||||||
android:text="Some Result"
|
android:text="Some Result"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="parent"
|
app:layout_constraintEnd_toStartOf="@+id/imgHltbItemResult"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imgHltbItemResult"
|
||||||
|
android:layout_width="105dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.727"
|
||||||
|
app:srcCompat="@android:drawable/ic_menu_gallery" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path
|
||||||
|
name="cached_files"
|
||||||
|
path="." />
|
||||||
|
<files-path
|
||||||
|
name="images"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
|
@ -73,6 +73,8 @@ I/System.out: <div class="search_list_tidbit text_white shadow_text">Ma
|
||||||
assertEquals(3, result.size)
|
assertEquals(3, result.size)
|
||||||
assertEquals("Super Mario Land", smland.title)
|
assertEquals("Super Mario Land", smland.title)
|
||||||
assertEquals("Super Mario 3D Land", sm3dland.title)
|
assertEquals("Super Mario 3D Land", sm3dland.title)
|
||||||
|
assertEquals("https://howlongtobeat.com/games/250px-Supermariolandboxart.jpg", smland.boxart)
|
||||||
|
assertEquals("https://howlongtobeat.com/games/250px-Super-Mario-3D-Land-Logo.jpg", sm3dland.boxart)
|
||||||
assertEquals(1.0, smland.howlong, 0.0)
|
assertEquals(1.0, smland.howlong, 0.0)
|
||||||
assertEquals(6.5, sm3dland.howlong, 0.0)
|
assertEquals(6.5, sm3dland.howlong, 0.0)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue