diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt index afa259071..835471365 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt @@ -130,7 +130,7 @@ private fun TreeItemView( Column(modifier = Modifier.weight(1f)) { Text( - text = item.title.orEmpty(), + text = item.title.resolve(), style = MaterialTheme.typography.bodyMedium, ) if (item.value != null) { diff --git a/app/src/commonTest/kotlin/com/codebutler/farebot/test/ClipperTransitTest.kt b/app/src/commonTest/kotlin/com/codebutler/farebot/test/ClipperTransitTest.kt index 10b014b8a..4cbd97187 100644 --- a/app/src/commonTest/kotlin/com/codebutler/farebot/test/ClipperTransitTest.kt +++ b/app/src/commonTest/kotlin/com/codebutler/farebot/test/ClipperTransitTest.kt @@ -106,11 +106,10 @@ class ClipperTransitTest { fun testClipperTripModeDetection_BART() { // BART with transportCode 0x6f -> METRO val trip = - ClipperTrip - .builder() - .agency(0x04) // AGENCY_BART - .transportCode(0x6f) - .build() + ClipperTrip( + agency = 0x04, // AGENCY_BART + transportCode = 0x6f, + ) assertEquals(Trip.Mode.METRO, trip.mode) } @@ -118,11 +117,10 @@ class ClipperTransitTest { fun testClipperTripModeDetection_MuniLightRail() { // Muni with transportCode 0x62 -> TRAM (default) val trip = - ClipperTrip - .builder() - .agency(0x12) // AGENCY_MUNI - .transportCode(0x62) - .build() + ClipperTrip( + agency = 0x12, // AGENCY_MUNI + transportCode = 0x62, + ) assertEquals(Trip.Mode.TRAM, trip.mode) } @@ -130,11 +128,10 @@ class ClipperTransitTest { fun testClipperTripModeDetection_Caltrain() { // Caltrain with transportCode 0x62 -> TRAIN val trip = - ClipperTrip - .builder() - .agency(0x06) // AGENCY_CALTRAIN - .transportCode(0x62) - .build() + ClipperTrip( + agency = 0x06, // AGENCY_CALTRAIN + transportCode = 0x62, + ) assertEquals(Trip.Mode.TRAIN, trip.mode) } @@ -142,11 +139,10 @@ class ClipperTransitTest { fun testClipperTripModeDetection_SMART() { // SMART with transportCode 0x62 -> TRAIN val trip = - ClipperTrip - .builder() - .agency(0x0c) // AGENCY_SMART - .transportCode(0x62) - .build() + ClipperTrip( + agency = 0x0c, // AGENCY_SMART + transportCode = 0x62, + ) assertEquals(Trip.Mode.TRAIN, trip.mode) } @@ -154,11 +150,10 @@ class ClipperTransitTest { fun testClipperTripModeDetection_GGFerry() { // GG Ferry with transportCode 0x62 -> FERRY val trip = - ClipperTrip - .builder() - .agency(0x19) // AGENCY_GG_FERRY - .transportCode(0x62) - .build() + ClipperTrip( + agency = 0x19, // AGENCY_GG_FERRY + transportCode = 0x62, + ) assertEquals(Trip.Mode.FERRY, trip.mode) } @@ -166,44 +161,40 @@ class ClipperTransitTest { fun testClipperTripModeDetection_SFBayFerry() { // SF Bay Ferry with transportCode 0x62 -> FERRY val trip = - ClipperTrip - .builder() - .agency(0x1b) // AGENCY_SF_BAY_FERRY - .transportCode(0x62) - .build() + ClipperTrip( + agency = 0x1b, // AGENCY_SF_BAY_FERRY + transportCode = 0x62, + ) assertEquals(Trip.Mode.FERRY, trip.mode) } @Test fun testClipperTripModeDetection_Bus() { val trip = - ClipperTrip - .builder() - .agency(0x01) // AGENCY_ACTRAN - .transportCode(0x61) - .build() + ClipperTrip( + agency = 0x01, // AGENCY_ACTRAN + transportCode = 0x61, + ) assertEquals(Trip.Mode.BUS, trip.mode) } @Test fun testClipperTripModeDetection_Unknown() { val trip = - ClipperTrip - .builder() - .agency(0x04) // AGENCY_BART - .transportCode(0xFF) - .build() + ClipperTrip( + agency = 0x04, // AGENCY_BART + transportCode = 0xFF, + ) assertEquals(Trip.Mode.OTHER, trip.mode) } @Test fun testClipperTripFareCurrency() { val trip = - ClipperTrip - .builder() - .agency(0x04) - .fare(350) - .build() + ClipperTrip( + agency = 0x04, + fareValue = 350, + ) val fareStr = trip.fare?.formatCurrencyString() ?: "" // Should format as USD assertTrue( @@ -215,12 +206,11 @@ class ClipperTransitTest { @Test fun testClipperTripWithBalance() { val trip = - ClipperTrip - .builder() - .agency(0x04) - .fare(200) - .balance(1000) - .build() + ClipperTrip( + agency = 0x04, + fareValue = 200, + balance = 1000, + ) val updated = trip.withBalance(500) assertEquals(500L, updated.getBalance()) } @@ -297,58 +287,52 @@ class ClipperTransitTest { fun testVehicleNumbers() { // Test null vehicle number (0) val trip0 = - ClipperTrip - .builder() - .agency(0x12) // Muni - .vehicleNum(0) - .build() + ClipperTrip( + agency = 0x12, // Muni + vehicleNum = 0, + ) assertNull(trip0.vehicleID) // Test null vehicle number (0xffff) val tripFfff = - ClipperTrip - .builder() - .agency(0x12) - .vehicleNum(0xffff) - .build() + ClipperTrip( + agency = 0x12, + vehicleNum = 0xffff, + ) assertNull(tripFfff.vehicleID) // Test regular vehicle number val trip1058 = - ClipperTrip - .builder() - .agency(0x12) - .vehicleNum(1058) - .build() + ClipperTrip( + agency = 0x12, + vehicleNum = 1058, + ) assertEquals("1058", trip1058.vehicleID) // Test regular vehicle number val trip1525 = - ClipperTrip - .builder() - .agency(0x12) - .vehicleNum(1525) - .build() + ClipperTrip( + agency = 0x12, + vehicleNum = 1525, + ) assertEquals("1525", trip1525.vehicleID) // Test LRV4 Muni vehicle numbers (5 digits, encoded as number*10 + letter) // 2010A = 20100 + 1 - 1 = 20101? No, the encoding is: number/10 gives the vehicle, %10 gives letter offset // 20101: 20101/10 = 2010, 20101%10 = 1, letter = 9+1 = A (in hex, 10 = A) val trip2010A = - ClipperTrip - .builder() - .agency(0x12) - .vehicleNum(20101) - .build() + ClipperTrip( + agency = 0x12, + vehicleNum = 20101, + ) assertEquals("2010A", trip2010A.vehicleID) // 2061B = vehicle/10 = 2061, letter offset = 2 -> 9+2 = B (11 in hex = B) val trip2061B = - ClipperTrip - .builder() - .agency(0x12) - .vehicleNum(20612) - .build() + ClipperTrip( + agency = 0x12, + vehicleNum = 20612, + ) assertEquals("2061B", trip2061B.vehicleID) } @@ -356,28 +340,25 @@ class ClipperTransitTest { fun testHumanReadableRouteID() { // Golden Gate Ferry should display route ID in hex val ggFerryTrip = - ClipperTrip - .builder() - .agency(0x19) // AGENCY_GG_FERRY - .route(0x1234) - .build() + ClipperTrip( + agency = 0x19, // AGENCY_GG_FERRY + route = 0x1234, + ) assertEquals("0x1234", ggFerryTrip.humanReadableRouteID) // Other agencies should not have humanReadableRouteID val bartTrip = - ClipperTrip - .builder() - .agency(0x04) // AGENCY_BART - .route(0x5678) - .build() + ClipperTrip( + agency = 0x04, // AGENCY_BART + route = 0x5678, + ) assertNull(bartTrip.humanReadableRouteID) val muniTrip = - ClipperTrip - .builder() - .agency(0x12) // AGENCY_MUNI - .route(0xABCD) - .build() + ClipperTrip( + agency = 0x12, // AGENCY_MUNI + route = 0xABCD, + ) assertNull(muniTrip.humanReadableRouteID) } diff --git a/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt b/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt index 2d4569a2d..035c7810b 100644 --- a/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt +++ b/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt @@ -1,96 +1,13 @@ package com.codebutler.farebot.base.ui import com.codebutler.farebot.base.util.FormattedString -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable -import org.jetbrains.compose.resources.StringResource -@Serializable data class FareBotUiTree( val items: List, ) { - companion object { - fun builder(): Builder = Builder() - - private suspend fun buildItems(itemBuilders: List): List = itemBuilders.map { it.build() } - } - - class Builder { - private val itemBuilders = mutableListOf() - - fun item(): Item.Builder { - val builder = Item.builder() - itemBuilders.add(builder) - return builder - } - - suspend fun build(): FareBotUiTree = FareBotUiTree(buildItems(itemBuilders)) - } - - @Serializable data class Item( - val title: String, - @Contextual val value: Any?, - val children: List, - ) { - companion object { - fun builder(): Builder = Builder() - } - - class Builder { - private var title: FormattedString = FormattedString("") - private var value: Any? = null - private val childBuilders = mutableListOf() - - fun title(text: String): Builder { - this.title = FormattedString(text) - return this - } - - fun title(textRes: StringResource): Builder { - this.title = FormattedString(textRes) - return this - } - - fun title(formattedString: FormattedString): Builder { - this.title = formattedString - return this - } - - fun value(value: Any?): Builder { - this.value = value - return this - } - - fun item(): Builder { - val builder = Item.builder() - childBuilders.add(builder) - return builder - } - - fun item( - title: String, - value: Any?, - ): Builder = - item() - .title(title) - .value(value) - - fun item( - title: StringResource, - value: Any?, - ): Builder = - item().also { - it.title = FormattedString(title) - it.value(value) - } - - suspend fun build(): Item = - Item( - title = title.resolveAsync(), - value = value, - children = buildItems(childBuilders), - ) - } - } + val title: FormattedString, + val value: Any? = null, + val children: List = emptyList(), + ) } diff --git a/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt b/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt index 07e6d040f..4666798a7 100644 --- a/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt +++ b/base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt @@ -22,44 +22,61 @@ package com.codebutler.farebot.base.ui +import com.codebutler.farebot.base.util.FormattedString import org.jetbrains.compose.resources.StringResource @DslMarker private annotation class UiTreeBuilderMarker -suspend fun uiTree(init: TreeScope.() -> Unit): FareBotUiTree { - val uiBuilder = FareBotUiTree.builder() - TreeScope(uiBuilder).init() - return uiBuilder.build() +fun uiTree(init: TreeScope.() -> Unit): FareBotUiTree { + val scope = TreeScope() + scope.init() + return FareBotUiTree(scope.items.toList()) } @UiTreeBuilderMarker -class TreeScope( - private val uiBuilder: FareBotUiTree.Builder, -) { +class TreeScope { + internal val items = mutableListOf() + fun item(init: ItemScope.() -> Unit) { - ItemScope(uiBuilder.item()).init() + val scope = ItemScope() + scope.init() + items.add(scope.build()) } } @UiTreeBuilderMarker -class ItemScope( - private val item: FareBotUiTree.Item.Builder, -) { - var title: Any? = null +class ItemScope { + private var _title: FormattedString = FormattedString("") + var title: Any? + get() = _title set(value) { - when (value) { - is StringResource -> item.title(value) - else -> item.title(value.toString()) - } + _title = + when (value) { + is FormattedString -> value + is StringResource -> FormattedString(value) + else -> FormattedString(value.toString()) + } } var value: Any? = null - set(value) { - item.value(value) - } + + private val children = mutableListOf() fun item(init: ItemScope.() -> Unit) { - ItemScope(item.item()).init() + val scope = ItemScope() + scope.init() + children.add(scope.build()) } + + fun addChildren(items: List) { + children.addAll(items) + } + + internal fun build(): FareBotUiTree.Item = + FareBotUiTree.Item( + title = _title, + value = value, + children = children.toList(), + ) } diff --git a/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCard.kt b/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCard.kt index 9867be372..986adb263 100644 --- a/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCard.kt +++ b/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCard.kt @@ -24,6 +24,7 @@ package com.codebutler.farebot.card.cepas import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.CurrencyFormatter import com.codebutler.farebot.base.util.DateFormatStyle import com.codebutler.farebot.base.util.formatDate @@ -46,48 +47,94 @@ data class CEPASCard( fun getHistory(purse: Int): CEPASHistory? = histories[purse] - override suspend fun getAdvancedUi(): FareBotUiTree { - val cardUiBuilder = FareBotUiTree.builder() - - val pursesUiBuilder = cardUiBuilder.item().title("Purses") - for (purse in purses) { - val purseUiBuilder = - pursesUiBuilder - .item() - .title("Purse ID ${purse.id}") - purseUiBuilder.item().title("CEPAS Version").value(purse.cepasVersion) - purseUiBuilder.item().title("Purse Status").value(purse.purseStatus) - purseUiBuilder - .item() - .title("Purse Balance") - .value(CurrencyFormatter.formatValue(purse.purseBalance / 100.0, "SGD")) - purseUiBuilder - .item() - .title("Purse Creation Date") - .value(formatDate(Instant.fromEpochMilliseconds(purse.purseCreationDate * 1000L), DateFormatStyle.LONG)) - purseUiBuilder - .item() - .title("Purse Expiry Date") - .value(formatDate(Instant.fromEpochMilliseconds(purse.purseExpiryDate * 1000L), DateFormatStyle.LONG)) - purseUiBuilder.item().title("Autoload Amount").value(purse.autoLoadAmount) - purseUiBuilder.item().title("CAN").value(purse.can) - purseUiBuilder.item().title("CSN").value(purse.csn) - - val transactionUiBuilder = cardUiBuilder.item().title("Last Transaction Information") - transactionUiBuilder.item().title("TRP").value(purse.lastTransactionTRP) - transactionUiBuilder.item().title("Credit TRP").value(purse.lastCreditTransactionTRP) - transactionUiBuilder.item().title("Credit Header").value(purse.lastCreditTransactionHeader) - transactionUiBuilder.item().title("Debit Options").value(purse.lastTransactionDebitOptionsByte) - - val otherUiBuilder = cardUiBuilder.item().title("Other Purse Information") - otherUiBuilder.item().title("Logfile Record Count").value(purse.logfileRecordCount) - otherUiBuilder.item().title("Issuer Data Length").value(purse.issuerDataLength) - otherUiBuilder.item().title("Issuer-specific Data").value(purse.issuerSpecificData) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = "Purses" + for (purse in purses) { + item { + title = "Purse ID ${purse.id}" + item { + title = "CEPAS Version" + value = purse.cepasVersion + } + item { + title = "Purse Status" + value = purse.purseStatus + } + item { + title = "Purse Balance" + value = CurrencyFormatter.formatValue(purse.purseBalance / 100.0, "SGD") + } + item { + title = "Purse Creation Date" + value = + formatDate( + Instant.fromEpochMilliseconds(purse.purseCreationDate * 1000L), + DateFormatStyle.LONG, + ) + } + item { + title = "Purse Expiry Date" + value = + formatDate( + Instant.fromEpochMilliseconds(purse.purseExpiryDate * 1000L), + DateFormatStyle.LONG, + ) + } + item { + title = "Autoload Amount" + value = purse.autoLoadAmount + } + item { + title = "CAN" + value = purse.can + } + item { + title = "CSN" + value = purse.csn + } + } + } + } + for (purse in purses) { + item { + title = "Last Transaction Information" + item { + title = "TRP" + value = purse.lastTransactionTRP + } + item { + title = "Credit TRP" + value = purse.lastCreditTransactionTRP + } + item { + title = "Credit Header" + value = purse.lastCreditTransactionHeader + } + item { + title = "Debit Options" + value = purse.lastTransactionDebitOptionsByte + } + } + item { + title = "Other Purse Information" + item { + title = "Logfile Record Count" + value = purse.logfileRecordCount + } + item { + title = "Issuer Data Length" + value = purse.issuerDataLength + } + item { + title = "Issuer-specific Data" + value = purse.issuerSpecificData + } + } + } } - return cardUiBuilder.build() - } - companion object { fun create( tagId: ByteArray, diff --git a/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCard.kt b/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCard.kt index 7d42963fe..6ee6c21cb 100644 --- a/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCard.kt +++ b/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCard.kt @@ -27,6 +27,7 @@ package com.codebutler.farebot.card.classic import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType @@ -56,49 +57,49 @@ class ClassicCard( return ClassicManufacturingInfo.parse(block0.data, tagId) } - override suspend fun getAdvancedUi(): FareBotUiTree { - val cardUiBuilder = FareBotUiTree.builder() - for (sector in sectors) { - val sectorIndexString = sector.index.toString(16) - val sectorUiBuilder = cardUiBuilder.item() - when (sector) { - is UnauthorizedClassicSector -> { - sectorUiBuilder.title( - FormattedString( - Res.string.classic_unauthorized_sector_title_format, - sectorIndexString, - ), - ) - } - is InvalidClassicSector -> { - sectorUiBuilder.title( - FormattedString( - Res.string.classic_invalid_sector_title_format, - sectorIndexString, - sector.error, - ), - ) - } - else -> { - val dataClassicSector = sector as DataClassicSector - sectorUiBuilder.title( - FormattedString(Res.string.classic_sector_title_format, sectorIndexString), - ) - for (block in dataClassicSector.blocks) { - sectorUiBuilder - .item() - .title( + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + for (sector in sectors) { + val sectorIndexString = sector.index.toString(16) + when (sector) { + is UnauthorizedClassicSector -> { + item { + title = + FormattedString( + Res.string.classic_unauthorized_sector_title_format, + sectorIndexString, + ) + } + } + is InvalidClassicSector -> { + item { + title = FormattedString( - Res.string.classic_block_title_format, - block.index.toString(), - ), - ).value(block.data) + Res.string.classic_invalid_sector_title_format, + sectorIndexString, + sector.error, + ) + } + } + else -> { + val dataClassicSector = sector as DataClassicSector + item { + title = FormattedString(Res.string.classic_sector_title_format, sectorIndexString) + for (block in dataClassicSector.blocks) { + item { + title = + FormattedString( + Res.string.classic_block_title_format, + block.index.toString(), + ) + value = block.data + } + } + } } } } } - return cardUiBuilder.build() - } companion object { fun create( diff --git a/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCard.kt b/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCard.kt index f4a454b62..cda93ee3a 100644 --- a/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCard.kt +++ b/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCard.kt @@ -26,6 +26,7 @@ package com.codebutler.farebot.card.desfire import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType import kotlinx.serialization.Contextual @@ -44,124 +45,183 @@ data class DesfireCard( fun getApplication(appId: Int): DesfireApplication? = applications.firstOrNull { it.id == appId } - override suspend fun getAdvancedUi(): FareBotUiTree { - val cardUiBuilder = FareBotUiTree.builder() - val appsUiBuilder = cardUiBuilder.item().title("Applications") - for (app in applications) { - val appUiBuilder = - appsUiBuilder - .item() - .title("Application: 0x${app.id.toString(16)}") - val filesUiBuilder = appUiBuilder.item().title("Files") - for (file in app.files) { - val fileUiBuilder = - filesUiBuilder - .item() - .title("File: 0x${file.id.toString(16)}") - val fileSettings = file.fileSettings - if (fileSettings != null) { - val settingsUiBuilder = fileUiBuilder.item().title("Settings") - settingsUiBuilder - .item() - .title("Type") - .value(fileSettings.fileTypeName) - if (fileSettings is StandardDesfireFileSettings) { - settingsUiBuilder - .item() - .title("Size") - .value(fileSettings.fileSize) - } else if (fileSettings is RecordDesfireFileSettings) { - settingsUiBuilder - .item() - .title("Cur Records") - .value(fileSettings.curRecords) - settingsUiBuilder - .item() - .title("Max Records") - .value(fileSettings.maxRecords) - settingsUiBuilder - .item() - .title("Record Size") - .value(fileSettings.recordSize) - } else if (fileSettings is ValueDesfireFileSettings) { - settingsUiBuilder - .item() - .title("Range") - .value("${fileSettings.lowerLimit} - ${fileSettings.upperLimit}") - settingsUiBuilder - .item() - .title("Limited Credit") - .value( - "${fileSettings.limitedCreditValue} (${if (fileSettings.limitedCreditEnabled) "enabled" else "disabled"})", - ) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = "Applications" + for (app in applications) { + item { + title = "Application: 0x${app.id.toString(16)}" + item { + title = "Files" + for (file in app.files) { + item { + title = "File: 0x${file.id.toString(16)}" + val fileSettings = file.fileSettings + if (fileSettings != null) { + item { + title = "Settings" + item { + title = "Type" + value = fileSettings.fileTypeName + } + if (fileSettings is StandardDesfireFileSettings) { + item { + title = "Size" + value = fileSettings.fileSize + } + } else if (fileSettings is RecordDesfireFileSettings) { + item { + title = "Cur Records" + value = fileSettings.curRecords + } + item { + title = "Max Records" + value = fileSettings.maxRecords + } + item { + title = "Record Size" + value = fileSettings.recordSize + } + } else if (fileSettings is ValueDesfireFileSettings) { + item { + title = "Range" + value = + "${fileSettings.lowerLimit} - ${fileSettings.upperLimit}" + } + item { + title = "Limited Credit" + value = + "${fileSettings.limitedCreditValue} (${if (fileSettings.limitedCreditEnabled) "enabled" else "disabled"})" + } + } + } + } + if (file is StandardDesfireFile) { + item { + title = "Data" + value = file.data + } + } else if (file is RecordDesfireFile) { + item { + title = "Records" + val records = file.records + for (i in records.indices) { + val record = records[i] + item { + title = "Record $i" + value = record.data + } + } + } + } else if (file is ValueDesfireFile) { + item { + title = "Value" + value = file.value + } + } else if (file is InvalidDesfireFile) { + item { + title = "Error" + value = file.errorMessage + } + } else if (file is UnauthorizedDesfireFile) { + item { + title = "Error" + value = file.errorMessage + } + } + } + } + } } } - if (file is StandardDesfireFile) { - fileUiBuilder - .item() - .title("Data") - .value(file.data) - } else if (file is RecordDesfireFile) { - val recordsUiBuilder = - fileUiBuilder - .item() - .title("Records") - val records = file.records - for (i in records.indices) { - val record = records[i] - recordsUiBuilder - .item() - .title("Record $i") - .value(record.data) - } - } else if (file is ValueDesfireFile) { - fileUiBuilder - .item() - .title("Value") - .value(file.value) - } else if (file is InvalidDesfireFile) { - fileUiBuilder - .item() - .title("Error") - .value(file.errorMessage) - } else if (file is UnauthorizedDesfireFile) { - fileUiBuilder - .item() - .title("Error") - .value(file.errorMessage) + } + item { + title = "Manufacturing Data" + item { + title = "Hardware Information" + item { + title = "Vendor ID" + value = manufacturingData.hwVendorID + } + item { + title = "Type" + value = manufacturingData.hwType + } + item { + title = "Subtype" + value = manufacturingData.hwSubType + } + item { + title = "Major Version" + value = manufacturingData.hwMajorVersion + } + item { + title = "Minor Version" + value = manufacturingData.hwMinorVersion + } + item { + title = "Storage Size" + value = manufacturingData.hwStorageSize + } + item { + title = "Protocol" + value = manufacturingData.hwProtocol + } + } + item { + title = "Software Information" + item { + title = "Vendor ID" + value = manufacturingData.swVendorID + } + item { + title = "Type" + value = manufacturingData.swType + } + item { + title = "Subtype" + value = manufacturingData.swSubType + } + item { + title = "Major Version" + value = manufacturingData.swMajorVersion + } + item { + title = "Minor Version" + value = manufacturingData.swMinorVersion + } + item { + title = "Storage Size" + value = manufacturingData.swStorageSize + } + item { + title = "Protocol" + value = manufacturingData.swProtocol + } + } + item { + title = "General Information" + item { + title = "Serial Number" + value = manufacturingData.uidHex + } + item { + title = "Batch Number" + value = manufacturingData.batchNoHex + } + item { + title = "Week of Production" + value = manufacturingData.weekProd.toString(16) + } + item { + title = "Year of Production" + value = manufacturingData.yearProd.toString(16) + } } } } - val manufacturingDataUiBuilder = cardUiBuilder.item().title("Manufacturing Data") - - val hwInfoUiBuilder = manufacturingDataUiBuilder.item().title("Hardware Information") - hwInfoUiBuilder.item().title("Vendor ID").value(manufacturingData.hwVendorID) - hwInfoUiBuilder.item().title("Type").value(manufacturingData.hwType) - hwInfoUiBuilder.item().title("Subtype").value(manufacturingData.hwSubType) - hwInfoUiBuilder.item().title("Major Version").value(manufacturingData.hwMajorVersion) - hwInfoUiBuilder.item().title("Minor Version").value(manufacturingData.hwMinorVersion) - hwInfoUiBuilder.item().title("Storage Size").value(manufacturingData.hwStorageSize) - hwInfoUiBuilder.item().title("Protocol").value(manufacturingData.hwProtocol) - - val swInfoUiBuilder = manufacturingDataUiBuilder.item().title("Software Information") - swInfoUiBuilder.item().title("Vendor ID").value(manufacturingData.swVendorID) - swInfoUiBuilder.item().title("Type").value(manufacturingData.swType) - swInfoUiBuilder.item().title("Subtype").value(manufacturingData.swSubType) - swInfoUiBuilder.item().title("Major Version").value(manufacturingData.swMajorVersion) - swInfoUiBuilder.item().title("Minor Version").value(manufacturingData.swMinorVersion) - swInfoUiBuilder.item().title("Storage Size").value(manufacturingData.swStorageSize) - swInfoUiBuilder.item().title("Protocol").value(manufacturingData.swProtocol) - - val generalInfoUiBuilder = manufacturingDataUiBuilder.item().title("General Information") - generalInfoUiBuilder.item().title("Serial Number").value(manufacturingData.uidHex) - generalInfoUiBuilder.item().title("Batch Number").value(manufacturingData.batchNoHex) - generalInfoUiBuilder.item().title("Week of Production").value(manufacturingData.weekProd.toString(16)) - generalInfoUiBuilder.item().title("Year of Production").value(manufacturingData.yearProd.toString(16)) - - return cardUiBuilder.build() - } - companion object { fun create( tagId: ByteArray, diff --git a/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FelicaCard.kt b/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FelicaCard.kt index 427c9045d..5563976fe 100644 --- a/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FelicaCard.kt +++ b/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FelicaCard.kt @@ -27,6 +27,7 @@ package com.codebutler.farebot.card.felica import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType import kotlinx.serialization.Contextual @@ -50,35 +51,38 @@ data class FelicaCard( fun getSystem(systemCode: Int): FelicaSystem? = systemsByCode[systemCode] - override suspend fun getAdvancedUi(): FareBotUiTree { - val cardUiBuilder = FareBotUiTree.builder() - cardUiBuilder.item().title("IDm").value(idm) - cardUiBuilder.item().title("PMm").value(pmm) - val systemsUiBuilder = cardUiBuilder.item().title("Systems") - for (system in systems) { - val systemUiBuilder = - systemsUiBuilder - .item() - .title("System: ${system.code.toString(16)}") - for (service in system.services) { - val serviceUiBuilder = - systemUiBuilder - .item() - .title( - "Service: 0x${service.serviceCode.toString( - 16, - )} (${FelicaUtils.getFriendlyServiceName(system.code, service.serviceCode)})", - ) - for (block in service.blocks) { - serviceUiBuilder - .item() - .title("Block ${block.address.toString().padStart(2, '0')}") - .value(block.data) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = "IDm" + value = idm + } + item { + title = "PMm" + value = pmm + } + item { + title = "Systems" + for (system in systems) { + item { + title = "System: ${system.code.toString(16)}" + for (service in system.services) { + item { + title = "Service: 0x${service.serviceCode.toString( + 16, + )} (${FelicaUtils.getFriendlyServiceName(system.code, service.serviceCode)})" + for (block in service.blocks) { + item { + title = "Block ${block.address.toString().padStart(2, '0')}" + value = block.data + } + } + } + } + } } } } - return cardUiBuilder.build() - } companion object { fun create( diff --git a/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816Card.kt b/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816Card.kt index 1d3f36247..adf4fe9a9 100644 --- a/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816Card.kt +++ b/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816Card.kt @@ -24,6 +24,7 @@ package com.codebutler.farebot.card.iso7816 import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType import kotlinx.serialization.Contextual @@ -51,47 +52,67 @@ data class ISO7816Card( fun getApplicationByName(appName: ByteArray): ISO7816Application? = applications.firstOrNull { it.appName?.toHexString() == appName.toHexString() } - override suspend fun getAdvancedUi(): FareBotUiTree { - val cardUiBuilder = FareBotUiTree.builder() + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = "Applications" + for (app in applications) { + item { + val appNameStr = app.appName?.let { formatAID(it) } ?: "Unknown" + title = "Application: $appNameStr (${app.type})" - val appsUiBuilder = cardUiBuilder.item().title("Applications") - for (app in applications) { - val appUiBuilder = appsUiBuilder.item() - val appNameStr = app.appName?.let { formatAID(it) } ?: "Unknown" - appUiBuilder.title("Application: $appNameStr (${app.type})") + // Show files + if (app.files.isNotEmpty()) { + item { + title = "Files" + for ((selector, file) in app.files) { + item { + title = "File: $selector" + if (file.binaryData != null) { + item { + title = "Binary Data" + value = file.binaryData + } + } + for ((index, record) in file.records.entries.sortedBy { it.key }) { + item { + title = "Record $index" + value = record + } + } + } + } + } + } - // Show files - if (app.files.isNotEmpty()) { - val filesUiBuilder = appUiBuilder.item().title("Files") - for ((selector, file) in app.files) { - val fileUiBuilder = filesUiBuilder.item().title("File: $selector") - if (file.binaryData != null) { - fileUiBuilder.item().title("Binary Data").value(file.binaryData) - } - for ((index, record) in file.records.entries.sortedBy { it.key }) { - fileUiBuilder.item().title("Record $index").value(record) - } - } - } - - // Show SFI files - if (app.sfiFiles.isNotEmpty()) { - val sfiUiBuilder = appUiBuilder.item().title("SFI Files") - for ((sfi, file) in app.sfiFiles.entries.sortedBy { it.key }) { - val fileUiBuilder = sfiUiBuilder.item().title("SFI 0x${sfi.toString(16)}") - if (file.binaryData != null) { - fileUiBuilder.item().title("Binary Data").value(file.binaryData) - } - for ((index, record) in file.records.entries.sortedBy { it.key }) { - fileUiBuilder.item().title("Record $index").value(record) + // Show SFI files + if (app.sfiFiles.isNotEmpty()) { + item { + title = "SFI Files" + for ((sfi, file) in app.sfiFiles.entries.sortedBy { it.key }) { + item { + title = "SFI 0x${sfi.toString(16)}" + if (file.binaryData != null) { + item { + title = "Binary Data" + value = file.binaryData + } + } + for ((index, record) in file.records.entries.sortedBy { it.key }) { + item { + title = "Record $index" + value = record + } + } + } + } + } + } } } } } - return cardUiBuilder.build() - } - companion object { fun create( tagId: ByteArray, diff --git a/card/ksx6924/src/commonMain/kotlin/com/codebutler/farebot/card/ksx6924/KSX6924PurseInfo.kt b/card/ksx6924/src/commonMain/kotlin/com/codebutler/farebot/card/ksx6924/KSX6924PurseInfo.kt index d9f7ab5d7..2937ce3c4 100644 --- a/card/ksx6924/src/commonMain/kotlin/com/codebutler/farebot/card/ksx6924/KSX6924PurseInfo.kt +++ b/card/ksx6924/src/commonMain/kotlin/com/codebutler/farebot/card/ksx6924/KSX6924PurseInfo.kt @@ -25,6 +25,7 @@ package com.codebutler.farebot.card.ksx6924 import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.ListItem +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.base.util.NumberUtils import com.codebutler.farebot.base.util.byteArrayToLong @@ -133,20 +134,49 @@ data class KSX6924PurseInfo( ListItem(Res.string.ksx6924_discount_type, resolver.resolveDisRate(disRate)), ) - suspend fun getAdvancedInfo(resolver: KSX6924PurseInfoResolver = KSX6924PurseInfoDefaultResolver): FareBotUiTree { - val b = FareBotUiTree.builder() - b.item().title(Res.string.ksx6924_crypto_algorithm).value(resolver.resolveCryptoAlgo(alg)) - b.item().title(Res.string.ksx6924_encryption_key_version).value(vk.hexString) - b.item().title(Res.string.ksx6924_auth_id).value(idtr.hexString) - b.item().title(Res.string.ksx6924_ticket_type).value(resolver.resolveUserCode(userCode)) - b.item().title(Res.string.ksx6924_max_balance).value(balMax.toString()) - b.item().title(Res.string.ksx6924_branch_code).value(bra.hexString) - b.item().title(Res.string.ksx6924_one_time_limit).value(mmax.toString()) - b.item().title(Res.string.ksx6924_mobile_carrier).value(resolver.resolveTCode(tcode)) - b.item().title(Res.string.ksx6924_financial_institution).value(resolver.resolveCCode(ccode)) - b.item().title(Res.string.ksx6924_rfu).value(rfu.hex()) - return b.build() - } + suspend fun getAdvancedInfo(resolver: KSX6924PurseInfoResolver = KSX6924PurseInfoDefaultResolver): FareBotUiTree = + uiTree { + item { + title = Res.string.ksx6924_crypto_algorithm + value = resolver.resolveCryptoAlgo(alg) + } + item { + title = Res.string.ksx6924_encryption_key_version + value = vk.hexString + } + item { + title = Res.string.ksx6924_auth_id + value = idtr.hexString + } + item { + title = Res.string.ksx6924_ticket_type + value = resolver.resolveUserCode(userCode) + } + item { + title = Res.string.ksx6924_max_balance + value = balMax.toString() + } + item { + title = Res.string.ksx6924_branch_code + value = bra.hexString + } + item { + title = Res.string.ksx6924_one_time_limit + value = mmax.toString() + } + item { + title = Res.string.ksx6924_mobile_carrier + value = resolver.resolveTCode(tcode) + } + item { + title = Res.string.ksx6924_financial_institution + value = resolver.resolveCCode(ccode) + } + item { + title = Res.string.ksx6924_rfu + value = rfu.hex() + } + } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCard.kt b/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCard.kt index b9a462f6b..6dc416af2 100644 --- a/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCard.kt +++ b/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCard.kt @@ -26,6 +26,7 @@ package com.codebutler.farebot.card.ultralight import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType @@ -68,20 +69,18 @@ data class UltralightCard( return result } - override suspend fun getAdvancedUi(): FareBotUiTree { - val builder = FareBotUiTree.builder() - val pagesBuilder = - builder - .item() - .title(Res.string.ultralight_pages) - for (page in pages) { - pagesBuilder - .item() - .title(FormattedString(Res.string.ultralight_page_title_format, page.index.toString())) - .value(page.data) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = Res.string.ultralight_pages + for (page in pages) { + item { + title = FormattedString(Res.string.ultralight_page_title_format, page.index.toString()) + value = page.data + } + } + } } - return builder.build() - } /** * Known Ultralight card type variants, detected via GET_VERSION command. diff --git a/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCard.kt b/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCard.kt index acc2c9ec9..7f7b5fcfd 100644 --- a/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCard.kt +++ b/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCard.kt @@ -23,6 +23,7 @@ package com.codebutler.farebot.card.vicinity import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType import kotlinx.serialization.Contextual @@ -77,28 +78,24 @@ data class VicinityCard( } @OptIn(ExperimentalStdlibApi::class) - override suspend fun getAdvancedUi(): FareBotUiTree { - val builder = FareBotUiTree.builder() - if (sysInfo != null) { - builder - .item() - .title("System Info") - .value(sysInfo) - } - val pagesBuilder = builder.item().title("Pages") - for (page in pages) { - val pageBuilder = - pagesBuilder - .item() - .title("Page ${page.index}") - if (page.isUnauthorized) { - pageBuilder.value("Unauthorized") - } else { - pageBuilder.value(page.data) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + if (sysInfo != null) { + item { + title = "System Info" + value = sysInfo + } + } + item { + title = "Pages" + for (page in pages) { + item { + title = "Page ${page.index}" + value = if (page.isUnauthorized) "Unauthorized" else page.data + } + } } } - return builder.build() - } companion object { fun create( diff --git a/docs/plans/2026-02-16-remove-builders-design.md b/docs/plans/2026-02-16-remove-builders-design.md new file mode 100644 index 000000000..9efbbd213 --- /dev/null +++ b/docs/plans/2026-02-16-remove-builders-design.md @@ -0,0 +1,37 @@ +# Remove Builder Classes + +## Problem + +The codebase has 4 Builder classes that are not idiomatic Kotlin. Kotlin data classes with named parameters and default values make Builders unnecessary. + +## Builders to Remove + +### 1. ClipperTrip.Builder +- 10 Long fields, all default to 0 +- Replace with: data class constructor with named params, all defaulting to 0 + +### 2. SeqGoTrip.Builder +- 8 fields (ints, nullable Instants, nullable Stations, Mode enum) +- Replace with: data class constructor with named params and defaults + +### 3. Station.Builder +- 8 fields (nullable Strings, one List) +- Replace with: data class constructor with named params, all defaulting to null/emptyList() + +### 4. FareBotUiTree.Builder + Item.Builder +- Hierarchical builder for UI tree structures +- Item.title is currently `String`, resolved from `FormattedString` during `suspend build()` +- `@Serializable` annotation is unnecessary (never serialized) +- Replace with: + - Drop `@Serializable` from FareBotUiTree and Item + - Change `Item.title` from `String` to `FormattedString` + - Remove both Builder classes + - Rewrite `uiTree {}` DSL to build Item objects directly + - Update all ~20 call sites that use the builder directly + +## Approach + +- Change `Item.title` to `FormattedString`, resolve at the UI layer instead +- Rewrite `UiTreeBuilder.kt` DSL to construct items directly (no intermediate Builder) +- For ClipperTrip, SeqGoTrip, Station: add default parameter values, remove Builder + companion factory +- Update all call sites diff --git a/docs/plans/2026-02-16-remove-builders.md b/docs/plans/2026-02-16-remove-builders.md new file mode 100644 index 000000000..97f14c60b --- /dev/null +++ b/docs/plans/2026-02-16-remove-builders.md @@ -0,0 +1,864 @@ +# Remove Builder Classes Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Remove all Builder classes and replace with idiomatic Kotlin patterns (data class constructors, DSL). + +**Architecture:** Three simple Builder classes (ClipperTrip, SeqGoTrip, Station) are replaced with data class constructors using named parameters and defaults. The hierarchical FareBotUiTree Builder is replaced by rewriting the existing `uiTree {}` DSL to build items directly, and converting all 19 imperative builder call sites to use the DSL. + +**Tech Stack:** Kotlin Multiplatform, Compose Multiplatform, compose-resources + +--- + +## Task 1: Rewrite FareBotUiTree data model + +**Files:** +- Modify: `base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt` + +**Step 1: Rewrite FareBotUiTree.kt** + +Replace the entire file with: + +```kotlin +package com.codebutler.farebot.base.ui + +import com.codebutler.farebot.base.util.FormattedString + +data class FareBotUiTree( + val items: List, +) { + data class Item( + val title: FormattedString, + val value: Any? = null, + val children: List = emptyList(), + ) +} +``` + +Key changes: +- Remove `@Serializable` from both classes (never serialized) +- Change `Item.title` from `String` to `FormattedString` +- Remove `Item.Builder`, `FareBotUiTree.Builder`, companion objects +- Remove `@Contextual` annotation on `value` +- `children` and `value` get defaults + +**Step 2: Commit** + +```bash +git add base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt +git commit -m "refactor: simplify FareBotUiTree to plain data classes + +Remove Builder classes and @Serializable annotation. Change Item.title +from String to FormattedString to defer resolution to UI layer." +``` + +--- + +## Task 2: Rewrite UiTreeBuilder DSL + +**Files:** +- Modify: `base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt` + +**Step 1: Rewrite UiTreeBuilder.kt** + +Replace the entire file with: + +```kotlin +package com.codebutler.farebot.base.ui + +import com.codebutler.farebot.base.util.FormattedString +import org.jetbrains.compose.resources.StringResource + +@DslMarker +private annotation class UiTreeBuilderMarker + +fun uiTree(init: TreeScope.() -> Unit): FareBotUiTree { + val scope = TreeScope() + scope.init() + return FareBotUiTree(scope.items.toList()) +} + +@UiTreeBuilderMarker +class TreeScope { + internal val items = mutableListOf() + + fun item(init: ItemScope.() -> Unit) { + val scope = ItemScope() + scope.init() + items.add(scope.build()) + } +} + +@UiTreeBuilderMarker +class ItemScope { + private var _title: FormattedString = FormattedString("") + var title: Any? + get() = _title + set(value) { + _title = when (value) { + is FormattedString -> value + is StringResource -> FormattedString(value) + else -> FormattedString(value.toString()) + } + } + + var value: Any? = null + + private val children = mutableListOf() + + fun item(init: ItemScope.() -> Unit) { + val scope = ItemScope() + scope.init() + children.add(scope.build()) + } + + fun addChildren(items: List) { + children.addAll(items) + } + + internal fun build(): FareBotUiTree.Item = FareBotUiTree.Item( + title = _title, + value = value, + children = children.toList(), + ) +} +``` + +Key changes: +- `uiTree` is no longer `suspend` (no async string resolution during build) +- DSL builds `Item` objects directly instead of delegating to Builder +- `ItemScope.title` accepts `Any?` (String, StringResource, FormattedString) for ergonomics +- `addChildren()` allows appending pre-built items (for OVChipIndex pattern) + +**Step 2: Commit** + +```bash +git add base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/UiTreeBuilder.kt +git commit -m "refactor: rewrite uiTree DSL to build items directly + +No longer wraps Builder classes. The DSL constructs FareBotUiTree.Item +objects directly. uiTree is no longer suspend since FormattedString +resolution is deferred to the UI layer." +``` + +--- + +## Task 3: Update CardAdvancedScreen for FormattedString title + +**Files:** +- Modify: `app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt` + +**Step 1: Change `item.title.orEmpty()` to `item.title.resolve()`** + +In the `TreeItemView` composable, change: +```kotlin +Text( + text = item.title.orEmpty(), +``` +to: +```kotlin +Text( + text = item.title.resolve(), +``` + +The `resolve()` method is a `@Composable` function on `FormattedString` that resolves string resources at render time. + +**Step 2: Commit** + +```bash +git add app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/CardAdvancedScreen.kt +git commit -m "refactor: resolve FormattedString title in CardAdvancedScreen" +``` + +--- + +## Task 4: Convert card module getAdvancedUi() to DSL + +**Files (8):** +- `card/classic/src/commonMain/kotlin/.../ClassicCard.kt` +- `card/desfire/src/commonMain/kotlin/.../DesfireCard.kt` +- `card/felica/src/commonMain/kotlin/.../FelicaCard.kt` +- `card/iso7816/src/commonMain/kotlin/.../ISO7816Card.kt` +- `card/cepas/src/commonMain/kotlin/.../CEPASCard.kt` +- `card/ultralight/src/commonMain/kotlin/.../UltralightCard.kt` +- `card/vicinity/src/commonMain/kotlin/.../VicinityCard.kt` +- `card/ksx6924/src/commonMain/kotlin/.../KSX6924PurseInfo.kt` + +**Step 1: Convert each file's getAdvancedUi() from builder to DSL** + +Each file follows the same pattern. Replace: +```kotlin +import com.codebutler.farebot.base.ui.FareBotUiTree +// ... +val builder = FareBotUiTree.builder() +builder.item().title("X").value(y) +return builder.build() +``` +with: +```kotlin +import com.codebutler.farebot.base.ui.uiTree +// ... +return uiTree { + item { title = "X"; value = y } +} +``` + +Specific conversions per file: + +**ClassicCard.kt** — nested sectors with conditional titles: +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + for (sector in sectors) { + val sectorIndexString = sector.index.toString(16) + item { + when (sector) { + is UnauthorizedClassicSector -> { + title = FormattedString(Res.string.classic_unauthorized_sector_title_format, sectorIndexString) + } + is InvalidClassicSector -> { + title = FormattedString(Res.string.classic_invalid_sector_title_format, sectorIndexString, sector.error) + } + else -> { + val dataClassicSector = sector as DataClassicSector + title = FormattedString(Res.string.classic_sector_title_format, sectorIndexString) + for (block in dataClassicSector.blocks) { + item { + title = FormattedString(Res.string.classic_block_title_format, block.index.toString()) + value = block.data + } + } + } + } + } + } +} +``` + +**DesfireCard.kt** — deeply nested apps/files/settings: +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { + title = "Applications" + for (app in applications) { + item { + title = "Application: 0x${app.id.toString(16)}" + item { + title = "Files" + for (file in app.files) { + item { + title = "File: 0x${file.id.toString(16)}" + val fileSettings = file.fileSettings + if (fileSettings != null) { + item { + title = "Settings" + item { title = "Type"; value = fileSettings.fileTypeName } + if (fileSettings is StandardDesfireFileSettings) { + item { title = "Size"; value = fileSettings.fileSize } + } else if (fileSettings is RecordDesfireFileSettings) { + item { title = "Cur Records"; value = fileSettings.curRecords } + item { title = "Max Records"; value = fileSettings.maxRecords } + item { title = "Record Size"; value = fileSettings.recordSize } + } else if (fileSettings is ValueDesfireFileSettings) { + item { title = "Range"; value = "${fileSettings.lowerLimit} - ${fileSettings.upperLimit}" } + item { + title = "Limited Credit" + value = "${fileSettings.limitedCreditValue} (${if (fileSettings.limitedCreditEnabled) "enabled" else "disabled"})" + } + } + } + } + if (file is StandardDesfireFile) { + item { title = "Data"; value = file.data } + } else if (file is RecordDesfireFile) { + item { + title = "Records" + for (i in file.records.indices) { + item { title = "Record $i"; value = file.records[i].data } + } + } + } else if (file is ValueDesfireFile) { + item { title = "Value"; value = file.value } + } else if (file is InvalidDesfireFile) { + item { title = "Error"; value = file.errorMessage } + } else if (file is UnauthorizedDesfireFile) { + item { title = "Error"; value = file.errorMessage } + } + } + } + } + } + } + } + item { + title = "Manufacturing Data" + item { + title = "Hardware Information" + item { title = "Vendor ID"; value = manufacturingData.hwVendorID } + item { title = "Type"; value = manufacturingData.hwType } + item { title = "Subtype"; value = manufacturingData.hwSubType } + item { title = "Major Version"; value = manufacturingData.hwMajorVersion } + item { title = "Minor Version"; value = manufacturingData.hwMinorVersion } + item { title = "Storage Size"; value = manufacturingData.hwStorageSize } + item { title = "Protocol"; value = manufacturingData.hwProtocol } + } + item { + title = "Software Information" + item { title = "Vendor ID"; value = manufacturingData.swVendorID } + item { title = "Type"; value = manufacturingData.swType } + item { title = "Subtype"; value = manufacturingData.swSubType } + item { title = "Major Version"; value = manufacturingData.swMajorVersion } + item { title = "Minor Version"; value = manufacturingData.swMinorVersion } + item { title = "Storage Size"; value = manufacturingData.swStorageSize } + item { title = "Protocol"; value = manufacturingData.swProtocol } + } + item { + title = "General Information" + item { title = "Serial Number"; value = manufacturingData.uidHex } + item { title = "Batch Number"; value = manufacturingData.batchNoHex } + item { title = "Week of Production"; value = manufacturingData.weekProd.toString(16) } + item { title = "Year of Production"; value = manufacturingData.yearProd.toString(16) } + } + } +} +``` + +**FelicaCard.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = "IDm"; value = idm } + item { title = "PMm"; value = pmm } + item { + title = "Systems" + for (system in systems) { + item { + title = "System: ${system.code.toString(16)}" + for (service in system.services) { + item { + title = "Service: 0x${service.serviceCode.toString(16)} (${FelicaUtils.getFriendlyServiceName(system.code, service.serviceCode)})" + for (block in service.blocks) { + item { + title = "Block ${block.address.toString().padStart(2, '0')}" + value = block.data + } + } + } + } + } + } + } +} +``` + +**ISO7816Card.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { + title = "Applications" + for (app in applications) { + item { + val appNameStr = app.appName?.let { formatAID(it) } ?: "Unknown" + title = "Application: $appNameStr (${app.type})" + if (app.files.isNotEmpty()) { + item { + title = "Files" + for ((selector, file) in app.files) { + item { + title = "File: $selector" + if (file.binaryData != null) { + item { title = "Binary Data"; value = file.binaryData } + } + for ((index, record) in file.records.entries.sortedBy { it.key }) { + item { title = "Record $index"; value = record } + } + } + } + } + } + if (app.sfiFiles.isNotEmpty()) { + item { + title = "SFI Files" + for ((sfi, file) in app.sfiFiles.entries.sortedBy { it.key }) { + item { + title = "SFI 0x${sfi.toString(16)}" + if (file.binaryData != null) { + item { title = "Binary Data"; value = file.binaryData } + } + for ((index, record) in file.records.entries.sortedBy { it.key }) { + item { title = "Record $index"; value = record } + } + } + } + } + } + } + } + } +} +``` + +**CEPASCard.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + for (purse in purses) { + item { + title = "Purse ID ${purse.id}" + item { title = "CEPAS Version"; value = purse.cepasVersion } + item { title = "Purse Status"; value = purse.purseStatus } + item { title = "Purse Balance"; value = CurrencyFormatter.formatValue(purse.purseBalance / 100.0, "SGD") } + item { title = "Purse Creation Date"; value = formatDate(Instant.fromEpochMilliseconds(purse.purseCreationDate * 1000L), DateFormatStyle.LONG) } + item { title = "Purse Expiry Date"; value = formatDate(Instant.fromEpochMilliseconds(purse.purseExpiryDate * 1000L), DateFormatStyle.LONG) } + item { title = "Autoload Amount"; value = purse.autoLoadAmount } + item { title = "CAN"; value = purse.can } + item { title = "CSN"; value = purse.csn } + } + item { + title = "Last Transaction Information" + item { title = "TRP"; value = purse.lastTransactionTRP } + item { title = "Credit TRP"; value = purse.lastCreditTransactionTRP } + item { title = "Credit Header"; value = purse.lastCreditTransactionHeader } + item { title = "Debit Options"; value = purse.lastTransactionDebitOptionsByte } + } + item { + title = "Other Purse Information" + item { title = "Logfile Record Count"; value = purse.logfileRecordCount } + item { title = "Issuer Data Length"; value = purse.issuerDataLength } + item { title = "Issuer-specific Data"; value = purse.issuerSpecificData } + } + } +} +``` + +**UltralightCard.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { + title = Res.string.ultralight_pages + for (page in pages) { + item { + title = FormattedString(Res.string.ultralight_page_title_format, page.index.toString()) + value = page.data + } + } + } +} +``` + +**VicinityCard.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + if (sysInfo != null) { + item { title = "System Info"; value = sysInfo } + } + item { + title = "Pages" + for (page in pages) { + item { + title = "Page ${page.index}" + value = if (page.isUnauthorized) "Unauthorized" else page.data + } + } + } +} +``` + +**KSX6924PurseInfo.kt:** +```kotlin +suspend fun getAdvancedInfo(resolver: KSX6924PurseInfoResolver = KSX6924PurseInfoDefaultResolver): FareBotUiTree = uiTree { + item { title = Res.string.ksx6924_crypto_algorithm; value = resolver.resolveCryptoAlgo(alg) } + item { title = Res.string.ksx6924_encryption_key_version; value = vk.hexString } + item { title = Res.string.ksx6924_auth_id; value = idtr.hexString } + item { title = Res.string.ksx6924_ticket_type; value = resolver.resolveUserCode(userCode) } + item { title = Res.string.ksx6924_max_balance; value = balMax.toString() } + item { title = Res.string.ksx6924_branch_code; value = bra.hexString } + item { title = Res.string.ksx6924_one_time_limit; value = mmax.toString() } + item { title = Res.string.ksx6924_mobile_carrier; value = resolver.resolveTCode(tcode) } + item { title = Res.string.ksx6924_financial_institution; value = resolver.resolveCCode(ccode) } + item { title = Res.string.ksx6924_rfu; value = rfu.hex() } +} +``` + +For each file, update imports: replace `import com.codebutler.farebot.base.ui.FareBotUiTree` with `import com.codebutler.farebot.base.ui.uiTree` (and keep FareBotUiTree import only if the type is referenced in return type annotations). + +**Step 2: Commit** + +```bash +git add card/ +git commit -m "refactor: convert card getAdvancedUi() from builder to DSL" +``` + +--- + +## Task 5: Convert transit module getAdvancedUi() to DSL + +**Files (12):** +- `transit/ovc/src/commonMain/kotlin/.../OVChipTransitInfo.kt` +- `transit/ovc/src/commonMain/kotlin/.../OVChipIndex.kt` +- `transit/octopus/src/commonMain/kotlin/.../OctopusTransitInfo.kt` +- `transit/smartrider/src/commonMain/kotlin/.../SmartRiderTransitInfo.kt` +- `transit/hsl/src/commonMain/kotlin/.../HSLTransitInfo.kt` +- `transit/charlie/src/commonMain/kotlin/.../CharlieCardTransitInfo.kt` +- `transit/nextfareul/src/commonMain/kotlin/.../NextfareUltralightTransitData.kt` +- `transit/calypso/src/commonMain/kotlin/.../LisboaVivaTransitInfo.kt` +- `transit/serialonly/src/commonMain/kotlin/.../StrelkaTransitInfo.kt` +- `transit/serialonly/src/commonMain/kotlin/.../HoloTransitInfo.kt` +- `transit/umarsh/src/commonMain/kotlin/.../UmarshTransitInfo.kt` +- `transit/troika/src/commonMain/kotlin/.../TroikaHybridTransitInfo.kt` + +**Step 1: Convert OVChipIndex.addAdvancedItems** + +Change from taking `FareBotUiTree.Item.Builder` to returning `List`: + +```kotlin +fun advancedItems(): List = listOf( + FareBotUiTree.Item(title = FormattedString("Transaction Slot"), value = if (recentTransactionSlot) "B" else "A"), + FareBotUiTree.Item(title = FormattedString("Info Slot"), value = if (recentInfoSlot) "B" else "A"), + FareBotUiTree.Item(title = FormattedString("Subscription Slot"), value = if (recentSubscriptionSlot) "B" else "A"), + FareBotUiTree.Item(title = FormattedString("Travelhistory Slot"), value = if (recentTravelhistorySlot) "B" else "A"), + FareBotUiTree.Item(title = FormattedString("Credit Slot"), value = if (recentCreditSlot) "B" else "A"), +) +``` + +**Step 2: Convert OVChipTransitInfo.getAdvancedUi()** + +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = "Credit Slot ID"; value = creditSlotId.toString() } + item { title = "Last Credit ID"; value = creditId.toString() } + item { + title = "Recent Slots" + addChildren(index.advancedItems()) + } +} +``` + +**Step 3: Convert remaining transit files** + +Each follows the same builder-to-DSL pattern. Specific conversions: + +**OctopusTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + val szt = shenzhenBalance + if (!hasOctopus || szt == null) return null + return uiTree { + item { + title = Res.string.octopus_alternate_purse_balances + item { + title = Res.string.octopus_szt + value = TransitCurrency.CNY(szt).formatCurrencyString(isBalance = true) + } + } + } +} +``` + +**SmartRiderTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = Res.string.smartrider_ticket_type; value = mTokenType.toString() } + if (mSmartRiderType == SmartRiderType.SMARTRIDER) { + item { title = Res.string.smartrider_autoload_threshold; value = TransitCurrency.AUD(mAutoloadThreshold).formatCurrencyString(true) } + item { title = Res.string.smartrider_autoload_value; value = TransitCurrency.AUD(mAutoloadValue).formatCurrencyString(true) } + } +} +``` + +**HSLTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + val tree = uiTree { + applicationVersion?.let { item { title = Res.string.hsl_application_version; value = it } } + applicationKeyVersion?.let { item { title = Res.string.hsl_application_key_version; value = it } } + platformType?.let { item { title = Res.string.hsl_platform_type; value = it } } + securityLevel?.let { item { title = Res.string.hsl_security_level; value = it } } + } + return if (tree.items.isEmpty()) null else tree +} +``` + +**CharlieCardTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + if (secondSerial == 0L || secondSerial == 0xffffffffL) return null + return uiTree { + item { title = Res.string.charlie_2nd_card_number; value = "A" + NumberUtils.zeroPad(secondSerial, 10) } + } +} +``` + +**NextfareUltralightTransitData.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = Res.string.nextfareul_machine_code; value = capsule.mMachineCode.toString(16) } +} +``` + +**LisboaVivaTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + if (tagId == null) return null + return uiTree { + item { title = Res.string.calypso_engraved_serial; value = tagId.toString() } + } +} +``` + +**StrelkaTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = Res.string.strelka_long_serial; value = mSerial } +} +``` + +**HoloTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree = uiTree { + item { title = Res.string.manufacture_id; value = mManufacturingId } +} +``` + +**UmarshTransitInfo.kt:** +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + val rubSectors = sectors.filter { it.denomination == UmarshDenomination.RUB } + if (rubSectors.isEmpty()) return null + return uiTree { + for (sec in rubSectors) { + item { title = Res.string.umarsh_machine_id; value = sec.machineId.toString() } + } + } +} +``` + +**TroikaHybridTransitInfo.kt** — flattens sub-trees: +```kotlin +override suspend fun getAdvancedUi(): FareBotUiTree? { + val trees = listOfNotNull( + troika.getAdvancedUi(), + podorozhnik?.getAdvancedUi(), + strelka?.getAdvancedUi(), + ) + if (trees.isEmpty()) return null + return FareBotUiTree(items = trees.flatMap { tree -> + tree.items.map { FareBotUiTree.Item(title = it.title, value = it.value) } + }) +} +``` + +**Step 4: Commit** + +```bash +git add transit/ +git commit -m "refactor: convert transit getAdvancedUi() from builder to DSL" +``` + +--- + +## Task 6: Remove ClipperTrip.Builder + +**Files:** +- Modify: `transit/clipper/src/commonMain/kotlin/.../ClipperTrip.kt` +- Modify: `transit/clipper/src/commonMain/kotlin/.../ClipperTransitFactory.kt` +- Modify: `app/src/commonTest/kotlin/.../ClipperTransitTest.kt` + +**Step 1: Add default values to ClipperTrip constructor, remove Builder** + +Add `= 0` defaults to all constructor parameters (vehicleNum and transportCode already have them). Remove the `Builder` class and `companion object`: + +```kotlin +class ClipperTrip( + private val timestamp: Long = 0, + private val exitTimestampValue: Long = 0, + private val balance: Long = 0, + private val fareValue: Long = 0, + private val agency: Long = 0, + private val from: Long = 0, + private val to: Long = 0, + private val route: Long = 0, + private val vehicleNum: Long = 0, + private val transportCode: Long = 0, +) : Trip() { + // ... all properties and methods stay the same ... + // DELETE: companion object { fun builder() } + // DELETE: class Builder { ... } +} +``` + +**Step 2: Update ClipperTransitFactory.createTrip()** + +Replace: +```kotlin +return ClipperTrip + .builder() + .timestamp(timestamp) + .exitTimestamp(exitTimestamp) + // ... + .build() +``` +with: +```kotlin +return ClipperTrip( + timestamp = timestamp, + exitTimestampValue = exitTimestamp, + balance = 0, + fareValue = fare, + agency = agency, + from = from, + to = to, + route = route, + vehicleNum = vehicleNum, + transportCode = transportCode, +) +``` + +**Step 3: Update ClipperTransitTest.kt** + +Replace all `ClipperTrip.builder().agency(x).transportCode(y).build()` with constructor calls: +```kotlin +// Before: +val trip = ClipperTrip.builder().agency(0x04).transportCode(0x6f).build() +// After: +val trip = ClipperTrip(agency = 0x04, transportCode = 0x6f) +``` + +Apply this to every test method (~15 call sites). + +**Step 4: Commit** + +```bash +git add transit/clipper/ app/src/commonTest/ +git commit -m "refactor: remove ClipperTrip.Builder, use constructor with named params" +``` + +--- + +## Task 7: Remove SeqGoTrip.Builder + +**Files:** +- Modify: `transit/seqgo/src/commonMain/kotlin/.../SeqGoTrip.kt` +- Modify: `transit/seqgo/src/commonMain/kotlin/.../SeqGoTransitFactory.kt` + +**Step 1: Add default values to SeqGoTrip constructor, remove Builder** + +```kotlin +class SeqGoTrip( + private val journeyId: Int = 0, + private val modeValue: Mode = Mode.OTHER, + private val startTime: Instant? = null, + private val endTime: Instant? = null, + private val startStationId: Int = 0, + private val endStationId: Int = 0, + private val startStationValue: Station? = null, + private val endStationValue: Station? = null, +) : Trip() { + // ... all properties and methods stay the same ... + // DELETE: class Builder { ... } + // DELETE: companion object { fun builder() } +} +``` + +**Step 2: Update SeqGoTransitFactory** + +Replace builder usage with direct construction. The tricky part: the builder pattern was used to conditionally add tap-off data. Use `var` locals + copy pattern, or build the trip in one expression: + +```kotlin +val tapOn = sortedTaps[i] +var endTime: Instant? = null +var endStationId = 0 +var endStation: Station? = null + +if (sortedTaps.size > i + 1 && + sortedTaps[i + 1].journey == tapOn.journey && + sortedTaps[i + 1].mode == tapOn.mode +) { + val tapOff = sortedTaps[i + 1] + endTime = tapOff.timestamp + endStationId = tapOff.station + endStation = SeqGoUtil.getStation(tapOff.station) + i++ +} + +trips.add( + SeqGoTrip( + journeyId = tapOn.journey, + modeValue = tapOn.mode, + startTime = tapOn.timestamp, + endTime = endTime, + startStationId = tapOn.station, + endStationId = endStationId, + startStationValue = SeqGoUtil.getStation(tapOn.station), + endStationValue = endStation, + ) +) +``` + +**Step 3: Commit** + +```bash +git add transit/seqgo/ +git commit -m "refactor: remove SeqGoTrip.Builder, use constructor with named params" +``` + +--- + +## Task 8: Remove Station.Builder + +**Files:** +- Modify: `transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt` + +**Step 1: Remove Builder class and companion factory method** + +Delete the `Builder` class (lines 110-170) and the `builder()` method from the companion object. The `Station` data class already has default values for all constructor parameters. No call sites to update (0 usages). + +**Step 2: Commit** + +```bash +git add transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt +git commit -m "refactor: remove unused Station.Builder" +``` + +--- + +## Task 9: Build and test + +**Step 1: Run tests** + +```bash +cd /workspace/.worktrees/remove-builders && ./gradlew allTests +``` + +Expected: All tests pass. + +**Step 2: Run full build** + +```bash +cd /workspace/.worktrees/remove-builders && ./gradlew assemble +``` + +Expected: Build succeeds. + +**Step 3: Fix any compilation errors** + +Common issues to watch for: +- Missing imports for `uiTree` or `FormattedString` +- `FareBotUiTree.builder()` references still lingering +- `FareBotUiTree.Item.Builder` type references in function signatures (OVChipIndex) + +--- + +## Task Dependency Graph + +``` +Task 1 (FareBotUiTree data model) + └─ Task 2 (UiTreeBuilder DSL) + ├─ Task 3 (CardAdvancedScreen) + ├─ Task 4 (card modules) ──┐ + └─ Task 5 (transit modules)┤ + ├─ Task 9 (build & test) +Task 6 (ClipperTrip.Builder) ─────┤ +Task 7 (SeqGoTrip.Builder) ───────┤ +Task 8 (Station.Builder) ─────────┘ +``` + +Tasks 6, 7, 8 are independent of tasks 1-5 and can run in parallel. +Tasks 4 and 5 can run in parallel after tasks 1-3 are complete. diff --git a/transit/calypso/src/commonMain/kotlin/com/codebutler/farebot/transit/calypso/lisboaviva/LisboaVivaTransitInfo.kt b/transit/calypso/src/commonMain/kotlin/com/codebutler/farebot/transit/calypso/lisboaviva/LisboaVivaTransitInfo.kt index bc26e985d..297d84cc2 100644 --- a/transit/calypso/src/commonMain/kotlin/com/codebutler/farebot/transit/calypso/lisboaviva/LisboaVivaTransitInfo.kt +++ b/transit/calypso/src/commonMain/kotlin/com/codebutler/farebot/transit/calypso/lisboaviva/LisboaVivaTransitInfo.kt @@ -23,6 +23,7 @@ package com.codebutler.farebot.transit.calypso.lisboaviva import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.ListItem import com.codebutler.farebot.base.ui.ListItemInterface +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.base.util.NumberUtils import com.codebutler.farebot.base.util.byteArrayToLong @@ -60,9 +61,12 @@ class LisboaVivaTransitInfo internal constructor( override suspend fun getAdvancedUi(): FareBotUiTree? { if (tagId == null) return null - val b = FareBotUiTree.builder() - b.item().title(Res.string.calypso_engraved_serial).value(tagId.toString()) - return b.build() + return uiTree { + item { + title = Res.string.calypso_engraved_serial + value = tagId.toString() + } + } } companion object { diff --git a/transit/charlie/src/commonMain/kotlin/com/codebutler/farebot/transit/charlie/CharlieCardTransitInfo.kt b/transit/charlie/src/commonMain/kotlin/com/codebutler/farebot/transit/charlie/CharlieCardTransitInfo.kt index e5bf707c1..a432671ca 100644 --- a/transit/charlie/src/commonMain/kotlin/com/codebutler/farebot/transit/charlie/CharlieCardTransitInfo.kt +++ b/transit/charlie/src/commonMain/kotlin/com/codebutler/farebot/transit/charlie/CharlieCardTransitInfo.kt @@ -23,6 +23,7 @@ package com.codebutler.farebot.transit.charlie import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.base.util.NumberUtils import com.codebutler.farebot.transit.Subscription @@ -88,9 +89,12 @@ class CharlieCardTransitInfo internal constructor( override suspend fun getAdvancedUi(): FareBotUiTree? { if (secondSerial == 0L || secondSerial == 0xffffffffL) return null - val b = FareBotUiTree.builder() - b.item().title(Res.string.charlie_2nd_card_number).value("A" + NumberUtils.zeroPad(secondSerial, 10)) - return b.build() + return uiTree { + item { + title = Res.string.charlie_2nd_card_number + value = "A" + NumberUtils.zeroPad(secondSerial, 10) + } + } } companion object { diff --git a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperData.kt b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperData.kt index 2e8b1a93a..8d6212919 100644 --- a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperData.kt +++ b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperData.kt @@ -70,15 +70,14 @@ internal object ClipperData { val id = (agency shl 16) or stationId val result = MdstStationLookup.getStation(CLIPPER_STR, id) if (result != null) { - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } if (agency == AGENCY_GGT || diff --git a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTransitFactory.kt b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTransitFactory.kt index 5e41d51ed..435ff1351 100644 --- a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTransitFactory.kt +++ b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTransitFactory.kt @@ -222,19 +222,17 @@ class ClipperTransitFactory : TransitFactory { return null } - return ClipperTrip - .builder() - .timestamp(timestamp) - .exitTimestamp(exitTimestamp) - .fare(fare) - .agency(agency) - .from(from) - .to(to) - .route(route) - .vehicleNum(vehicleNum) - .transportCode(transportCode) - .balance(0) // Filled in later - .build() + return ClipperTrip( + timestamp = timestamp, + exitTimestampValue = exitTimestamp, + fareValue = fare, + agency = agency, + from = from, + to = to, + route = route, + vehicleNum = vehicleNum, + transportCode = transportCode, + ) } private fun parseRefills(card: DesfireCard): List { diff --git a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTrip.kt b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTrip.kt index 2f6c836f8..7c53119e7 100644 --- a/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTrip.kt +++ b/transit/clipper/src/commonMain/kotlin/com/codebutler/farebot/transit/clipper/ClipperTrip.kt @@ -34,14 +34,14 @@ import com.codebutler.farebot.transit.Trip import kotlin.time.Instant class ClipperTrip( - private val timestamp: Long, - private val exitTimestampValue: Long, - private val balance: Long, - private val fareValue: Long, - private val agency: Long, - private val from: Long, - private val to: Long, - private val route: Long, + private val timestamp: Long = 0, + private val exitTimestampValue: Long = 0, + private val balance: Long = 0, + private val fareValue: Long = 0, + private val agency: Long = 0, + private val from: Long = 0, + private val to: Long = 0, + private val route: Long = 0, private val vehicleNum: Long = 0, private val transportCode: Long = 0, ) : Trip() { @@ -126,55 +126,4 @@ class ClipperTrip( vehicleNum, transportCode, ) - - companion object { - fun builder(): Builder = Builder() - } - - class Builder { - private var timestamp: Long = 0 - private var exitTimestamp: Long = 0 - private var balance: Long = 0 - private var fare: Long = 0 - private var agency: Long = 0 - private var from: Long = 0 - private var to: Long = 0 - private var route: Long = 0 - private var vehicleNum: Long = 0 - private var transportCode: Long = 0 - - fun timestamp(timestamp: Long): Builder = apply { this.timestamp = timestamp } - - fun exitTimestamp(exitTimestamp: Long): Builder = apply { this.exitTimestamp = exitTimestamp } - - fun balance(balance: Long): Builder = apply { this.balance = balance } - - fun fare(fare: Long): Builder = apply { this.fare = fare } - - fun agency(agency: Long): Builder = apply { this.agency = agency } - - fun from(from: Long): Builder = apply { this.from = from } - - fun to(to: Long): Builder = apply { this.to = to } - - fun route(route: Long): Builder = apply { this.route = route } - - fun vehicleNum(vehicleNum: Long): Builder = apply { this.vehicleNum = vehicleNum } - - fun transportCode(transportCode: Long): Builder = apply { this.transportCode = transportCode } - - fun build(): ClipperTrip = - ClipperTrip( - timestamp, - exitTimestamp, - balance, - fare, - agency, - from, - to, - route, - vehicleNum, - transportCode, - ) - } } diff --git a/transit/easycard/src/commonMain/kotlin/com/codebutler/farebot/transit/easycard/EasyCardTransitFactory.kt b/transit/easycard/src/commonMain/kotlin/com/codebutler/farebot/transit/easycard/EasyCardTransitFactory.kt index 34401b2d2..c97d790e2 100644 --- a/transit/easycard/src/commonMain/kotlin/com/codebutler/farebot/transit/easycard/EasyCardTransitFactory.kt +++ b/transit/easycard/src/commonMain/kotlin/com/codebutler/farebot/transit/easycard/EasyCardTransitFactory.kt @@ -112,15 +112,14 @@ class EasyCardTransitFactory : TransitFactory // Try MDST database first val result = MdstStationLookup.getStation(EASYCARD_STR, stationId) if (result != null) { - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } // Fallback to hardcoded map val name = EasyCardStations[stationId] diff --git a/transit/ezlink/src/commonMain/kotlin/com/codebutler/farebot/transit/ezlink/EZLinkData.kt b/transit/ezlink/src/commonMain/kotlin/com/codebutler/farebot/transit/ezlink/EZLinkData.kt index 95440c05c..e51265587 100644 --- a/transit/ezlink/src/commonMain/kotlin/com/codebutler/farebot/transit/ezlink/EZLinkData.kt +++ b/transit/ezlink/src/commonMain/kotlin/com/codebutler/farebot/transit/ezlink/EZLinkData.kt @@ -52,16 +52,15 @@ internal object EZLinkData { val result = MdstStationLookup.getStation(EZLINK_STR, stationId) if (result != null) { - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .code(code) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + humanReadableId = code, + ) } return Station.unknown(code) diff --git a/transit/hsl/src/commonMain/kotlin/com/codebutler/farebot/transit/hsl/HSLTransitInfo.kt b/transit/hsl/src/commonMain/kotlin/com/codebutler/farebot/transit/hsl/HSLTransitInfo.kt index b7245fb71..ddee3a615 100644 --- a/transit/hsl/src/commonMain/kotlin/com/codebutler/farebot/transit/hsl/HSLTransitInfo.kt +++ b/transit/hsl/src/commonMain/kotlin/com/codebutler/farebot/transit/hsl/HSLTransitInfo.kt @@ -25,6 +25,7 @@ package com.codebutler.farebot.transit.hsl import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.transit.Subscription import com.codebutler.farebot.transit.TransitBalance @@ -51,12 +52,33 @@ class HSLTransitInfo( get() = TransitBalance(balance = TransitCurrency.EUR(mBalance)) override suspend fun getAdvancedUi(): FareBotUiTree? { - val b = FareBotUiTree.builder() - applicationVersion?.let { b.item().title(Res.string.hsl_application_version).value(it) } - applicationKeyVersion?.let { b.item().title(Res.string.hsl_application_key_version).value(it) } - platformType?.let { b.item().title(Res.string.hsl_platform_type).value(it) } - securityLevel?.let { b.item().title(Res.string.hsl_security_level).value(it) } - val tree = b.build() + val tree = + uiTree { + applicationVersion?.let { + item { + title = Res.string.hsl_application_version + value = it + } + } + applicationKeyVersion?.let { + item { + title = Res.string.hsl_application_key_version + value = it + } + } + platformType?.let { + item { + title = Res.string.hsl_platform_type + value = it + } + } + securityLevel?.let { + item { + title = Res.string.hsl_security_level + value = it + } + } + } return if (tree.items.isEmpty()) null else tree } } diff --git a/transit/kmt/src/commonMain/kotlin/com/codebutler/farebot/transit/kmt/KMTData.kt b/transit/kmt/src/commonMain/kotlin/com/codebutler/farebot/transit/kmt/KMTData.kt index d4062fe59..f0809d1dd 100644 --- a/transit/kmt/src/commonMain/kotlin/com/codebutler/farebot/transit/kmt/KMTData.kt +++ b/transit/kmt/src/commonMain/kotlin/com/codebutler/farebot/transit/kmt/KMTData.kt @@ -92,15 +92,14 @@ internal object KMTData { // Try MDST database first val result = MdstStationLookup.getStation(KMT_STR, code) if (result != null) { - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } // Fallback to hardcoded map return KCI_STATIONS[code] diff --git a/transit/nextfareul/src/commonMain/kotlin/com/codebutler/farebot/transit/nextfareul/NextfareUltralightTransitData.kt b/transit/nextfareul/src/commonMain/kotlin/com/codebutler/farebot/transit/nextfareul/NextfareUltralightTransitData.kt index ad4e78dc0..a8a65eac5 100644 --- a/transit/nextfareul/src/commonMain/kotlin/com/codebutler/farebot/transit/nextfareul/NextfareUltralightTransitData.kt +++ b/transit/nextfareul/src/commonMain/kotlin/com/codebutler/farebot/transit/nextfareul/NextfareUltralightTransitData.kt @@ -25,6 +25,7 @@ package com.codebutler.farebot.transit.nextfareul import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.ListItem import com.codebutler.farebot.base.ui.ListItemInterface +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.base.util.Luhn import com.codebutler.farebot.base.util.NumberUtils @@ -102,11 +103,13 @@ abstract class NextfareUltralightTransitData : TransitInfo() { return items } - override suspend fun getAdvancedUi(): FareBotUiTree { - val b = FareBotUiTree.builder() - b.item().title(Res.string.nextfareul_machine_code).value(capsule.mMachineCode.toString(16)) - return b.build() - } + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = Res.string.nextfareul_machine_code + value = capsule.mMachineCode.toString(16) + } + } protected abstract fun makeCurrency(value: Int): TransitCurrency diff --git a/transit/octopus/src/commonMain/kotlin/com/codebutler/farebot/transit/octopus/OctopusTransitInfo.kt b/transit/octopus/src/commonMain/kotlin/com/codebutler/farebot/transit/octopus/OctopusTransitInfo.kt index ef7596872..0efff65d1 100644 --- a/transit/octopus/src/commonMain/kotlin/com/codebutler/farebot/transit/octopus/OctopusTransitInfo.kt +++ b/transit/octopus/src/commonMain/kotlin/com/codebutler/farebot/transit/octopus/OctopusTransitInfo.kt @@ -23,6 +23,7 @@ package com.codebutler.farebot.transit.octopus import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.transit.TransitBalance import com.codebutler.farebot.transit.TransitCurrency @@ -87,20 +88,16 @@ class OctopusTransitInfo( } override suspend fun getAdvancedUi(): FareBotUiTree? { - // Dual-mode card, show the CNY balance here. val szt = shenzhenBalance - if (hasOctopus && szt != null) { - val uiBuilder = FareBotUiTree.builder() - val apbUiBuilder = - uiBuilder - .item() - .title(Res.string.octopus_alternate_purse_balances) - apbUiBuilder.item( - Res.string.octopus_szt, - TransitCurrency.CNY(szt).formatCurrencyString(isBalance = true), - ) - return uiBuilder.build() + if (!hasOctopus || szt == null) return null + return uiTree { + item { + title = Res.string.octopus_alternate_purse_balances + item { + title = Res.string.octopus_szt + value = TransitCurrency.CNY(szt).formatCurrencyString(isBalance = true) + } + } } - return null } } diff --git a/transit/orca/src/commonMain/kotlin/com/codebutler/farebot/transit/orca/OrcaTransaction.kt b/transit/orca/src/commonMain/kotlin/com/codebutler/farebot/transit/orca/OrcaTransaction.kt index 685c674b0..c8a11edbf 100644 --- a/transit/orca/src/commonMain/kotlin/com/codebutler/farebot/transit/orca/OrcaTransaction.kt +++ b/transit/orca/src/commonMain/kotlin/com/codebutler/farebot/transit/orca/OrcaTransaction.kt @@ -257,15 +257,14 @@ class OrcaTransaction( stationId: Int, ): Station? { val result = MdstStationLookup.getStation(dbName, stationId) ?: return null - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } companion object { diff --git a/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipIndex.kt b/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipIndex.kt index 4f7617291..351a54fd7 100644 --- a/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipIndex.kt +++ b/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipIndex.kt @@ -24,6 +24,7 @@ package com.codebutler.farebot.transit.ovc import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.transit.en1545.getBitsFromBuffer import kotlinx.serialization.Serializable @@ -37,13 +38,23 @@ data class OVChipIndex internal constructor( val recentCreditSlot: Boolean, // Most recent credit index slot (0xF90(false) or 0xFA0(true)) val subscriptionIndex: List, ) { - fun addAdvancedItems(b: FareBotUiTree.Item.Builder) { - b.item("Transaction Slot", if (recentTransactionSlot) "B" else "A") - b.item("Info Slot", if (recentInfoSlot) "B" else "A") - b.item("Subscription Slot", if (recentSubscriptionSlot) "B" else "A") - b.item("Travelhistory Slot", if (recentTravelhistorySlot) "B" else "A") - b.item("Credit Slot", if (recentCreditSlot) "B" else "A") - } + fun advancedItems(): List = + listOf( + FareBotUiTree.Item( + title = FormattedString("Transaction Slot"), + value = if (recentTransactionSlot) "B" else "A", + ), + FareBotUiTree.Item(title = FormattedString("Info Slot"), value = if (recentInfoSlot) "B" else "A"), + FareBotUiTree.Item( + title = FormattedString("Subscription Slot"), + value = if (recentSubscriptionSlot) "B" else "A", + ), + FareBotUiTree.Item( + title = FormattedString("Travelhistory Slot"), + value = if (recentTravelhistorySlot) "B" else "A", + ), + FareBotUiTree.Item(title = FormattedString("Credit Slot"), value = if (recentCreditSlot) "B" else "A"), + ) companion object { fun parse(data: ByteArray): OVChipIndex { diff --git a/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipTransitInfo.kt b/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipTransitInfo.kt index c3bbb75a7..f6d759c90 100644 --- a/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipTransitInfo.kt +++ b/transit/ovc/src/commonMain/kotlin/com/codebutler/farebot/transit/ovc/OVChipTransitInfo.kt @@ -28,6 +28,7 @@ import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.HeaderListItem import com.codebutler.farebot.base.ui.ListItem import com.codebutler.farebot.base.ui.ListItemInterface +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.transit.Subscription import com.codebutler.farebot.transit.TransitBalance @@ -148,14 +149,21 @@ class OVChipTransitInfo( return li } - override suspend fun getAdvancedUi(): FareBotUiTree { - val b = FareBotUiTree.builder() - b.item().title("Credit Slot ID").value(creditSlotId.toString()) - b.item().title("Last Credit ID").value(creditId.toString()) - val slotsItem = b.item().title("Recent Slots") - index.addAdvancedItems(slotsItem) - return b.build() - } + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = "Credit Slot ID" + value = creditSlotId.toString() + } + item { + title = "Last Credit ID" + value = creditId.toString() + } + item { + title = "Recent Slots" + addChildren(index.advancedItems()) + } + } companion object { private val NAME = FormattedString("OV-chipkaart") diff --git a/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTopup.kt b/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTopup.kt index fd01887c1..465445af8 100644 --- a/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTopup.kt +++ b/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTopup.kt @@ -73,14 +73,13 @@ internal class PodorozhnikTopup( stationId: Int, ): Station? { val result = MdstStationLookup.getStation(dbName, stationId) ?: return null - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } } diff --git a/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTrip.kt b/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTrip.kt index 8dd68d7a5..cadde325c 100644 --- a/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTrip.kt +++ b/transit/podorozhnik/src/commonMain/kotlin/com/codebutler/farebot/transit/podorozhnik/PodorozhnikTrip.kt @@ -102,15 +102,14 @@ internal class PodorozhnikTrip( stationId: Int, ): Station? { val result = MdstStationLookup.getStation(dbName, stationId) ?: return null - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } companion object { diff --git a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTransitFactory.kt b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTransitFactory.kt index 805531c33..0f2a1d156 100644 --- a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTransitFactory.kt +++ b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTransitFactory.kt @@ -27,6 +27,7 @@ import com.codebutler.farebot.card.classic.ClassicCard import com.codebutler.farebot.card.classic.DataClassicSector import com.codebutler.farebot.transit.CardInfo import com.codebutler.farebot.transit.Refill +import com.codebutler.farebot.transit.Station import com.codebutler.farebot.transit.TransitFactory import com.codebutler.farebot.transit.TransitIdentity import com.codebutler.farebot.transit.TransitRegion @@ -36,6 +37,7 @@ import com.codebutler.farebot.transit.seqgo.record.SeqGoRecord import com.codebutler.farebot.transit.seqgo.record.SeqGoTapRecord import com.codebutler.farebot.transit.seqgo.record.SeqGoTopupRecord import farebot.transit.seqgo.generated.resources.* +import kotlin.time.Instant class SeqGoTransitFactory : TransitFactory { override val allCards: List @@ -111,26 +113,33 @@ class SeqGoTransitFactory : TransitFactory { var i = 0 while (sortedTaps.size > i) { val tapOn = sortedTaps[i] - val tripBuilder = SeqGoTrip.builder() - - tripBuilder.journeyId(tapOn.journey) - tripBuilder.startTime(tapOn.timestamp) - tripBuilder.startStationId(tapOn.station) - tripBuilder.startStation(SeqGoUtil.getStation(tapOn.station)) - tripBuilder.mode(tapOn.mode) + var endTime: Instant? = null + var endStationId = 0 + var endStation: Station? = null if (sortedTaps.size > i + 1 && sortedTaps[i + 1].journey == tapOn.journey && sortedTaps[i + 1].mode == tapOn.mode ) { val tapOff = sortedTaps[i + 1] - tripBuilder.endTime(tapOff.timestamp) - tripBuilder.endStationId(tapOff.station) - tripBuilder.endStation(SeqGoUtil.getStation(tapOff.station)) + endTime = tapOff.timestamp + endStationId = tapOff.station + endStation = SeqGoUtil.getStation(tapOff.station) i++ } - trips.add(tripBuilder.build()) + trips.add( + SeqGoTrip( + journeyId = tapOn.journey, + modeValue = tapOn.mode, + startTime = tapOn.timestamp, + endTime = endTime, + startStationId = tapOn.station, + endStationId = endStationId, + startStationValue = SeqGoUtil.getStation(tapOn.station), + endStationValue = endStation, + ), + ) i++ } diff --git a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTrip.kt b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTrip.kt index 18832ceca..49cba4dd2 100644 --- a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTrip.kt +++ b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoTrip.kt @@ -33,14 +33,14 @@ import kotlin.time.Instant * Represents trip events on Go Card. */ class SeqGoTrip( - private val journeyId: Int, - private val modeValue: Mode, - private val startTime: Instant?, - private val endTime: Instant?, - private val startStationId: Int, - private val endStationId: Int, - private val startStationValue: Station?, - private val endStationValue: Station?, + private val journeyId: Int = 0, + private val modeValue: Mode = Mode.OTHER, + private val startTime: Instant? = null, + private val endTime: Instant? = null, + private val startStationId: Int = 0, + private val endStationId: Int = 0, + private val startStationValue: Station? = null, + private val endStationValue: Station? = null, ) : Trip() { override val startTimestamp: Instant? get() = startTime @@ -74,47 +74,4 @@ class SeqGoTrip( fun getStartStationId(): Int = startStationId fun getEndStationId(): Int = endStationId - - class Builder { - private var journeyId: Int = 0 - private var mode: Mode = Mode.OTHER - private var startTime: Instant? = null - private var endTime: Instant? = null - private var startStationId: Int = 0 - private var endStationId: Int = 0 - private var startStation: Station? = null - private var endStation: Station? = null - - fun journeyId(journeyId: Int) = apply { this.journeyId = journeyId } - - fun mode(mode: Mode) = apply { this.mode = mode } - - fun startTime(startTime: Instant?) = apply { this.startTime = startTime } - - fun endTime(endTime: Instant?) = apply { this.endTime = endTime } - - fun startStationId(startStationId: Int) = apply { this.startStationId = startStationId } - - fun endStationId(endStationId: Int) = apply { this.endStationId = endStationId } - - fun startStation(station: Station?) = apply { this.startStation = station } - - fun endStation(station: Station?) = apply { this.endStation = station } - - fun build(): SeqGoTrip = - SeqGoTrip( - journeyId = journeyId, - modeValue = mode, - startTime = startTime, - endTime = endTime, - startStationId = startStationId, - endStationId = endStationId, - startStationValue = startStation, - endStationValue = endStation, - ) - } - - companion object { - fun builder(): Builder = Builder() - } } diff --git a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoUtil.kt b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoUtil.kt index e4080fb97..92c9f3487 100644 --- a/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoUtil.kt +++ b/transit/seqgo/src/commonMain/kotlin/com/codebutler/farebot/transit/seqgo/SeqGoUtil.kt @@ -85,11 +85,10 @@ object SeqGoUtil { val result = MdstStationLookup.getStation(SEQ_GO_STR, stationId) ?: return null - return Station - .builder() - .stationName(result.stationName) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } } diff --git a/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/HoloTransitInfo.kt b/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/HoloTransitInfo.kt index cbe18f3ad..f89dff611 100644 --- a/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/HoloTransitInfo.kt +++ b/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/HoloTransitInfo.kt @@ -13,6 +13,7 @@ package com.codebutler.farebot.transit.serialonly import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.ListItem import com.codebutler.farebot.base.ui.ListItemInterface +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import farebot.transit.serialonly.generated.resources.Res import farebot.transit.serialonly.generated.resources.last_transaction @@ -43,11 +44,13 @@ class HoloTransitInfo( ), ) - override suspend fun getAdvancedUi(): FareBotUiTree { - val b = FareBotUiTree.builder() - b.item().title(Res.string.manufacture_id).value(mManufacturingId) - return b.build() - } + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = Res.string.manufacture_id + value = mManufacturingId + } + } override val reason get() = Reason.NOT_STORED override val cardName get() = HoloTransitFactory.NAME diff --git a/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/StrelkaTransitInfo.kt b/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/StrelkaTransitInfo.kt index 6a1e577eb..61e4fdf87 100644 --- a/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/StrelkaTransitInfo.kt +++ b/transit/serialonly/src/commonMain/kotlin/com/codebutler/farebot/transit/serialonly/StrelkaTransitInfo.kt @@ -11,6 +11,7 @@ package com.codebutler.farebot.transit.serialonly import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import farebot.transit.serialonly.generated.resources.Res import farebot.transit.serialonly.generated.resources.card_name_strelka @@ -19,11 +20,13 @@ import farebot.transit.serialonly.generated.resources.strelka_long_serial class StrelkaTransitInfo( private val mSerial: String, ) : SerialOnlyTransitInfo() { - override suspend fun getAdvancedUi(): FareBotUiTree { - val b = FareBotUiTree.builder() - b.item().title(Res.string.strelka_long_serial).value(mSerial) - return b.build() - } + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = Res.string.strelka_long_serial + value = mSerial + } + } override val reason get() = Reason.MORE_RESEARCH_NEEDED override val serialNumber get() = StrelkaTransitFactory.formatShortSerial(mSerial) diff --git a/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTagRecord.kt b/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTagRecord.kt index 486939f89..924367282 100644 --- a/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTagRecord.kt +++ b/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTagRecord.kt @@ -122,15 +122,14 @@ class SmartRiderTagRecord( stationId: Int, ): Station? { val result = MdstStationLookup.getStation(dbName, stationId) ?: return null - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } companion object { diff --git a/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTransitInfo.kt b/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTransitInfo.kt index 34ce3d77a..c71c7ef67 100644 --- a/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTransitInfo.kt +++ b/transit/smartrider/src/commonMain/kotlin/com/codebutler/farebot/transit/smartrider/SmartRiderTransitInfo.kt @@ -23,6 +23,7 @@ package com.codebutler.farebot.transit.smartrider import com.codebutler.farebot.base.ui.FareBotUiTree +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.transit.Subscription import com.codebutler.farebot.transit.TransitBalance @@ -101,22 +102,23 @@ class SmartRiderTransitInfo( override val subscriptions: List? = null - override suspend fun getAdvancedUi(): FareBotUiTree? { - val uiBuilder = FareBotUiTree.builder() - uiBuilder - .item() - .title(Res.string.smartrider_ticket_type) - .value(mTokenType.toString()) - if (mSmartRiderType == SmartRiderType.SMARTRIDER) { - uiBuilder - .item() - .title(Res.string.smartrider_autoload_threshold) - .value(TransitCurrency.AUD(mAutoloadThreshold).formatCurrencyString(true)) - uiBuilder - .item() - .title(Res.string.smartrider_autoload_value) - .value(TransitCurrency.AUD(mAutoloadValue).formatCurrencyString(true)) + override suspend fun getAdvancedUi(): FareBotUiTree = + uiTree { + item { + title = Res.string.smartrider_ticket_type + value = mTokenType.toString() + } + if (mSmartRiderType == SmartRiderType.SMARTRIDER) { + item { + title = Res.string.smartrider_autoload_threshold + value = + TransitCurrency.AUD(mAutoloadThreshold).formatCurrencyString(true) + } + item { + title = Res.string.smartrider_autoload_value + value = + TransitCurrency.AUD(mAutoloadValue).formatCurrencyString(true) + } + } } - return uiBuilder.build() - } } diff --git a/transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt b/transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt index 01748f0b8..ba5dc0e4d 100644 --- a/transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt +++ b/transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt @@ -103,69 +103,5 @@ data class Station( latitude = latitude?.toFloatOrNull(), longitude = longitude?.toFloatOrNull(), ) - - fun builder(): Builder = Builder() - } - - class Builder { - private var stationName: String? = null - private var shortStationName: String? = null - private var companyName: String? = null - private var lineNames: List = emptyList() - private var latitude: String? = null - private var longitude: String? = null - private var code: String? = null - private var abbreviation: String? = null - - fun stationName(stationName: String?): Builder { - this.stationName = stationName - return this - } - - fun shortStationName(shortStationName: String?): Builder { - this.shortStationName = shortStationName - return this - } - - fun companyName(companyName: String?): Builder { - this.companyName = companyName - return this - } - - fun lineNames(lineNames: List): Builder { - this.lineNames = lineNames - return this - } - - fun latitude(latitude: String?): Builder { - this.latitude = latitude - return this - } - - fun longitude(longitude: String?): Builder { - this.longitude = longitude - return this - } - - fun code(code: String?): Builder { - this.code = code - return this - } - - fun abbreviation(abbreviation: String?): Builder { - this.abbreviation = abbreviation - return this - } - - fun build(): Station = - Station( - stationName = stationName, - shortStationName = shortStationName ?: abbreviation, - companyName = companyName, - lineNames = lineNames, - latitude = latitude?.toFloatOrNull(), - longitude = longitude?.toFloatOrNull(), - humanReadableId = code, - ) } } diff --git a/transit/suica/src/commonMain/kotlin/com/codebutler/farebot/transit/suica/SuicaUtil.kt b/transit/suica/src/commonMain/kotlin/com/codebutler/farebot/transit/suica/SuicaUtil.kt index 302aaf5bf..e73bca229 100644 --- a/transit/suica/src/commonMain/kotlin/com/codebutler/farebot/transit/suica/SuicaUtil.kt +++ b/transit/suica/src/commonMain/kotlin/com/codebutler/farebot/transit/suica/SuicaUtil.kt @@ -496,13 +496,12 @@ internal object SuicaUtil { val result = MdstStationLookup.getStation(SUICA_BUS_STR, stationId) if (result != null) { - return Station - .builder() - .companyName(result.companyName) - .stationName(result.stationName) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + companyName = result.companyName, + stationName = result.stationName, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } // Return unknown station with formatted ID @@ -526,14 +525,13 @@ internal object SuicaUtil { val result = MdstStationLookup.getStation(SUICA_RAIL_STR, stationId) if (result != null) { - return Station - .builder() - .companyName(result.companyName) - .lineNames(result.lineNames) - .stationName(result.stationName) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + companyName = result.companyName, + lineNames = result.lineNames, + stationName = result.stationName, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } // Return unknown station with formatted ID diff --git a/transit/tfi-leap/src/commonMain/kotlin/com/codebutler/farebot/transit/tfileap/LeapTrip.kt b/transit/tfi-leap/src/commonMain/kotlin/com/codebutler/farebot/transit/tfileap/LeapTrip.kt index 20e8c1841..8762c38c2 100644 --- a/transit/tfi-leap/src/commonMain/kotlin/com/codebutler/farebot/transit/tfileap/LeapTrip.kt +++ b/transit/tfi-leap/src/commonMain/kotlin/com/codebutler/farebot/transit/tfileap/LeapTrip.kt @@ -96,15 +96,14 @@ class LeapTrip internal constructor( private fun lookupStation(stationId: Int): Station? { val result = MdstStationLookup.getStation(LEAP_STR, stationId) ?: return null - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } companion object { diff --git a/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaHybridTransitInfo.kt b/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaHybridTransitInfo.kt index 6a184875e..03b1de5a9 100644 --- a/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaHybridTransitInfo.kt +++ b/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaHybridTransitInfo.kt @@ -113,12 +113,11 @@ class TroikaHybridTransitInfo( strelka?.getAdvancedUi(), ) if (trees.isEmpty()) return null - val b = FareBotUiTree.builder() - for (tree in trees) { - for (item in tree.items) { - b.item().title(item.title).value(item.value) - } - } - return b.build() + return FareBotUiTree( + items = + trees.flatMap { tree -> + tree.items.map { FareBotUiTree.Item(title = it.title, value = it.value) } + }, + ) } } diff --git a/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaUltralightTransitFactory.kt b/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaUltralightTransitFactory.kt index 8f2f2d45e..5c4011daf 100644 --- a/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaUltralightTransitFactory.kt +++ b/transit/troika/src/commonMain/kotlin/com/codebutler/farebot/transit/troika/TroikaUltralightTransitFactory.kt @@ -615,15 +615,14 @@ private class TroikaTrip( if (validator == null || validator == 0) return null val result = MdstStationLookup.getStation(TROIKA_STR, validator) if (result != null) { - return Station - .Builder() - .stationName(result.stationName) - .shortStationName(result.shortStationName) - .companyName(result.companyName) - .lineNames(result.lineNames) - .latitude(if (result.hasLocation) result.latitude.toString() else null) - .longitude(if (result.hasLocation) result.longitude.toString() else null) - .build() + return Station( + stationName = result.stationName, + shortStationName = result.shortStationName, + companyName = result.companyName, + lineNames = result.lineNames, + latitude = if (result.hasLocation) result.latitude else null, + longitude = if (result.hasLocation) result.longitude else null, + ) } return Station.unknown(validator.toString()) } diff --git a/transit/umarsh/src/commonMain/kotlin/com/codebutler/farebot/transit/umarsh/UmarshTransitInfo.kt b/transit/umarsh/src/commonMain/kotlin/com/codebutler/farebot/transit/umarsh/UmarshTransitInfo.kt index f46cb8fb3..dc1b3bbca 100644 --- a/transit/umarsh/src/commonMain/kotlin/com/codebutler/farebot/transit/umarsh/UmarshTransitInfo.kt +++ b/transit/umarsh/src/commonMain/kotlin/com/codebutler/farebot/transit/umarsh/UmarshTransitInfo.kt @@ -25,6 +25,7 @@ package com.codebutler.farebot.transit.umarsh import com.codebutler.farebot.base.ui.FareBotUiTree import com.codebutler.farebot.base.ui.ListItem import com.codebutler.farebot.base.ui.ListItemInterface +import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.util.FormattedString import com.codebutler.farebot.base.util.NumberUtils import com.codebutler.farebot.transit.Subscription @@ -82,11 +83,14 @@ class UmarshTransitInfo( override suspend fun getAdvancedUi(): FareBotUiTree? { val rubSectors = sectors.filter { it.denomination == UmarshDenomination.RUB } if (rubSectors.isEmpty()) return null - val b = FareBotUiTree.builder() - for (sec in rubSectors) { - b.item().title(Res.string.umarsh_machine_id).value(sec.machineId.toString()) + return uiTree { + for (sec in rubSectors) { + item { + title = Res.string.umarsh_machine_id + value = sec.machineId.toString() + } + } } - return b.build() } override val trips: List?