From 934d174ffd62929fb7d9353d4c2fd05e84ecc0b4 Mon Sep 17 00:00:00 2001 From: Kyle Corry Date: Wed, 10 Jul 2024 18:21:58 -0400 Subject: [PATCH] Add ability to import path from CSV --- .../trail_sense/shared/io/CsvIOService.kt | 11 +++- .../tools/packs/infrastructure/IPackRepo.kt | 2 +- .../infrastructure/LighterPackIOService.kt | 50 +++++++++++----- .../tools/packs/infrastructure/PackRepo.kt | 7 ++- .../tools/packs/ui/PackListFragment.kt | 6 ++ .../ui/commands/ExportPackingListCommand.kt | 7 +-- .../ui/commands/ImportPackingListCommand.kt | 59 +++++++++++++++++++ .../main/res/layout/fragment_pack_list.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ImportPackingListCommand.kt diff --git a/app/src/main/java/com/kylecorry/trail_sense/shared/io/CsvIOService.kt b/app/src/main/java/com/kylecorry/trail_sense/shared/io/CsvIOService.kt index d1c309035..d0fbb51f7 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/shared/io/CsvIOService.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/shared/io/CsvIOService.kt @@ -13,7 +13,16 @@ class CsvIOService(private val uriPicker: UriPicker, private val uriService: Uri } override suspend fun import(): List>? = onIO { - val uri = uriPicker.open(listOf("text/csv")) ?: return@onIO null + val uri = + uriPicker.open( + listOf( + "text/csv", + "application/csv", + "text/comma-separated-values", + "text/plain" + ) + ) + ?: return@onIO null val stream = uriService.inputStream(uri) ?: return@onIO null CSVConvert.parse(stream.readText()) } diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/IPackRepo.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/IPackRepo.kt index ddcb4cd9f..0425ca489 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/IPackRepo.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/IPackRepo.kt @@ -23,7 +23,7 @@ interface IPackRepo { suspend fun addPack(pack: Pack): Long - suspend fun addItem(item: PackItem) + suspend fun addItem(item: PackItem): Long suspend fun deleteAll() diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/LighterPackIOService.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/LighterPackIOService.kt index 948818fd5..a70f0c5dc 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/LighterPackIOService.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/LighterPackIOService.kt @@ -1,5 +1,6 @@ package com.kylecorry.trail_sense.tools.packs.infrastructure +import android.content.Context import com.kylecorry.andromeda.fragments.AndromedaFragment import com.kylecorry.luna.text.toDoubleCompat import com.kylecorry.sol.units.Weight @@ -34,15 +35,16 @@ class LighterPackIOService(uriPicker: UriPicker, uriService: UriService) : private fun toCsv(pack: List): List> { val headers = listOf( - "item name", - "category", - "desc", - "qty", - "weight", - "unit", - "packed qty", - "desired qty" + HEADER_ITEM_NAME, + HEADER_CATEGORY, + HEADER_DESCRIPTION, + HEADER_QUANTITY, + HEADER_WEIGHT_AMOUNT, + HEADER_WEIGHT_UNIT, + HEADER_PACKED_QUANTITY, + HEADER_DESIRED_QUANTITY ) + val items = pack.map { listOf( it.name, @@ -63,13 +65,23 @@ class LighterPackIOService(uriPicker: UriPicker, uriService: UriService) : return null } + val headerRow = data.first().map { it.lowercase() } + val nameIdx = headerRow.indexOf(HEADER_ITEM_NAME.lowercase()) + val categoryIdx = headerRow.indexOf(HEADER_CATEGORY.lowercase()) + val weightAmountIdx = headerRow.indexOf(HEADER_WEIGHT_AMOUNT.lowercase()) + val weightUnitIdx = headerRow.indexOf(HEADER_WEIGHT_UNIT.lowercase()) + val qtyIdx = headerRow.indexOf(HEADER_QUANTITY.lowercase()) + val packedQtyIdx = headerRow.indexOf(HEADER_PACKED_QUANTITY.lowercase()) + val desiredQtyIdx = headerRow.indexOf(HEADER_DESIRED_QUANTITY.lowercase()) + return data.drop(1).map { it -> - val name = it.getOrNull(0) ?: "" - val category = parseCategoryString(it.getOrNull(1) ?: "") - val weight = it.getOrNull(4)?.toFloatOrNull() - val unit = it.getOrNull(5)?.let { parseWeightUnit(it) } ?: WeightUnits.Grams - val packedQty = it.getOrNull(6)?.toDoubleCompat() ?: 0.0 - val desiredQty = it.getOrNull(7)?.toDoubleCompat() ?: 0.0 + val name = it.getOrNull(nameIdx) ?: "" + val category = parseCategoryString(it.getOrNull(categoryIdx) ?: "") + val weight = it.getOrNull(weightAmountIdx)?.toFloatOrNull() + val unit = it.getOrNull(weightUnitIdx)?.let { parseWeightUnit(it) } ?: WeightUnits.Grams + val packedQty = it.getOrNull(packedQtyIdx)?.toDoubleCompat() ?: 0.0 + val desiredQty = it.getOrNull(desiredQtyIdx)?.toDoubleCompat() ?: it.getOrNull(qtyIdx) + ?.toDoubleCompat() ?: 0.0 PackItem( 0, 0, @@ -108,6 +120,16 @@ class LighterPackIOService(uriPicker: UriPicker, uriService: UriService) : } companion object { + + private const val HEADER_ITEM_NAME = "item name" + private const val HEADER_CATEGORY = "category" + private const val HEADER_DESCRIPTION = "desc" + private const val HEADER_QUANTITY = "qty" + private const val HEADER_WEIGHT_AMOUNT = "weight" + private const val HEADER_WEIGHT_UNIT = "unit" + private const val HEADER_PACKED_QUANTITY = "packed qty" + private const val HEADER_DESIRED_QUANTITY = "desired qty" + fun create(fragment: AndromedaFragment): LighterPackIOService { return LighterPackIOService( FragmentUriPicker(fragment), diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/PackRepo.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/PackRepo.kt index 9e49e9a67..cc2ccc458 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/PackRepo.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/infrastructure/PackRepo.kt @@ -49,8 +49,8 @@ class PackRepo private constructor(context: Context) : IPackRepo { packDao.delete(mapper.mapToPackEntity(pack)) } - override suspend fun addPack(pack: Pack): Long { - return if (pack.id == 0L) { + override suspend fun addPack(pack: Pack): Long = onIO { + if (pack.id == 0L) { packDao.insert(mapper.mapToPackEntity(pack)) } else { packDao.update(mapper.mapToPackEntity(pack)) @@ -73,9 +73,10 @@ class PackRepo private constructor(context: Context) : IPackRepo { return newId } - override suspend fun addItem(item: PackItem) { + override suspend fun addItem(item: PackItem) = onIO { if (item.id != 0L) { inventoryItemDao.update(mapper.mapToItemEntity(item)) + item.id } else { inventoryItemDao.insert(mapper.mapToItemEntity(item)) } diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/PackListFragment.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/PackListFragment.kt index 8d6dc12c3..ff1587563 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/PackListFragment.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/PackListFragment.kt @@ -18,6 +18,7 @@ import com.kylecorry.trail_sense.databinding.FragmentPackListBinding import com.kylecorry.trail_sense.tools.packs.domain.Pack import com.kylecorry.trail_sense.tools.packs.infrastructure.PackRepo import com.kylecorry.trail_sense.tools.packs.ui.commands.ExportPackingListCommand +import com.kylecorry.trail_sense.tools.packs.ui.commands.ImportPackingListCommand import com.kylecorry.trail_sense.tools.packs.ui.mappers.PackAction import com.kylecorry.trail_sense.tools.packs.ui.mappers.PackListItemMapper import kotlinx.coroutines.Dispatchers @@ -60,6 +61,11 @@ class PackListFragment : BoundFragment() { } binding.addBtn.setOnClickListener { createPack() } + + // TODO: Temporary + binding.packListTitle.rightButton.setOnClickListener { + ImportPackingListCommand(this).execute() + } } private fun renamePack(pack: Pack) { diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ExportPackingListCommand.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ExportPackingListCommand.kt index 6afeb8709..4eda68c13 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ExportPackingListCommand.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ExportPackingListCommand.kt @@ -8,8 +8,6 @@ import com.kylecorry.andromeda.fragments.inBackground import com.kylecorry.luna.text.slugify import com.kylecorry.trail_sense.R import com.kylecorry.trail_sense.shared.commands.generic.Command -import com.kylecorry.trail_sense.shared.io.ExternalUriService -import com.kylecorry.trail_sense.shared.io.FragmentUriPicker import com.kylecorry.trail_sense.tools.packs.domain.Pack import com.kylecorry.trail_sense.tools.packs.domain.PackItem import com.kylecorry.trail_sense.tools.packs.infrastructure.LighterPackIOService @@ -17,10 +15,7 @@ import com.kylecorry.trail_sense.tools.packs.infrastructure.PackRepo class ExportPackingListCommand(private val fragment: AndromedaFragment) : Command { - private val exportService = LighterPackIOService( - FragmentUriPicker(fragment), - ExternalUriService(fragment.requireContext()) - ) + private val exportService = LighterPackIOService.create(fragment) private val repo = PackRepo.getInstance(fragment.requireContext()) diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ImportPackingListCommand.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ImportPackingListCommand.kt new file mode 100644 index 000000000..ffe8f9a16 --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/packs/ui/commands/ImportPackingListCommand.kt @@ -0,0 +1,59 @@ +package com.kylecorry.trail_sense.tools.packs.ui.commands + +import androidx.core.os.bundleOf +import androidx.navigation.fragment.findNavController +import com.kylecorry.andromeda.alerts.Alerts +import com.kylecorry.andromeda.alerts.toast +import com.kylecorry.andromeda.core.coroutines.BackgroundMinimumState +import com.kylecorry.andromeda.fragments.AndromedaFragment +import com.kylecorry.andromeda.fragments.inBackground +import com.kylecorry.andromeda.pickers.CoroutinePickers +import com.kylecorry.trail_sense.R +import com.kylecorry.trail_sense.shared.commands.Command +import com.kylecorry.trail_sense.shared.navigateWithAnimation +import com.kylecorry.trail_sense.tools.packs.domain.Pack +import com.kylecorry.trail_sense.tools.packs.domain.PackItem +import com.kylecorry.trail_sense.tools.packs.infrastructure.LighterPackIOService +import com.kylecorry.trail_sense.tools.packs.infrastructure.PackRepo + +class ImportPackingListCommand(private val fragment: AndromedaFragment) : Command { + + private val importService = LighterPackIOService.create(fragment) + private val repo = PackRepo.getInstance(fragment.requireContext()) + + override fun execute() { + fragment.inBackground(BackgroundMinimumState.Created) { + var items: List? = null + Alerts.withLoading(fragment.requireContext(), fragment.getString(R.string.loading)) { + items = importService.import() + } + + // If items are null or empty, show a toast and return + if (items.isNullOrEmpty()) { + fragment.toast(fragment.getString(R.string.no_items_found)) + return@inBackground + } + + // Ask the user for the pack name + // TODO: Default to the file name + val name = CoroutinePickers.text( + fragment.requireContext(), + fragment.getString(R.string.name) + ) ?: return@inBackground + + // Create the pack + var packId = 0L + Alerts.withLoading(fragment.requireContext(), fragment.getString(R.string.loading)) { + packId = repo.addPack(Pack(0, name)) + for (item in items!!) { + val newItem = item.copy(packId = packId) + repo.addItem(newItem) + } + } + + // Open the pack + val bundle = bundleOf("pack_id" to packId) + fragment.findNavController().navigateWithAnimation(R.id.packItemListFragment, bundle) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pack_list.xml b/app/src/main/res/layout/fragment_pack_list.xml index 97d571775..0a9dab5af 100644 --- a/app/src/main/res/layout/fragment_pack_list.xml +++ b/app/src/main/res/layout/fragment_pack_list.xml @@ -12,6 +12,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:rightButtonIcon="@drawable/ic_import_export" app:showSubtitle="false" app:title="@string/packing_lists" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb9b584c9..1bed65632 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1438,4 +1438,5 @@ Bottom navigation slot %d pref_weather_forecast_source Forecast algorithm + No items found \ No newline at end of file