Skip to content

Commit c66670d

Browse files
authored
Merge pull request #985 from synonymdev/feat/in-app-v61
feat: v61 compact widgets support
2 parents 16d6f1f + 3d22482 commit c66670d

42 files changed

Lines changed: 1769 additions & 920 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/androidTest/java/to/bitkit/ui/screens/widgets/calculator/CalculatorCardIntegrationTest.kt renamed to app/src/androidTest/java/to/bitkit/ui/screens/widgets/calculator/CalculatorWidgetInputTest.kt

Lines changed: 67 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package to.bitkit.ui.screens.widgets.calculator
22

3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.fillMaxSize
35
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.runtime.collectAsState
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.ui.Alignment
49
import androidx.compose.ui.Modifier
5-
import androidx.compose.ui.test.assertTextContains
6-
import androidx.compose.ui.test.assertIsDisplayed
7-
import androidx.compose.ui.test.hasSetTextAction
8-
import androidx.compose.ui.test.hasText
910
import androidx.compose.ui.test.junit4.createComposeRule
10-
import androidx.compose.ui.test.onRoot
11-
import androidx.compose.ui.test.performTextClearance
12-
import androidx.compose.ui.test.performTextInput
13-
import androidx.compose.ui.test.printToString
11+
import androidx.compose.ui.test.onAllNodesWithTag
12+
import androidx.compose.ui.test.onNodeWithTag
13+
import androidx.compose.ui.test.performClick
1414
import androidx.lifecycle.ViewModel
1515
import androidx.lifecycle.ViewModelProvider
1616
import androidx.lifecycle.ViewModelStore
@@ -50,6 +50,7 @@ import to.bitkit.test.annotations.CalculatorWidget
5050
import to.bitkit.test.annotations.DeviceIntegration
5151
import to.bitkit.test.annotations.DeviceUiIntegration
5252
import to.bitkit.ui.screens.widgets.calculator.components.CalculatorCard
53+
import to.bitkit.ui.screens.widgets.calculator.components.CalculatorNumberPadBar
5354
import to.bitkit.ui.theme.AppThemeSurface
5455
import java.util.Locale
5556
import javax.inject.Inject
@@ -62,7 +63,7 @@ import kotlin.test.assertEquals
6263
@CalculatorWidget
6364
@DeviceIntegration
6465
@DeviceUiIntegration
65-
class CalculatorCardIntegrationTest {
66+
class CalculatorWidgetInputTest {
6667

6768
@get:Rule
6869
val hiltRule = HiltAndroidRule(this)
@@ -154,41 +155,27 @@ class CalculatorCardIntegrationTest {
154155
}
155156

156157
@Test
157-
fun btcInputUpdatesFiatValueAndPersistsWidgetState() {
158-
setCalculatorCard()
158+
fun btcInputViaNumberPadUpdatesFiatAndPersistsWidgetState() {
159+
setCalculatorWidget()
159160

160-
replaceInput(BTC_INPUT_INDEX, "12340")
161+
composeTestRule.onNodeWithTag(BTC_INPUT_TAG).performClick()
162+
awaitNumberPad()
163+
tapKeys("N1", "N2", "N3", "N4", "N0")
161164

162-
waitForValues(
163-
btcValue = "12340",
164-
fiatValue = "12.34",
165-
)
166-
167-
assertInputText(BTC_INPUT_INDEX, "12 340")
168-
assertInputText(FIAT_INPUT_INDEX, "12.34")
169-
assertPersistedValues(
170-
btcValue = "12340",
171-
fiatValue = "12.34",
172-
)
165+
waitForValues(btcValue = "12340", fiatValue = "12.34")
166+
assertPersistedValues(btcValue = "12340", fiatValue = "12.34")
173167
}
174168

175169
@Test
176-
fun fiatInputUpdatesBtcValueAndPersistsWidgetState() {
177-
setCalculatorCard()
170+
fun fiatInputViaNumberPadUpdatesBtcAndPersistsWidgetState() {
171+
setCalculatorWidget()
178172

179-
replaceInput(FIAT_INPUT_INDEX, "10.00")
173+
composeTestRule.onNodeWithTag(FIAT_INPUT_TAG).performClick()
174+
awaitNumberPad()
175+
tapKeys("N1", "N0", "NDecimal", "N0", "N0")
180176

181-
waitForValues(
182-
btcValue = "10000",
183-
fiatValue = "10.00",
184-
)
185-
186-
assertInputText(BTC_INPUT_INDEX, "10 000")
187-
assertInputText(FIAT_INPUT_INDEX, "10.00")
188-
assertPersistedValues(
189-
btcValue = "10000",
190-
fiatValue = "10.00",
191-
)
177+
waitForValues(btcValue = "10000", fiatValue = "10.00")
178+
assertPersistedValues(btcValue = "10000", fiatValue = "10.00")
192179
}
193180

194181
private fun createCalculatorViewModel(): CalculatorViewModel {
@@ -206,80 +193,70 @@ class CalculatorCardIntegrationTest {
206193
)[CalculatorViewModel::class.java]
207194
}
208195

209-
private fun setCalculatorCard() {
196+
private fun setCalculatorWidget() {
210197
composeTestRule.setContent {
211198
AppThemeSurface {
212-
CalculatorCard(
213-
calculatorViewModel = calculatorViewModel,
214-
modifier = Modifier.fillMaxWidth()
215-
)
199+
val state by calculatorViewModel.uiState.collectAsState()
200+
Box(modifier = Modifier.fillMaxSize()) {
201+
CalculatorCard(
202+
btcPrimaryDisplayUnit = state.displayUnit,
203+
btcValue = state.btcValue,
204+
fiatSymbol = state.currencySymbol,
205+
fiatName = state.selectedCurrency,
206+
fiatValue = state.fiatValue,
207+
activeInput = state.activeInput,
208+
onSelectInput = calculatorViewModel::onInputSelected,
209+
modifier = Modifier.fillMaxWidth()
210+
)
211+
state.activeInput?.let { active ->
212+
CalculatorNumberPadBar(
213+
activeInput = active,
214+
btcValue = state.btcValue,
215+
fiatValue = state.fiatValue,
216+
btcPrimaryDisplayUnit = state.displayUnit,
217+
onBtcChange = calculatorViewModel::onBtcInputChanged,
218+
onFiatChange = calculatorViewModel::onFiatInputChanged,
219+
modifier = Modifier.align(Alignment.BottomCenter)
220+
)
221+
}
222+
}
216223
}
217224
}
218225
composeTestRule.waitForIdle()
226+
}
227+
228+
private fun awaitNumberPad() {
219229
composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) {
220-
composeTestRule.onAllNodes(hasSetTextAction()).fetchSemanticsNodes().size == INPUT_COUNT
230+
composeTestRule.onAllNodesWithTag(NUMBER_PAD_TAG).fetchSemanticsNodes().isNotEmpty()
221231
}
222232
}
223233

224-
private fun inputAt(index: Int) = composeTestRule.onAllNodes(hasSetTextAction())[index]
225-
226-
private fun replaceInput(
227-
index: Int,
228-
text: String,
229-
) {
230-
inputAt(index).performTextClearance()
231-
inputAt(index).performTextInput(text)
234+
private fun tapKeys(vararg keys: String) {
235+
keys.forEach { key ->
236+
composeTestRule.onNodeWithTag(key).performClick()
237+
composeTestRule.waitForIdle()
238+
}
232239
}
233240

234241
private fun waitForValues(
235242
btcValue: String,
236243
fiatValue: String,
237244
) {
238-
runCatching {
239-
composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) {
240-
calculatorViewModel.uiState.value.btcValue == btcValue &&
241-
calculatorViewModel.uiState.value.fiatValue == fiatValue
242-
}
243-
}.onFailure {
244-
throw AssertionError(
245-
buildString {
246-
append("Expected calculatorValues btcValue='$btcValue', fiatValue='$fiatValue', ")
247-
append("but was '${calculatorViewModel.uiState.value}'. Persisted values were ")
248-
append("'${widgetsRepo.widgetsDataFlow.value.calculatorValues}'. Semantics tree:\n")
249-
append(composeTestRule.onRoot(useUnmergedTree = true).printToString())
250-
},
251-
it,
252-
)
245+
composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) {
246+
calculatorViewModel.uiState.value.btcValue == btcValue &&
247+
calculatorViewModel.uiState.value.fiatValue == fiatValue
253248
}
254-
255249
val expectedValues = CalculatorValues(
256250
btcValue = btcValue,
257251
fiatValue = fiatValue,
258252
satsValue = btcValue.toLong(),
259253
displayUnit = BitcoinDisplayUnit.MODERN,
260254
)
261-
runCatching {
262-
composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) {
263-
widgetsRepo.widgetsDataFlow.value.calculatorValues == expectedValues
264-
}
265-
}.onFailure {
266-
throw AssertionError(
267-
"Expected persisted values '$expectedValues', but was " +
268-
"'${widgetsRepo.widgetsDataFlow.value.calculatorValues}'",
269-
it,
270-
)
255+
composeTestRule.waitUntil(timeoutMillis = TIMEOUT_MS) {
256+
widgetsRepo.widgetsDataFlow.value.calculatorValues == expectedValues
271257
}
272258
}
273259

274-
private fun assertInputText(
275-
inputIndex: Int,
276-
text: String,
277-
) {
278-
inputAt(inputIndex).assertTextContains(text, substring = true)
279-
composeTestRule.onNode(hasText(text, substring = true), useUnmergedTree = true)
280-
.assertIsDisplayed()
281-
}
282-
283260
private fun assertPersistedValues(
284261
btcValue: String,
285262
fiatValue: String,
@@ -296,9 +273,9 @@ class CalculatorCardIntegrationTest {
296273
}
297274

298275
companion object {
299-
private const val BTC_INPUT_INDEX = 0
300-
private const val FIAT_INPUT_INDEX = 1
301-
private const val INPUT_COUNT = 2
276+
private const val BTC_INPUT_TAG = "CalculatorBtcInput"
277+
private const val FIAT_INPUT_TAG = "CalculatorFiatInput"
278+
private const val NUMBER_PAD_TAG = "CalculatorNumberPad"
302279
private const val TIMEOUT_MS = 5_000L
303280
private const val TEST_CREATED_AT = 0L
304281
private const val TEST_USD_RATE = "100000"
@@ -330,6 +307,7 @@ class CalculatorCardIntegrationTest {
330307

331308
@Provides
332309
@Named("enablePolling")
310+
@Suppress("FunctionOnlyReturningConstant")
333311
fun provideEnablePolling(): Boolean = false
334312
}
335313
}

app/src/main/java/to/bitkit/data/WidgetsStore.kt

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import to.bitkit.data.dto.BlockDTO
1313
import to.bitkit.data.dto.WeatherDTO
1414
import to.bitkit.data.dto.price.PriceDTO
1515
import to.bitkit.data.serializers.WidgetsSerializer
16+
import to.bitkit.models.WidgetSize
1617
import to.bitkit.models.WidgetType
1718
import to.bitkit.models.WidgetWithPosition
1819
import to.bitkit.models.WidgetsBackupV1
@@ -123,15 +124,28 @@ class WidgetsStore @Inject constructor(
123124
Logger.info("Deleted all widgets data.")
124125
}
125126

126-
suspend fun addWidget(type: WidgetType) {
127-
if (store.data.first().widgets.map { it.type }.contains(type)) return
127+
suspend fun addWidget(type: WidgetType, size: WidgetSize = WidgetSize.default(type)) {
128+
store.updateData { data ->
129+
val existing = data.widgets.firstOrNull { it.type == type }
130+
if (existing != null) {
131+
data.copy(
132+
widgets = data.widgets
133+
.map { if (it.type == type) it.copy(size = size) else it }
134+
.sortedBy { it.position }
135+
)
136+
} else {
137+
val nextPosition = (data.widgets.maxOfOrNull { it.position } ?: -1) + 1
138+
data.copy(
139+
widgets = (data.widgets + WidgetWithPosition(type = type, position = nextPosition, size = size))
140+
.sortedBy { it.position }
141+
)
142+
}
143+
}
144+
}
128145

146+
suspend fun updateWidgetSize(type: WidgetType, size: WidgetSize) {
129147
store.updateData { data ->
130-
val nextPosition = (data.widgets.maxOfOrNull { it.position } ?: -1) + 1
131-
data.copy(
132-
widgets = (data.widgets + WidgetWithPosition(type = type, position = nextPosition))
133-
.sortedBy { it.position }
134-
)
148+
data.copy(widgets = data.widgets.map { if (it.type == type) it.copy(size = size) else it })
135149
}
136150
}
137151

@@ -161,9 +175,13 @@ class WidgetsStore @Inject constructor(
161175
@Serializable
162176
data class WidgetsData(
163177
val widgets: List<WidgetWithPosition> = listOf(
164-
WidgetWithPosition(type = WidgetType.SUGGESTIONS, position = 0),
165-
WidgetWithPosition(type = WidgetType.PRICE, position = 1),
166-
WidgetWithPosition(type = WidgetType.BLOCK, position = 2),
178+
WidgetWithPosition(type = WidgetType.SUGGESTIONS, position = 0, size = WidgetSize.WIDE),
179+
WidgetWithPosition(type = WidgetType.PRICE, position = 1, size = WidgetSize.WIDE),
180+
WidgetWithPosition(type = WidgetType.BLOCK, position = 2, size = WidgetSize.SMALL),
181+
WidgetWithPosition(type = WidgetType.FACTS, position = 3, size = WidgetSize.SMALL),
182+
WidgetWithPosition(type = WidgetType.WEATHER, position = 4, size = WidgetSize.SMALL),
183+
WidgetWithPosition(type = WidgetType.CALCULATOR, position = 5, size = WidgetSize.SMALL),
184+
WidgetWithPosition(type = WidgetType.NEWS, position = 6, size = WidgetSize.WIDE),
167185
),
168186
val headlinePreferences: HeadlinePreferences = HeadlinePreferences(),
169187
val blocksPreferences: BlocksPreferences = BlocksPreferences(),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package to.bitkit.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
enum class WidgetSize {
8+
@SerialName("small")
9+
SMALL,
10+
11+
@SerialName("wide")
12+
WIDE;
13+
14+
companion object {
15+
fun default(type: WidgetType): WidgetSize = when (type) {
16+
WidgetType.PRICE,
17+
WidgetType.NEWS,
18+
WidgetType.SUGGESTIONS,
19+
-> WIDE
20+
21+
else -> SMALL
22+
}
23+
}
24+
}

app/src/main/java/to/bitkit/models/WidgetWithPosition.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ import kotlinx.serialization.Serializable
88
data class WidgetWithPosition(
99
val type: WidgetType,
1010
val position: Int = 0,
11+
val size: WidgetSize = WidgetSize.WIDE,
1112
)
13+
14+
fun WidgetWithPosition.effectiveSize(): WidgetSize =
15+
if (type == WidgetType.SUGGESTIONS) WidgetSize.WIDE else size

app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import to.bitkit.data.widgets.PriceService
3333
import to.bitkit.data.widgets.WeatherService
3434
import to.bitkit.data.widgets.WidgetService
3535
import to.bitkit.di.BgDispatcher
36+
import to.bitkit.models.WidgetSize
3637
import to.bitkit.models.WidgetType
3738
import to.bitkit.models.WidgetWithPosition
3839
import to.bitkit.models.widget.BlocksPreferences
@@ -173,7 +174,11 @@ class WidgetsRepo @Inject constructor(
173174
Logger.verbose("Stopped refresh coroutine for $widgetType", context = TAG)
174175
}
175176

176-
suspend fun addWidget(type: WidgetType) = withContext(bgDispatcher) { widgetsStore.addWidget(type) }
177+
suspend fun addWidget(type: WidgetType, size: WidgetSize = WidgetSize.default(type)) =
178+
withContext(bgDispatcher) { widgetsStore.addWidget(type, size) }
179+
180+
suspend fun updateWidgetSize(type: WidgetType, size: WidgetSize) =
181+
withContext(bgDispatcher) { widgetsStore.updateWidgetSize(type, size) }
177182

178183
suspend fun deleteWidget(type: WidgetType) = withContext(bgDispatcher) { widgetsStore.deleteWidget(type) }
179184

0 commit comments

Comments
 (0)