diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f094ad..bff1e36 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,8 @@ dependencies { // --- kotlinx extras implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1") // --- navigation // see https://developer.android.com/guide/navigation/navigation-getting-started diff --git a/app/src/androidTest/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGoTest.kt b/app/src/androidTest/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGoTest.kt new file mode 100644 index 0000000..2d6a2de --- /dev/null +++ b/app/src/androidTest/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGoTest.kt @@ -0,0 +1,35 @@ +package be.kuleuven.howlongtobeat.cartridges + +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + +class CartridgeFinderViaDuckDuckGoTest { + + private lateinit var repo: CartridgeFinderViaDuckDuckGo + + @Before + fun setUp() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + repo = CartridgeFinderViaDuckDuckGo(appContext) + // no need to set the main dispatcher, it's running on the device itself! + } + + @Test + fun find_knownCodesForDuckDuckGo_returnsCorrectCartridge() = runBlocking { + launch(Dispatchers.Main) { + val smbDeluxe = repo.find("CGB-AHYE-USA") + assertEquals("super mario bros deluxe", smbDeluxe?.title) + assertEquals("CGB-AHYE-USA", smbDeluxe?.code) + + val marioGolf = repo.find("cgb-awxp-eur-1") + assertEquals("mario golf", marioGolf?.title) + assertEquals("cgb-awxp-eur-1", marioGolf?.code) + } + println("launched main dispatcher") // this must be there: Kotlin-to-Java doesn't recognize the retval because of runBlocking. + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/kuleuven/howlongtobeat/hltb/HLTBClientTest.kt b/app/src/androidTest/java/be/kuleuven/howlongtobeat/hltb/HLTBClientTest.kt new file mode 100644 index 0000000..d2bdd64 --- /dev/null +++ b/app/src/androidTest/java/be/kuleuven/howlongtobeat/hltb/HLTBClientTest.kt @@ -0,0 +1,46 @@ +package be.kuleuven.howlongtobeat.hltb + +import androidx.test.platform.app.InstrumentationRegistry +import be.kuleuven.howlongtobeat.cartridges.Cartridge +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + +class HLTBClientTest { + + private lateinit var client: HLTBClient + + @Before + fun setUp() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + client = HLTBClient(appContext) + } + + @Test + fun find_unknownGame_returnsNull() = runBlocking { + launch(Dispatchers.Main) { + val results = client.find(Cartridge("type", "moesjamarramarra tis niet omdat ik wijs dat ge moet kijken he", "invalid")) + assertNull(results) + } + println("Dispatche launched") + } + + @Test + fun find_someValidGame_retrievesSomeGamesBasedOnTitle() = runBlocking { + launch(Dispatchers.Main) { + val results = client.find(Cartridge("type", "Gex: Enter the Gecko", "CGB-GEX-666")) + assertEquals(1, results?.size) + val gex = results?.single()!! + + assertEquals("Gex Enter the Gecko", gex.title) + assertEquals("CGB-GEX-666", gex.cartCode) + assertEquals(9.0, gex.howlong) // that long, huh. + } + println("Dispatche launched") + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/be/kuleuven/howlongtobeat/model/room/GamePersistenceTests.kt b/app/src/androidTest/java/be/kuleuven/howlongtobeat/model/room/GamePersistenceTests.kt index 8d47066..0da0be4 100644 --- a/app/src/androidTest/java/be/kuleuven/howlongtobeat/model/room/GamePersistenceTests.kt +++ b/app/src/androidTest/java/be/kuleuven/howlongtobeat/model/room/GamePersistenceTests.kt @@ -33,20 +33,20 @@ class GamePersistenceTests { @Test fun todoItemCanBePersisted() { - val item = Game("brush my little pony", false) + val item = Game("brush my little pony","code", 0.0, false) dao.insert(arrayListOf(item)) val refreshedItem = dao.query().single() with(refreshedItem) { assertEquals(item.title, title) - assertEquals(item.isDone, isDone) + assertEquals(item.finished, finished) assertEquals(1, id) } } @Test fun updateUpdatesTodoPropertiesInDb() { - var todo = Game("git good at Hollow Knight", false) + var todo = Game("git good at Hollow Knight", "code", 10.5, false) dao.insert(arrayListOf(todo)) todo = dao.query().single() // refresh to get the ID, otherwise update() will update where ID = 0 @@ -57,12 +57,12 @@ class GamePersistenceTests { assertEquals(1, itemsFromDb.size) with(itemsFromDb.single()) { assertEquals(todo.title, title) - assertEquals(true, isDone) + assertEquals(true, finished) } } private fun finallyFinishHollowKnight(item: Game) { println("Congrats! On to Demon Souls?") - item.check() + item.finish() } } \ No newline at end of file diff --git a/app/src/main/assets/cartridges.csv b/app/src/main/assets/cartridges.csv index f3a245d..79115fa 100644 --- a/app/src/main/assets/cartridges.csv +++ b/app/src/main/assets/cartridges.csv @@ -293,6 +293,8 @@ DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #1,gekkio-1,https://gbhwdb.gekk DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #2,gekkio-2,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-2.html,gekkio,DMG-ML-ITA,20A,DMG-BEAN-02,I,6,Jun/1996,June/1996,1996,6,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9627 D,Sharp,Sharp,27/1996,Week 27/1996,1996,,27,MBC1B1,DMG MBC1B1 Nintendo S 9625 3 A,Sharp,Sharp,25/1996,Week 25/1996,1996,,25,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #3,gekkio-3,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-3.html,gekkio,DMG-MLA,22A,DMG-BEAN-02,B,5,Jul/1993,July/1993,1993,7,LH53514,DMG-MLA-1 S LH5314B2 JAPAN B1 9339 E,Sharp,Sharp,39/1993,Week 39/1993,1993,,39,MBC1B,DMG MBC1B Nintendo J9330BR,Motorola,Motorola,30/1993,Week 30/1993,1993,,30,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/tobiasvl-1.html,tobiasvl,DMG-ML-USA-1,22A,DMG-BEAN-10,I,5,Jul/1998,July/1998,1998,7,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9836 D,Sharp,Sharp,36/1998,Week 36/1998,1998,,36,MBC1B,DMG MBC1-B Nintendo P 8'59,,,1998,1998,1998,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (Europe)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MQ-EUR,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MO-USA,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MQ-NOE,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/tobiasvl-1.html,tobiasvl,DMG-MQ-UKV,23,DMG-DECN-02,I,2,Nov/1992,November/1992,1992,11,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9248 D,Sharp,Sharp,48/1992,Week 48/1992,1992,,48,MBC1B,Nintendo DMG MBC1B N 9245BA035,,,45/1992,Week 45/1992,1992,,45,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D244 7 BC,LSI Logic,LSI Logic,44/1992,Week 44/1992,1992,,44,MM1026A,2J5 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1,20B,DMG-DECN-10,K,6,Mar/1998,March/1998,1998,3,N-4001EJGW,DMG-MQE-2 E1 N-4001EJGW-J08 9822E7026,,,22/1998,Week 22/1998,1998,,22,MBC1B1,DMG MBC1B1 Nintendo S 9816 5 A,Sharp,Sharp,16/1998,Week 16/1998,1998,,16,BR6265BF-10SL,BR6265BF-10SL 814 136N,ROHM,ROHM,14/1998,Week 14/1998,1998,,14,MM1026A,746 26A,Mitsumi,Mitsumi,1997,1997,1997,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt b/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt index 4ca38a7..fa70ad6 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/AndroidExtensions.kt @@ -1,20 +1,30 @@ package be.kuleuven.howlongtobeat -import android.app.Activity +import android.content.ContentResolver +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.view.View import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import java.net.URL import kotlin.math.roundToInt -fun Uri.toBitmap(activity: Activity): Bitmap - = BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this)) +fun Fragment.requireContentResolver(): ContentResolver = requireContext().contentResolver + +fun Uri.toBitmap(contentResolver: ContentResolver): Bitmap + = BitmapFactory.decodeStream(contentResolver.openInputStream(this)) fun URL.downloadAsImage(): Bitmap = BitmapFactory.decodeStream(this.openConnection().getInputStream()) +fun Bitmap.save(location: String, context: Context) { + context.openFileOutput(location, Context.MODE_PRIVATE).use { + compress(Bitmap.CompressFormat.JPEG, 85, it) + } +} + fun Bitmap.scaleToWidth(width: Int): Bitmap { val aspectRatio = this.width.toFloat() / this.height.toFloat() val height = (width / aspectRatio).roundToInt() diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/GameDetailFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/GameDetailFragment.kt new file mode 100644 index 0000000..609dfdd --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/GameDetailFragment.kt @@ -0,0 +1,49 @@ +package be.kuleuven.howlongtobeat + +import android.graphics.BitmapFactory +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import be.kuleuven.howlongtobeat.databinding.FragmentGamedetailBinding +import be.kuleuven.howlongtobeat.model.Game +import be.kuleuven.howlongtobeat.model.GameRepository +import com.google.android.material.snackbar.Snackbar + +class GameDetailFragment : Fragment(R.layout.fragment_gamedetail) { + + private lateinit var binding: FragmentGamedetailBinding + private lateinit var gameRepo: GameRepository + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentGamedetailBinding.inflate(layoutInflater) + + gameRepo = GameRepository.defaultImpl(requireContext()) + val gameId = arguments?.getSerializable(Game.GAME_ID).toString().toInt() + val game = gameRepo.find(gameId) + + binding.txtDetailTitle.text = "${game.title}\n${game.cartCode}" + binding.txtDetailGameStats.text = "${game.howLongToBeat} hr(s) to beat" + binding.chkDetailFinished.isChecked = game.finished + binding.chkDetailFinished.setOnClickListener { + game.finished = binding.chkDetailFinished.isChecked + gameRepo.update(game) + + Snackbar.make(binding.root, "Saved!", Snackbar.LENGTH_SHORT).show() + } + + requireContext().openFileInput(game.boxartFileName).use { + binding.imgDetailBoxArt.setImageBitmap(BitmapFactory.decodeStream(it)) + } + requireContext().openFileInput(game.snapshotFileName).use { + binding.imgDetailSnapshot.setImageBitmap(BitmapFactory.decodeStream(it)) + } + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/GameListAdapter.kt b/app/src/main/java/be/kuleuven/howlongtobeat/GameListAdapter.kt index 50d99c5..f3a4d39 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/GameListAdapter.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/GameListAdapter.kt @@ -5,27 +5,36 @@ import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.TextView +import androidx.fragment.app.findFragment import androidx.recyclerview.widget.RecyclerView import be.kuleuven.howlongtobeat.model.Game class GameListAdapter(val items: List) : RecyclerView.Adapter() { + private lateinit var parentFragment: GameListFragment + inner class GameListViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameListViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_game, parent, false) + parentFragment = parent.findFragment() return GameListViewHolder(view) } override fun onBindViewHolder(holder: GameListViewHolder, position: Int) { - val currentTodoItem = items[position] + val game = items[position] holder.itemView.apply { - val checkBoxTodo = findViewById(R.id.chkTodoDone) - findViewById(R.id.txtTodoTitle).text = currentTodoItem.title + setOnLongClickListener { + parentFragment.selectGame(game) + true + } - checkBoxTodo.isChecked = currentTodoItem.isDone - checkBoxTodo.setOnClickListener { - currentTodoItem.isDone = checkBoxTodo.isChecked + val chkFinished = findViewById(R.id.chkGameFinished) + findViewById(R.id.txtTodoTitle).text = game.title + + chkFinished.isChecked = game.finished + chkFinished.setOnClickListener { + game.finished = chkFinished.isChecked } } } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt index 8df3b12..9a943bd 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/GameListFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager @@ -41,32 +42,22 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) { } private fun loadGames() { + gameList.clear() gameList.addAll(gameRepository.load()) if(!gameList.any()) { gameList.add(Game.NONE_YET) } } - /* - fun onHltbGamesRetrieved(games: List) { - gameList.clear() - gameList.addAll(games.map { Game("${it.title} (${it.howlong})", false) }) - adapter.notifyDataSetChanged() + fun selectGame(game: Game) { + findNavController().navigate(R.id.action_gameListFragment_to_gameDetailFragment, bundleOf(Game.GAME_ID to game.id.toString())) } fun clearAllItems() { gameList.clear() + gameList.add(Game.NONE_YET) + adapter.notifyDataSetChanged() } - fun clearLatestItem() { - if(gameList.size >= 1) { - gameList.removeAt(gameList.size - 1) - adapter.notifyItemRemoved(gameList.size - 1) - } - } - - */ - - } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt index 06ce954..fe61476 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsAdapter.kt @@ -1,6 +1,6 @@ package be.kuleuven.howlongtobeat -import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,8 +31,11 @@ class HltbResultsAdapter(private val items: List) : Recycle val itm = items[position] holder.itemView.apply { - setOnClickListener { - parentFragment.addResultToGameLibrary(itm) + var art = BitmapFactory.decodeResource(resources, R.drawable.emptygb) + + setOnLongClickListener { + parentFragment.addResultToGameLibrary(itm, art) + true } findViewById(R.id.txtHltbItemResult).text = itm.toString() @@ -40,7 +43,6 @@ class HltbResultsAdapter(private val items: List) : Recycle if(itm.hasBoxart()) { MainScope().launch{ - var art: Bitmap? = null withContext(Dispatchers.IO) { art = itm.boxartUrl().downloadAsImage() } @@ -49,7 +51,7 @@ class HltbResultsAdapter(private val items: List) : Recycle } } } else { - boxArtView.visibility = View.INVISIBLE + boxArtView.setImageBitmap(art) } } } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt index ffbf936..c11bd53 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/HltbResultsFragment.kt @@ -1,5 +1,8 @@ package be.kuleuven.howlongtobeat +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,8 +15,10 @@ 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 +import java.io.FileInputStream class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) { + private lateinit var cachedSnapshotPath: Uri private lateinit var binding: FragmentHltbresultsBinding private lateinit var adapter: HltbResultsAdapter private lateinit var gameRepository: GameRepository @@ -26,6 +31,7 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) { binding = FragmentHltbresultsBinding.inflate(layoutInflater) val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List + cachedSnapshotPath = Uri.parse(arguments?.getSerializable(HowLongToBeatResult.SNAPSHOT_URI) as String) gameRepository = GameRepository.defaultImpl(requireContext()) @@ -36,9 +42,22 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) { return binding.root } - fun addResultToGameLibrary(hltbResult: HowLongToBeatResult) { - gameRepository.save(Game(hltbResult)) + fun addResultToGameLibrary(hltbResult: HowLongToBeatResult, downloadedBoxart: Bitmap) { + val game = Game(hltbResult) + downloadedBoxart.save(game.boxartFileName, requireContext()) + copyCachedSnapshotTo(game.snapshotFileName) + gameRepository.save(game) + Snackbar.make(requireView(), "Added ${hltbResult.title} to library!", Snackbar.LENGTH_LONG).show() findNavController().navigate(R.id.action_hltbResultsFragment_to_gameListFragment) } + + private fun copyCachedSnapshotTo(destination: String) { + val inStream = requireContext().contentResolver.openInputStream(cachedSnapshotPath) as FileInputStream + inStream.use { + requireContext().openFileOutput(destination, Context.MODE_PRIVATE).use { outStream -> + inStream.channel.transferTo(0, inStream.channel.size(), outStream.channel) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt b/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt index 724474d..965f38f 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/LoadingFragment.kt @@ -14,8 +14,10 @@ import androidx.core.content.PermissionChecker import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import be.kuleuven.howlongtobeat.cartridges.Cartridge +import be.kuleuven.howlongtobeat.cartridges.CartridgeFinderViaDuckDuckGo import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository +import be.kuleuven.howlongtobeat.cartridges.CartridgesRepositoryGekkioFi +import be.kuleuven.howlongtobeat.cartridges.findFirstCartridgeForRepos import be.kuleuven.howlongtobeat.databinding.FragmentLoadingBinding import be.kuleuven.howlongtobeat.google.GoogleVisionClient import be.kuleuven.howlongtobeat.hltb.HLTBClient @@ -28,7 +30,7 @@ import java.io.File class LoadingFragment : Fragment(R.layout.fragment_loading) { private lateinit var hltbClient: HLTBClient - private lateinit var cartRepo: CartridgesRepository + private lateinit var cartRepos: List private lateinit var imageRecognizer: ImageRecognizer private lateinit var cameraPermissionActivityResult: ActivityResultLauncher @@ -46,7 +48,11 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { binding = FragmentLoadingBinding.inflate(layoutInflater) main = activity as MainActivity - cartRepo = CartridgesRepository.fromAsset(main.applicationContext) + // If we fail to find info in the first repo, it falls back to the second one: a (scraped) DuckDuckGo search. + cartRepos = listOf( + CartridgesRepositoryGekkioFi.fromAsset(main.applicationContext), + CartridgeFinderViaDuckDuckGo(main.applicationContext) + ) imageRecognizer = GoogleVisionClient() hltbClient = HLTBClient(main.applicationContext) @@ -55,11 +61,27 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { binding.btnRetryAfterLoading.setOnClickListener { tryToMakeCameraSnap() } - tryToMakeCameraSnap() return binding.root } + override fun onViewStateRestored(savedInstanceState: Bundle?) { + val inProgress = savedInstanceState?.getBoolean("inprogress") ?: false + if(!inProgress) { + // Don't do this in onCreateView, things go awry if you rotate the smartphone! + tryToMakeCameraSnap() + } + + super.onViewStateRestored(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.run { + putBoolean("inprogress", snapshot != null) + } + super.onSaveInstanceState(outState) + } + private fun cameraSnapTaken(succeeded: Boolean) { if(!succeeded || snapshot == null) { errorInProgress("Photo could not be saved, try again?") @@ -67,42 +89,40 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { } progress("Scaling image for upload...") - val bitmap = snapshot!!.toBitmap(main).scaleToWidth(1600) + val bitmap = snapshot!!.toBitmap(requireContentResolver()).scaleToWidth(1600) MainScope().launch{ - findGameBasedOnCameraSnap(bitmap) + try { + findGameBasedOnCameraSnap(bitmap) + } catch (errorDuringFind: UnableToFindGameException) { + errorInProgress("${errorDuringFind.message}\nRetry?") + } } } private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) { - /* + var picToAnalyze = pic + // Uncomment this line if you want to stub out camera pictures + // picToAnalyze = BitmapFactory.decodeResource(resources, R.drawable.sml2) + progress("Recognizing game cart from picture...") - val cartCode = imageRecognizer.recognizeCartCode(pic) + val cartCode = imageRecognizer.recognizeCartCode(picToAnalyze) + ?: throw UnableToFindGameException("No cart code in your pic found") - if (cartCode == null) { - errorInProgress("No cart code in your pic found. Retry?") - return - } + progress("Found cart code $cartCode\nLooking in DBs for matching game...") + val foundCart = findFirstCartridgeForRepos(cartCode, cartRepos) + ?: throw UnableToFindGameException("$cartCode is an unknown game cart.") - progress("Found cart code $cartCode, looking in DB...") - val foundCart = cartRepo.find(cartCode) + progress("Valid cart code: $cartCode\n Looking in HLTB for ${foundCart.title}...") + val hltbResults = hltbClient.find(foundCart) + ?: throw UnableToFindGameException("HLTB does not know ${foundCart.title}") - if (foundCart == Cartridge.UNKNOWN_CART) { - 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 ${foundCart.code}", Snackbar.LENGTH_LONG).show() - - // TODO wat als geen hltb results gevonden? - val bundle = bundleOf(HowLongToBeatResult.RESULT to it) - findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle) - } + Snackbar.make(requireView(), "Found ${hltbResults.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show() + val bundle = bundleOf( + HowLongToBeatResult.RESULT to hltbResults, + HowLongToBeatResult.SNAPSHOT_URI to snapshot.toString() + ) + findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle) } private fun cameraPermissionAcceptedOrDenied(succeeded: Boolean) { @@ -130,6 +150,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { createNewFile() deleteOnExit() } + snapshot = FileProvider.getUriForFile(main.applicationContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile) } @@ -144,6 +165,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { } private fun errorInProgress(msg: String) { + snapshot = null progress(msg) binding.indeterminateBar.visibility = View.GONE Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/MainActivity.kt b/app/src/main/java/be/kuleuven/howlongtobeat/MainActivity.kt index eeccd4c..b19af77 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/MainActivity.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/MainActivity.kt @@ -1,21 +1,32 @@ package be.kuleuven.howlongtobeat +import android.app.AlertDialog +import android.content.DialogInterface import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GravityCompat +import androidx.navigation.fragment.NavHostFragment import be.kuleuven.howlongtobeat.databinding.ActivityMainBinding +import be.kuleuven.howlongtobeat.model.GameRepository +import kotlin.math.roundToInt class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var menuBarToggle: ActionBarDrawerToggle + private lateinit var gameRepository: GameRepository + + private val navHostFragment: NavHostFragment + get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) + gameRepository = GameRepository.defaultImpl(applicationContext) setupMenuDrawer() setContentView(binding.root) @@ -30,24 +41,56 @@ class MainActivity : AppCompatActivity() { binding.navView.setNavigationItemSelectedListener { when (it.itemId) { - R.id.mnuClear -> clearAllItems() - R.id.mnuClearLatest -> clearLatestItem() - R.id.mnuReset -> resetItems() + R.id.mnuClear -> tryToClearAllItems() + R.id.mnuStats -> showStats() } true } } private fun clearAllItems() { - //todoFragment.clearAllItems() + gameRepository.overwrite(listOf()) + val currentActiveFragment = navHostFragment.childFragmentManager.fragments[0] as GameListFragment + currentActiveFragment.clearAllItems() + binding.drawerLayout.closeDrawer(GravityCompat.START) } - private fun clearLatestItem() { - //todoFragment.clearLatestItem() + private fun tryToClearAllItems() { + AlertDialog.Builder(this) + .setTitle("Delete all games from the DB") + .setMessage("Are you sure?") + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton("Yup") { dialog, _ -> + clearAllItems() + dialog.dismiss() + } + .setNegativeButton("Nah") { dialog, _ -> + close(dialog) + } + .create() + .show() } - private fun resetItems() { - //todoFragment.resetItems() + private fun close(dialog: DialogInterface) { + dialog.dismiss() + binding.drawerLayout.closeDrawer(GravityCompat.START) + } + + private fun showStats() { + val allGames = gameRepository.load() + val hoursToBeat = allGames.filter { !it.finished }.map { it.howLongToBeat }.sum() + val hoursAlreadyBeat = allGames.filter { it.finished }.map { it.howLongToBeat }.sum() + val percCompleted = if(hoursAlreadyBeat > 0) ((hoursToBeat / hoursAlreadyBeat) * 100).roundToInt() else 0 + + AlertDialog.Builder(this) + .setTitle("Game library stats") + .setMessage("Total games: ${allGames.size}\nHours still to beat: ${hoursToBeat}\nHours already beat: ${hoursAlreadyBeat}\n\n$percCompleted% total completed.") + .setIcon(android.R.drawable.ic_dialog_info) + .setNeutralButton("Nice!") { dialog, _ -> + close(dialog) + } + .create() + .show() } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/UnableToFindGameException.kt b/app/src/main/java/be/kuleuven/howlongtobeat/UnableToFindGameException.kt new file mode 100644 index 0000000..88e6e50 --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/UnableToFindGameException.kt @@ -0,0 +1,3 @@ +package be.kuleuven.howlongtobeat + +class UnableToFindGameException(message: String?) : Throwable(message) \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/Cartridge.kt b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/Cartridge.kt index 7d894f6..3b1a295 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/Cartridge.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/Cartridge.kt @@ -5,8 +5,11 @@ data class Cartridge(val type: String, val name: String, val code: String) { companion object { val bracketRe = """\(.+\)""".toRegex() val UNKNOWN_CART = Cartridge("", "UNKNOWN CART", "DMG-???") + val KNOWN_CART_PREFIXES = listOf("DMG", "CGB") - fun isValid(code: String): Boolean = code.startsWith("DMG-") || code.startsWith("CGB-") + fun isValid(code: String): Boolean = + code != "" && !code.contains("\n") && + KNOWN_CART_PREFIXES.any { code.startsWith("$it-") } } val title = bracketRe.replace(name, "").replace("-", "").trim() diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGo.kt b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGo.kt new file mode 100644 index 0000000..4355e7a --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgeFinderViaDuckDuckGo.kt @@ -0,0 +1,33 @@ +package be.kuleuven.howlongtobeat.cartridges + +import android.content.Context +import com.android.volley.Response +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import kotlin.coroutines.suspendCoroutine + +class CartridgeFinderViaDuckDuckGo(private val context: Context) : CartridgesRepository { + + class CartridgeFinderViaDuckDuckGoRequest(query: String, responseListener: Response.Listener) : + StringRequest( + Method.GET, "https://html.duckduckgo.com/html/?q=${query}", responseListener, + Response.ErrorListener { + println("Something went wrong: ${it.message}") + }) { + + override fun getHeaders(): MutableMap { + return hashMapOf("User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:90.0) Gecko/20100101 Firefox/90.0") + } + } + + override suspend fun find(code: String?): Cartridge? = suspendCoroutine { cont -> + if(code == null) { + cont.resumeWith(Result.success(null)) + } else { + val queue = Volley.newRequestQueue(context) + queue.add(CartridgeFinderViaDuckDuckGoRequest(code) { html -> + cont.resumeWith(Result.success(DuckDuckGoResultParser.parse(html, code))) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepository.kt b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepository.kt index 4b6914a..7223f0d 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepository.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepository.kt @@ -1,38 +1,13 @@ package be.kuleuven.howlongtobeat.cartridges -import android.content.Context -import java.io.InputStream - -/** - * Reads cartridges.csv export from https://gbhwdb.gekkio.fi/cartridges/ - * Headers: type,name,title,slug,url,contributor,code,... - * E.g.: DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1, - */ -class CartridgesRepository(csvStream: InputStream) { - - companion object { - fun fromAsset(context: Context): CartridgesRepository = - CartridgesRepository(context.assets.open("cartridges.csv")) - } - - val cartridges: List = - csvStream.bufferedReader().use { - it.readLines() - }.filter { - !it.startsWith("type,name,title,") - }.map { - val data = it.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)".toRegex()) - Cartridge(data[0].replace("\"", "").trim(), data[1].replace("\"", "").trim(), data[6].replace("\"", "").trim()) - }.filter { it.code != "" } - .toList() - - fun find(code: String?): Cartridge { - if(code == null) return Cartridge.UNKNOWN_CART - val possiblyFound = cartridges.filter { - it.code == code || it.code.contains(code) || code.contains(it.code) - }.firstOrNull() - - return possiblyFound ?: Cartridge.UNKNOWN_CART - } - +interface CartridgesRepository { + suspend fun find(code: String?): Cartridge? } + +suspend fun findFirstCartridgeForRepos(code: String?, repos: List): Cartridge? { + for(repo in repos) { + val result = repo.find(code) + if(result != null) return result + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFi.kt b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFi.kt new file mode 100644 index 0000000..206575a --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFi.kt @@ -0,0 +1,41 @@ +package be.kuleuven.howlongtobeat.cartridges + +import android.content.Context +import java.io.InputStream +import kotlin.coroutines.suspendCoroutine + +/** + * Reads cartridges.csv export from https://gbhwdb.gekkio.fi/cartridges/ + * Headers: type,name,title,slug,url,contributor,code,... + * E.g.: DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1, + */ +class CartridgesRepositoryGekkioFi(csvStream: InputStream) : CartridgesRepository { + + companion object { + fun fromAsset(context: Context): CartridgesRepositoryGekkioFi = + CartridgesRepositoryGekkioFi(context.assets.open("cartridges.csv")) + } + + val cartridges: List = + csvStream.bufferedReader().use { + it.readLines() + }.filter { + !it.startsWith("type,name,title,") + }.map { + val data = it.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)".toRegex()) + Cartridge(data[0].replace("\"", "").trim(), data[1].replace("\"", "").trim(), data[6].replace("\"", "").trim()) + }.filter { it.code != "" } + .toList() + + override suspend fun find(code: String?): Cartridge? = suspendCoroutine { cont -> + if(code == null) { + cont.resumeWith(Result.success(null)) + } else { + val possiblyFound = cartridges.filter { + it.code == code || it.code.contains(code) || code.contains(it.code) + }.firstOrNull() + cont.resumeWith(Result.success(possiblyFound)) + } + } + +} diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParser.kt b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParser.kt new file mode 100644 index 0000000..8bda028 --- /dev/null +++ b/app/src/main/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParser.kt @@ -0,0 +1,41 @@ +package be.kuleuven.howlongtobeat.cartridges + +object DuckDuckGoResultParser { + + private val resultMatcher = """(.+)""".toRegex() + private val specialCharsToRemove = listOf( + "|", + ".", + "-", + "/", + ",", + "!", + "Get information and compare prices of", + "for Game Boy", + "Release Information", + "Nintendo", + "Game Boy Advance", + "Game Boy color", + "Game Boy", + "GameBoy", + "Game ", + "GBC", + "GBA", + "VGDb", + "ebay" + ) + + fun parse(html: String, fromCode: String): Cartridge? { + // There are bound to be multiple results. Just fetch the first one as an educated guess + val matched = resultMatcher.find(html) ?: return null + + var match = matched.groupValues[1] + .lowercase() + .replace(fromCode.lowercase(), "") + + specialCharsToRemove.forEach { + match = match.replace(it.lowercase(), "") + } + return Cartridge("Unknown", match.trim(), fromCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HLTBClient.kt b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HLTBClient.kt index def7b73..ff28cf3 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HLTBClient.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HLTBClient.kt @@ -1,9 +1,11 @@ package be.kuleuven.howlongtobeat.hltb import android.content.Context +import be.kuleuven.howlongtobeat.cartridges.Cartridge import com.android.volley.Response import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley +import kotlin.coroutines.suspendCoroutine class HLTBClient(val context: Context) { @@ -41,10 +43,11 @@ class HLTBClient(val context: Context) { } } - fun find(query: String, onResponseFetched: (List) -> Unit) { + suspend fun find(cart: Cartridge): List? = suspendCoroutine { cont -> val queue = Volley.newRequestQueue(context) - val req = HLTBRequest(query) { - onResponseFetched(HowLongToBeatResultParser.parse(it)) + val req = HLTBRequest(cart.title) { + val hltbResults = HowLongToBeatResultParser.parse(it, cart) + cont.resumeWith(Result.success(hltbResults)) } queue.add(req) } diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt index 23e5e7f..69f8259 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResult.kt @@ -4,12 +4,13 @@ import kotlinx.serialization.Serializable import java.net.URL @Serializable -data class HowLongToBeatResult(val title: String, val howlong: Double, val boxart: String = "") : java.io.Serializable { +data class HowLongToBeatResult(val title: String, val cartCode: String, val howlong: Double, val boxartUrl: String = "") : java.io.Serializable { companion object { const val RESULT = "HowLongToBeatResult" + const val SNAPSHOT_URI = "SnapshotUri" } - fun hasBoxart(): Boolean = boxart.startsWith(HLTBClient.DOMAIN) - fun boxartUrl(): URL = URL(boxart) + fun hasBoxart(): Boolean = boxartUrl.startsWith(HLTBClient.DOMAIN) + fun boxartUrl(): URL = URL(boxartUrl) override fun toString(): String = "$title ($howlong hrs)" } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParser.kt b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParser.kt index 316c071..b5d8e75 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParser.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParser.kt @@ -1,12 +1,14 @@ package be.kuleuven.howlongtobeat.hltb +import be.kuleuven.howlongtobeat.cartridges.Cartridge + object HowLongToBeatResultParser { private val titleMatcher = """(.+) Hours""".toRegex() private val boxArtMatcher = """.+ { + fun parse(html: String, sourceCart: Cartridge): List? { val result = arrayListOf() val rows = html.split("\n") for(i in 0..rows.size - 1) { @@ -16,11 +18,11 @@ object HowLongToBeatResultParser { val hour = parseHoursFromRow(i, rows) val boxart = parseBoxArtFromRow(i, rows) - result.add(HowLongToBeatResult(title, hour, boxart)) + result.add(HowLongToBeatResult(title, sourceCart.code, hour, boxart)) } } - return result + return if(result.any()) result else null } private fun parseBoxArtFromRow(row: Int, rows: List): String { diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt index d6b4818..1533fe9 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/Game.kt @@ -8,19 +8,25 @@ import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult @Entity data class Game( @ColumnInfo(name = "title") val title: String, - @ColumnInfo(name = "is_done") var isDone: Boolean = false, + @ColumnInfo(name = "code") val cartCode: String, + @ColumnInfo(name = "hltb") val howLongToBeat: Double, + @ColumnInfo(name = "finished") var finished: Boolean = false, @PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable { - constructor(result: HowLongToBeatResult) : this(result.title) + constructor(result: HowLongToBeatResult) : this(result.title, result.cartCode, result.howlong) - // TODO more columns (platform? hours, paths of images?) + val boxartFileName + get() = "box-${cartCode}.jpg" + val snapshotFileName + get() = "snap-${cartCode}.jpg" - fun check() { - isDone = true + fun finish() { + finished = true } companion object { - val NONE_YET = Game("No entries yet, add one!") + val NONE_YET = Game("No entries yet, add one!", "", 0.0) + val GAME_ID = "GameId" } } \ No newline at end of file diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt index b5e6620..3a6490e 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/GameRepository.kt @@ -15,6 +15,10 @@ interface GameRepository { fun load(): List + fun update(game: Game) + + fun find(id: Int): Game + fun save(game: Game) fun overwrite(items: List) diff --git a/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt b/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt index 905edeb..c2a39e7 100644 --- a/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt +++ b/app/src/main/java/be/kuleuven/howlongtobeat/model/room/GameRepositoryRoomImpl.kt @@ -19,6 +19,9 @@ class GameRepositoryRoomImpl(appContext: Context) : } override fun load(): List = dao.query() + override fun update(game: Game) = dao.update(listOf(game)) + + override fun find(id: Int): Game = load().single { it.id == id } override fun save(game: Game) { db.runInTransaction { diff --git a/app/src/main/res/drawable/emptygb.jpg b/app/src/main/res/drawable/emptygb.jpg new file mode 100644 index 0000000..d996718 Binary files /dev/null and b/app/src/main/res/drawable/emptygb.jpg differ diff --git a/app/src/main/res/drawable/sml2.jpg b/app/src/main/res/drawable/sml2.jpg new file mode 100644 index 0000000..b267657 Binary files /dev/null and b/app/src/main/res/drawable/sml2.jpg differ diff --git a/app/src/main/res/drawable/supermarioland2.jpg b/app/src/main/res/drawable/supermarioland2.jpg deleted file mode 100644 index 196d184..0000000 Binary files a/app/src/main/res/drawable/supermarioland2.jpg and /dev/null differ diff --git a/app/src/main/res/layout/fragment_gamedetail.xml b/app/src/main/res/layout/fragment_gamedetail.xml new file mode 100644 index 0000000..cb5d91f --- /dev/null +++ b/app/src/main/res/layout/fragment_gamedetail.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_game.xml b/app/src/main/res/layout/item_game.xml index 1c65be6..ce48609 100644 --- a/app/src/main/res/layout/item_game.xml +++ b/app/src/main/res/layout/item_game.xml @@ -13,12 +13,12 @@ android:text="Some Title" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/chkTodoDone" + app:layout_constraintEnd_toStartOf="@+id/chkGameFinished" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml index e498c5d..91c92aa 100644 --- a/app/src/main/res/menu/nav_menu.xml +++ b/app/src/main/res/menu/nav_menu.xml @@ -2,12 +2,9 @@ + android:title="Remove All Games" /> - + android:id="@+id/mnuStats" + android:title="Statistics" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index e312296..00b1777 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -11,6 +11,9 @@ + + \ No newline at end of file diff --git a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgeTest.kt b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgeTest.kt index 4ac2e5a..5096724 100644 --- a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgeTest.kt +++ b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgeTest.kt @@ -1,10 +1,28 @@ package be.kuleuven.howlongtobeat.cartridges -import junit.framework.Assert.assertEquals + +import junit.framework.TestCase.* import org.junit.Test class CartridgeTest { + @Test + fun isValid_OnlyCartridgeCodeItself_IsValid() { + assertTrue(Cartridge.isValid("DMG-MQ-EUR")) + assertTrue(Cartridge.isValid("DMG-MQ-USA-1")) + assertTrue(Cartridge.isValid("CGB-ABC-DEF-WHATEVER")) + } + + @Test + fun isValid_Empty_IsNotValid() { + assertFalse(Cartridge.isValid("")) + } + + @Test + fun isValid_CatridgeCodeWithOtherJunkOnNewLine_IsNotValid() { + assertFalse(Cartridge.isValid("DMG-MQ-EUR\nMADE IN JAPAN")) + } + @Test fun titleReplacesIrrelevantDetailsFromName() { val cart = Cartridge("type", "name (irrelevant details please remove thxxx)", "DMG-whatever") diff --git a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFiTest.kt b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFiTest.kt new file mode 100644 index 0000000..fd76774 --- /dev/null +++ b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryGekkioFiTest.kt @@ -0,0 +1,64 @@ +package be.kuleuven.howlongtobeat.car + +import be.kuleuven.howlongtobeat.cartridges.CartridgesRepositoryGekkioFi +import junit.framework.Assert.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Executors + + +class CartridgesRepositoryGekkioFiTest { + + private lateinit var repo: CartridgesRepositoryGekkioFi + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + @Before + fun setUp() { + repo = javaClass.getResource("/cartridges.csv").openStream().use { + CartridgesRepositoryGekkioFi(it) + } + Dispatchers.setMain(dispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + dispatcher.close() + } + + @Test + fun noSingleCodeShouldBeEmpty() { + repo.cartridges.forEach { + assertTrue("Code of ${it.title} should be filled in but is '${it.code}'", it.code.length > 4) + } + } + + @Test + fun findReturnsCartridgeObjectOfCode() = runBlocking { + launch(Dispatchers.Main) { + val smbDeluxe = repo.find("CGB-AHYE-USA") + assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe")) + } + println("done") + } + + @Test + fun readsWholeCsvFileAsListOfCartridges() { + assertFalse(repo.cartridges.isEmpty()) + val smbDeluxe = repo.cartridges.find { + it.code == "CGB-AHYE-USA" + } + assertTrue(smbDeluxe != null) + assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe")) + assertEquals("Super Mario Bros. Deluxe", smbDeluxe.title) + } + +} \ No newline at end of file diff --git a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryTest.kt b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryTest.kt deleted file mode 100644 index df36d56..0000000 --- a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/CartridgesRepositoryTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package be.kuleuven.howlongtobeat.car - -import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository -import junit.framework.Assert.* -import org.junit.Before -import org.junit.Test - - - -class CartridgesRepositoryTest { - - private lateinit var repo: CartridgesRepository - - @Before - fun setUp() { - repo = javaClass.getResource("/cartridges.csv").openStream().use { - CartridgesRepository(it) - } - } - - @Test - fun noSingleCodeShouldBeEmpty() { - repo.cartridges.forEach { - assertTrue("Code of ${it.title} should be filled in but is '${it.code}'", it.code.length > 4) - } - } - - @Test - fun readsWholeCsvFileAsListOfCartridges() { - assertFalse(repo.cartridges.isEmpty()) - val smbDeluxe = repo.cartridges.find { - it.code == "CGB-AHYE-USA" - } - assertTrue(smbDeluxe != null) - assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe")) - assertEquals("Super Mario Bros. Deluxe", smbDeluxe.title) - } - -} \ No newline at end of file diff --git a/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParserTest.kt b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParserTest.kt new file mode 100644 index 0000000..3523ba2 --- /dev/null +++ b/app/src/test/java/be/kuleuven/howlongtobeat/cartridges/DuckDuckGoResultParserTest.kt @@ -0,0 +1,994 @@ +package be.kuleuven.howlongtobeat.cartridges + +import junit.framework.TestCase.assertEquals +import org.junit.Test +import junit.framework.TestCase.assertNull as assertNull1 + +class DuckDuckGoResultParserTest { + + @Test + fun parse_marioGolfCodeSample() { + val html = """ + + + + + + + + + + + + + cgb-awxp-eur-1 at DuckDuckGo + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + + +
+
+ +
+ + + +
+
+ +
+ + + + + + + + """.trimIndent() + + val cart = DuckDuckGoResultParser.parse(html, "cgb-awxp-eur-1") + assertEquals("mario golf", cart?.title) + } + + @Test + fun parse_NoResultsFound_returnsNull() { + val result = DuckDuckGoResultParser.parse("whooptie-doo", "some-code") + assertNull1(result) + } + + @Test + fun parse_ResultWithStuffBetweenBrackets_removesThose() { + val html = """ + My Little Pony (USA Release)! + """.trimIndent() + + val cart = DuckDuckGoResultParser.parse(html, "some-code") + assertEquals("my little pony", cart?.title) + } + + @Test + fun parse_ResultWithCodeAgainInResult_removesRedundantCode() { + val html = """ + My Little Pony (USA Release) DMG-PONY-007 ebay + """.trimIndent() + + val cart = DuckDuckGoResultParser.parse(html, "DMG-PONY-007") + assertEquals("my little pony", cart?.title) + assertEquals("DMG-PONY-007", cart?.code) + } + +} \ No newline at end of file diff --git a/app/src/test/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParserTest.kt b/app/src/test/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParserTest.kt index 4bbd06f..b058577 100644 --- a/app/src/test/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParserTest.kt +++ b/app/src/test/java/be/kuleuven/howlongtobeat/hltb/HowLongToBeatResultParserTest.kt @@ -1,14 +1,16 @@ package be.kuleuven.howlongtobeat.hltb +import be.kuleuven.howlongtobeat.cartridges.Cartridge +import junit.framework.TestCase.assertNull import org.junit.Assert.assertEquals import org.junit.Test class HowLongToBeatResultParserTest { @Test - fun parseWithEmptyStringReturnsEmptyList() { - val result = HowLongToBeatResultParser.parse("") - assertEquals(0, result.size) + fun parseWithEmptyStringReturnsNull() { + val result = HowLongToBeatResultParser.parse("", Cartridge.UNKNOWN_CART) + assertNull(result) } @Test @@ -66,16 +68,22 @@ I/System.out:
Ma Box Art """.trimIndent() - val result = HowLongToBeatResultParser.parse(html) + val result = HowLongToBeatResultParser.parse(html, Cartridge("DMG", "name", "DMG-CODE1"))!! val smland = result[0] val sm3dland = result[1] assertEquals(3, result.size) + assertEquals("Super Mario Land", smland.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("https://howlongtobeat.com/games/250px-Supermariolandboxart.jpg", smland.boxartUrl) + assertEquals("https://howlongtobeat.com/games/250px-Super-Mario-3D-Land-Logo.jpg", sm3dland.boxartUrl) + assertEquals(1.0, smland.howlong, 0.0) assertEquals(6.5, sm3dland.howlong, 0.0) + + assertEquals("DMG-CODE1", smland.cartCode) + assertEquals("DMG-CODE1", sm3dland.cartCode) } } \ No newline at end of file