Skip to content

Commit 47545d3

Browse files
committed
fix: polish widgets foundation
- fix: simplify widget size carousel - fix: align widget screen backgrounds - fix: color widget config bars - fix: comment out small previews - fix: match widget config system bars - fix: scope widget preview sizes - fix: remove price widget duplicate symbol - fix: add widget release changelog - fix: complete os widget actions - fix: show wide widget previews
1 parent 2637a68 commit 47545d3

26 files changed

Lines changed: 210 additions & 133 deletions

app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class HeadlinesPreviewContentTest {
5858
// Verify settings and preview section
5959
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
6060
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
61-
composeTestRule.onNodeWithTag("headline_card_small").assertExists()
61+
composeTestRule.onNodeWithTag("headline_card_wide").assertExists()
6262

6363
// Verify buttons
6464
composeTestRule.onNodeWithTag("buttons_row").assertExists()
@@ -164,7 +164,7 @@ class HeadlinesPreviewContentTest {
164164
composeTestRule.onNodeWithTag("divider").assertExists()
165165
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
166166
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
167-
composeTestRule.onNodeWithTag("headline_card_small").assertExists()
167+
composeTestRule.onNodeWithTag("headline_card_wide").assertExists()
168168
composeTestRule.onNodeWithTag("buttons_row").assertExists()
169169
composeTestRule.onNodeWithTag("WidgetDelete").assertExists()
170170
composeTestRule.onNodeWithTag("WidgetSave").assertExists()

app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package to.bitkit.appwidget.config
22

3-
import android.app.Activity
43
import android.appwidget.AppWidgetManager
54
import android.content.Intent
65
import android.os.Bundle
@@ -21,6 +20,7 @@ import to.bitkit.appwidget.ui.price.PriceGlanceWidget
2120
import to.bitkit.appwidget.ui.weather.WeatherGlanceReceiver
2221
import to.bitkit.appwidget.ui.weather.WeatherGlanceWidget
2322
import to.bitkit.ui.theme.AppThemeSurface
23+
import to.bitkit.ui.utils.enableAppEdgeToEdge
2424
import to.bitkit.utils.Logger
2525

2626
@AndroidEntryPoint
@@ -35,6 +35,7 @@ class AppWidgetConfigActivity : ComponentActivity() {
3535

3636
override fun onCreate(savedInstanceState: Bundle?) {
3737
super.onCreate(savedInstanceState)
38+
enableAppEdgeToEdge()
3839

3940
val appWidgetId = intent?.extras?.getInt(
4041
AppWidgetManager.EXTRA_APPWIDGET_ID,
@@ -59,7 +60,9 @@ class AppWidgetConfigActivity : ComponentActivity() {
5960
onConfirm = {
6061
when (viewModel.uiState.value.type) {
6162
AppWidgetType.PRICE -> PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity)
62-
AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(this@AppWidgetConfigActivity)
63+
AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(
64+
this@AppWidgetConfigActivity,
65+
)
6366
AppWidgetType.BLOCKS -> BlocksGlanceWidget().updateAll(this@AppWidgetConfigActivity)
6467
AppWidgetType.FACTS -> Unit
6568
AppWidgetType.WEATHER -> WeatherGlanceWidget().updateAll(this@AppWidgetConfigActivity)
@@ -70,7 +73,7 @@ class AppWidgetConfigActivity : ComponentActivity() {
7073
AppWidgetManager.EXTRA_APPWIDGET_ID,
7174
appWidgetId,
7275
)
73-
setResult(Activity.RESULT_OK, result)
76+
setResult(RESULT_OK, result)
7477
finish()
7578
},
7679
onCancel = { finish() },

app/src/main/java/to/bitkit/appwidget/config/BlocksConfigContent.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package to.bitkit.appwidget.config
22

33
import androidx.annotation.DrawableRes
4-
import androidx.compose.foundation.background
54
import androidx.compose.foundation.layout.Arrangement
65
import androidx.compose.foundation.layout.Column
76
import androidx.compose.foundation.layout.Row
@@ -61,10 +60,7 @@ internal fun BlocksConfigContent(
6160
)
6261
}
6362

64-
ScreenColumn(
65-
noBackground = true,
66-
modifier = Modifier.background(Colors.Gray7)
67-
) {
63+
ScreenColumn {
6864
AppTopBar(
6965
titleText = stringResource(R.string.widgets__blocks__name),
7066
onBackClick = onCancel,

app/src/main/java/to/bitkit/appwidget/config/HeadlinesConfigContent.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package to.bitkit.appwidget.config
22

3-
import androidx.compose.foundation.background
43
import androidx.compose.foundation.layout.Arrangement
54
import androidx.compose.foundation.layout.Column
65
import androidx.compose.foundation.layout.Row
@@ -43,10 +42,7 @@ internal fun HeadlinesConfigContent(
4342
val prefs = state.headlinePreferences
4443
val previewArticle = state.previewArticle
4544

46-
ScreenColumn(
47-
noBackground = true,
48-
modifier = Modifier.background(Colors.Gray7)
49-
) {
45+
ScreenColumn {
5046
AppTopBar(
5147
titleText = stringResource(R.string.widgets__news__name),
5248
onBackClick = onCancel,

app/src/main/java/to/bitkit/appwidget/config/PriceConfigContent.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package to.bitkit.appwidget.config
22

3-
import androidx.compose.foundation.background
43
import androidx.compose.foundation.clickable
54
import androidx.compose.foundation.layout.Arrangement
65
import androidx.compose.foundation.layout.Column
@@ -44,10 +43,7 @@ internal fun PriceConfigContent(
4443
val prefs = state.pricePreferences
4544
val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD
4645

47-
ScreenColumn(
48-
noBackground = true,
49-
modifier = Modifier.background(Colors.Gray7)
50-
) {
46+
ScreenColumn {
5147
AppTopBar(
5248
titleText = stringResource(R.string.widgets__price__name),
5349
onBackClick = onCancel,

app/src/main/java/to/bitkit/appwidget/config/WeatherConfigContent.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package to.bitkit.appwidget.config
22

3-
import androidx.compose.foundation.background
43
import androidx.compose.foundation.layout.Arrangement
54
import androidx.compose.foundation.layout.Column
65
import androidx.compose.foundation.layout.Row
@@ -37,15 +36,11 @@ internal fun WeatherConfigContent(
3736
onReset: () -> Unit,
3837
onSave: () -> Unit,
3938
onCancel: () -> Unit,
40-
modifier: Modifier = Modifier,
4139
) {
4240
val prefs = state.weatherPreferences
4341
val weather = state.previewWeather
4442

45-
ScreenColumn(
46-
noBackground = true,
47-
modifier = modifier.background(Colors.Gray7)
48-
) {
43+
ScreenColumn {
4944
AppTopBar(
5045
titleText = stringResource(R.string.widgets__weather__name),
5146
onBackClick = onCancel,

app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package to.bitkit.appwidget.ui.facts
22

3+
import android.content.Intent
34
import androidx.compose.runtime.Composable
45
import androidx.compose.ui.unit.dp
56
import androidx.glance.GlanceModifier
67
import androidx.glance.Image
78
import androidx.glance.ImageProvider
89
import androidx.glance.LocalContext
910
import androidx.glance.LocalSize
11+
import androidx.glance.appwidget.action.actionStartActivity
1012
import androidx.glance.layout.Alignment
1113
import androidx.glance.layout.Box
1214
import androidx.glance.layout.fillMaxSize
@@ -18,6 +20,7 @@ import to.bitkit.appwidget.ui.components.CaptionB
1820
import to.bitkit.appwidget.ui.components.GlanceLayoutDimens
1921
import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold
2022
import to.bitkit.appwidget.ui.theme.GlanceTextStyles
23+
import to.bitkit.ui.MainActivity
2124

2225
private val BADGE_SIZE = 32.dp
2326
private val BADGE_RESERVED_END = 40.dp
@@ -28,8 +31,11 @@ fun FactsGlanceContent(
2831
fact: String?,
2932
) {
3033
val context = LocalContext.current
34+
val openAppIntent = Intent(context, MainActivity::class.java).apply {
35+
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
36+
}
3137

32-
GlanceWidgetScaffold {
38+
GlanceWidgetScaffold(onClick = actionStartActivity(openAppIntent)) {
3339
if (fact == null) {
3440
CaptionB(text = context.getString(R.string.appwidget__loading))
3541
return@GlanceWidgetScaffold

app/src/main/java/to/bitkit/data/widgets/PriceService.kt

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import to.bitkit.models.WidgetType
2222
import to.bitkit.utils.AppError
2323
import to.bitkit.utils.Logger
2424
import java.text.NumberFormat
25-
import java.util.Currency
2625
import java.util.Locale
2726
import javax.inject.Inject
2827
import javax.inject.Singleton
@@ -147,24 +146,15 @@ class PriceService @Inject constructor(
147146
)
148147
}
149148

150-
private fun formatPrice(pair: TradingPair, price: Double): String {
149+
private fun formatPrice(
150+
pair: TradingPair,
151+
price: Double,
152+
locale: Locale = Locale.getDefault(),
153+
): String {
151154
return runCatching {
152-
val currency = Currency.getInstance(pair.quote)
153-
val numberFormat = NumberFormat.getCurrencyInstance(Locale.US).apply {
154-
this.currency = currency
155-
maximumFractionDigits = when {
156-
price >= 1000 -> 0
157-
price >= 1 -> 2
158-
else -> 6
159-
}
160-
}
161-
162-
// Format and remove currency symbol, keeping only the number with formatting
163-
val formatted = numberFormat.format(price)
164-
val currencySymbol = currency.symbol
165-
formatted.replace(currencySymbol, "").trim()
155+
formatPriceValue(price = price, locale = locale)
166156
}.onFailure {
167-
Logger.warn("Error formatting price for ${pair.displayName}", e = it, context = TAG)
157+
Logger.warn("Failed to format price for '${pair.displayName}'", it, context = TAG)
168158
}.getOrDefault(String.format(Locale.US, "%.2f", price))
169159
}
170160

@@ -180,3 +170,25 @@ sealed class PriceError(message: String) : AppError(message) {
180170
class InvalidResponse(override val message: String) : PriceError(message)
181171
class NetworkError(override val message: String) : PriceError(message)
182172
}
173+
174+
private const val GROUPED_PRICE_THRESHOLD = 1_000.0
175+
private const val STANDARD_PRICE_THRESHOLD = 1.0
176+
private const val GROUPED_PRICE_DECIMALS = 0
177+
private const val STANDARD_PRICE_DECIMALS = 2
178+
private const val SMALL_PRICE_DECIMALS = 6
179+
private const val MIN_PRICE_DECIMALS = 0
180+
181+
internal fun formatPriceValue(
182+
price: Double,
183+
locale: Locale = Locale.getDefault(),
184+
): String {
185+
return NumberFormat.getNumberInstance(locale).apply {
186+
maximumFractionDigits = when {
187+
price >= GROUPED_PRICE_THRESHOLD -> GROUPED_PRICE_DECIMALS
188+
price >= STANDARD_PRICE_THRESHOLD -> STANDARD_PRICE_DECIMALS
189+
else -> SMALL_PRICE_DECIMALS
190+
}
191+
minimumFractionDigits = MIN_PRICE_DECIMALS
192+
isGroupingUsed = true
193+
}.format(price)
194+
}

app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.ui.screens.wallets
33
import androidx.compose.animation.AnimatedVisibility
44
import androidx.compose.animation.core.AnimationConstants
55
import androidx.compose.animation.core.Spring
6+
import androidx.compose.animation.core.animateFloatAsState
67
import androidx.compose.animation.core.exponentialDecay
78
import androidx.compose.animation.core.spring
89
import androidx.compose.animation.core.tween
@@ -659,8 +660,19 @@ private fun WidgetsPage(
659660
onMoveWidget: (Int, Int) -> Unit,
660661
) {
661662
val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding()
663+
val widgetsScrollState = rememberScrollState()
664+
val calculatorIndex = homeUiState.widgetsWithPosition.indexOfFirst { it.type == WidgetType.CALCULATOR }
665+
val shouldAnchorCalculatorNumpad = isCalculatorInputActive && calculatorIndex == 0
662666
var pageBounds by remember { mutableStateOf<Rect?>(null) }
663667
var calculatorBounds by remember { mutableStateOf<Rect?>(null) }
668+
val scrollModifier = if (shouldAnchorCalculatorNumpad) {
669+
Modifier
670+
} else {
671+
Modifier.verticalScroll(
672+
state = widgetsScrollState,
673+
enabled = !isCalculatorInputActive,
674+
)
675+
}
664676

665677
LaunchedEffect(homeUiState.widgetsWithPosition, homeUiState.isEditingWidgets) {
666678
val hasCalculator = homeUiState.widgetsWithPosition.any { it.type == WidgetType.CALCULATOR }
@@ -684,10 +696,7 @@ private fun WidgetsPage(
684696
modifier = Modifier
685697
.padding(horizontal = 16.dp)
686698
.fillMaxSize()
687-
.verticalScroll(
688-
state = rememberScrollState(),
689-
enabled = !isCalculatorInputActive,
690-
)
699+
.then(scrollModifier)
691700
) {
692701
StatusBarSpacer()
693702
TopBarSpacer()
@@ -713,22 +722,31 @@ private fun WidgetsPage(
713722
Widgets(
714723
homeUiState = homeUiState,
715724
calculatorInputDismissKey = calculatorInputDismissKey,
725+
isCalculatorInputActive = isCalculatorInputActive,
726+
shouldAnchorCalculatorNumpad = shouldAnchorCalculatorNumpad,
716727
onCalculatorInputActiveChanged = onCalculatorInputActiveChanged,
717728
onCalculatorBoundsChanged = { calculatorBounds = it },
718729
onRemoveSuggestion = onRemoveSuggestion,
719730
onClickSuggestion = onClickSuggestion,
731+
modifier = if (shouldAnchorCalculatorNumpad) {
732+
Modifier.weight(1f)
733+
} else {
734+
Modifier.fillMaxWidth()
735+
}
720736
)
721737
}
722738

723-
VerticalSpacer(16.dp)
739+
if (!isCalculatorInputActive) {
740+
VerticalSpacer(16.dp)
724741

725-
TertiaryButton(
726-
text = stringResource(R.string.widgets__add),
727-
onClick = onClickAddWidget,
728-
modifier = Modifier.testTag("WidgetsAdd")
729-
)
742+
TertiaryButton(
743+
text = stringResource(R.string.widgets__add),
744+
onClick = onClickAddWidget,
745+
modifier = Modifier.testTag("WidgetsAdd")
746+
)
730747

731-
VerticalSpacer(150.dp + imeBottomPadding)
748+
VerticalSpacer(150.dp + imeBottomPadding)
749+
}
732750
}
733751
}
734752
}
@@ -836,16 +854,38 @@ private fun WidgetsOnboardingHint(modifier: Modifier = Modifier) {
836854
private fun Widgets(
837855
homeUiState: HomeUiState,
838856
calculatorInputDismissKey: Int,
857+
isCalculatorInputActive: Boolean,
858+
shouldAnchorCalculatorNumpad: Boolean,
839859
onCalculatorInputActiveChanged: (Boolean) -> Unit,
840860
onCalculatorBoundsChanged: (Rect) -> Unit,
841861
onRemoveSuggestion: (Suggestion) -> Unit,
842862
onClickSuggestion: (Suggestion) -> Unit,
863+
modifier: Modifier = Modifier,
843864
) {
865+
val calculatorTopWeight by animateFloatAsState(
866+
targetValue = if (shouldAnchorCalculatorNumpad) 1f else 0f,
867+
label = "calculatorTopWeight",
868+
)
869+
val widgets = if (isCalculatorInputActive) {
870+
val calculatorIndex = homeUiState.widgetsWithPosition.indexOfFirst { it.type == WidgetType.CALCULATOR }
871+
if (calculatorIndex == -1) {
872+
homeUiState.widgetsWithPosition
873+
} else {
874+
homeUiState.widgetsWithPosition.take(calculatorIndex + 1)
875+
}
876+
} else {
877+
homeUiState.widgetsWithPosition
878+
}
879+
844880
Column(
845-
modifier = Modifier.fillMaxWidth(),
846-
verticalArrangement = Arrangement.spacedBy(16.dp)
881+
modifier = modifier.fillMaxWidth(),
882+
verticalArrangement = if (calculatorTopWeight > 0f) {
883+
Arrangement.Top
884+
} else {
885+
Arrangement.spacedBy(16.dp)
886+
}
847887
) {
848-
homeUiState.widgetsWithPosition.forEach { widgetsWithPosition ->
888+
widgets.forEach { widgetsWithPosition ->
849889
when (widgetsWithPosition.type) {
850890
WidgetType.BLOCK -> {
851891
homeUiState.currentBlock?.run {
@@ -872,6 +912,10 @@ private fun Widgets(
872912
}
873913

874914
WidgetType.CALCULATOR -> {
915+
if (calculatorTopWeight > 0f) {
916+
FillHeight(weight = calculatorTopWeight)
917+
}
918+
875919
CalculatorCard(
876920
dismissNumberPadKey = calculatorInputDismissKey,
877921
onInputActiveChange = onCalculatorInputActiveChanged,

0 commit comments

Comments
 (0)