Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class HeadlinesPreviewContentTest {
// Verify settings and preview section
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
composeTestRule.onNodeWithTag("headline_card_small").assertExists()
composeTestRule.onNodeWithTag("headline_card_wide").assertExists()

// Verify buttons
composeTestRule.onNodeWithTag("buttons_row").assertExists()
Expand Down Expand Up @@ -164,7 +164,7 @@ class HeadlinesPreviewContentTest {
composeTestRule.onNodeWithTag("divider").assertExists()
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
composeTestRule.onNodeWithTag("headline_card_small").assertExists()
composeTestRule.onNodeWithTag("headline_card_wide").assertExists()
composeTestRule.onNodeWithTag("buttons_row").assertExists()
composeTestRule.onNodeWithTag("WidgetDelete").assertExists()
composeTestRule.onNodeWithTag("WidgetSave").assertExists()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import to.bitkit.data.dto.WeatherDTO
import to.bitkit.data.dto.price.GraphPeriod
import to.bitkit.data.dto.price.PriceDTO
import to.bitkit.data.serializers.AppWidgetDataSerializer
import to.bitkit.repositories.CurrencyRepo
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -32,6 +33,7 @@ private val Context.appWidgetDataStore: DataStore<AppWidgetData> by dataStore(
interface AppWidgetEntryPoint {
fun appWidgetPreferencesStore(): AppWidgetPreferencesStore
fun appWidgetDataRepository(): AppWidgetDataRepository
fun currencyRepo(): CurrencyRepo
}

@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package to.bitkit.appwidget.config

import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
Expand All @@ -20,6 +19,7 @@ import to.bitkit.appwidget.ui.price.PriceGlanceWidget
import to.bitkit.appwidget.ui.weather.WeatherGlanceReceiver
import to.bitkit.appwidget.ui.weather.WeatherGlanceWidget
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.utils.enableAppEdgeToEdge
import to.bitkit.utils.Logger

@AndroidEntryPoint
Expand All @@ -34,6 +34,7 @@ class AppWidgetConfigActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableAppEdgeToEdge()

val appWidgetId = intent?.extras?.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
Expand All @@ -58,7 +59,9 @@ class AppWidgetConfigActivity : ComponentActivity() {
onConfirm = {
when (viewModel.uiState.value.type) {
AppWidgetType.PRICE -> PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity)
AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(this@AppWidgetConfigActivity)
AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(
this@AppWidgetConfigActivity,
)
AppWidgetType.BLOCKS -> BlocksGlanceWidget().updateAll(this@AppWidgetConfigActivity)
AppWidgetType.FACTS -> Unit
AppWidgetType.WEATHER -> WeatherGlanceWidget().updateAll(this@AppWidgetConfigActivity)
Expand All @@ -68,7 +71,7 @@ class AppWidgetConfigActivity : ComponentActivity() {
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId,
)
setResult(Activity.RESULT_OK, result)
setResult(RESULT_OK, result)
finish()
},
onCancel = { finish() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import to.bitkit.appwidget.model.HomeHeadlinePreferences
import to.bitkit.appwidget.model.HomePricePreferences
import to.bitkit.appwidget.model.HomeWeatherPreferences
import to.bitkit.data.dto.FeeCondition
import to.bitkit.data.dto.WeatherDTO
import to.bitkit.data.dto.price.GraphPeriod
import to.bitkit.data.dto.price.TradingPair
import to.bitkit.models.widget.ArticleModel
Expand All @@ -29,6 +30,7 @@ import to.bitkit.models.widget.PricePreferences
import to.bitkit.models.widget.WeatherDataOption
import to.bitkit.models.widget.WeatherPreferences
import to.bitkit.models.widget.toArticleModel
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.ui.screens.widgets.blocks.WeatherModel
import to.bitkit.ui.screens.widgets.blocks.toWeatherModel
import to.bitkit.utils.Logger
Expand All @@ -39,6 +41,7 @@ import javax.inject.Inject
class AppWidgetConfigViewModel @Inject constructor(
private val preferencesStore: AppWidgetPreferencesStore,
private val dataRepository: AppWidgetDataRepository,
private val currencyRepo: CurrencyRepo,
) : ViewModel() {

companion object {
Expand All @@ -53,7 +56,7 @@ class AppWidgetConfigViewModel @Inject constructor(
val entry = preferencesStore.getEntry(appWidgetId)
val data = preferencesStore.data.first()
val previewArticle = data.cachedArticles.randomOrNull()?.toArticleModel() ?: DEFAULT_PREVIEW_ARTICLE
val previewWeather = data.cachedWeather?.toWeatherModel() ?: DEFAULT_PREVIEW_WEATHER
val previewWeather = data.cachedWeather?.toPreviewWeather() ?: DEFAULT_PREVIEW_WEATHER

_uiState.update {
it.copy(
Expand All @@ -72,7 +75,7 @@ class AppWidgetConfigViewModel @Inject constructor(
dataRepository.fetchWeather()
.onSuccess { fetched ->
preferencesStore.cacheWeather(fetched)
_uiState.update { it.copy(previewWeather = fetched.toWeatherModel()) }
_uiState.update { it.copy(previewWeather = fetched.toPreviewWeather()) }
}
.onFailure { Logger.warn("Failed to fetch weather for config preview", it, context = TAG) }
}
Expand Down Expand Up @@ -239,6 +242,10 @@ class AppWidgetConfigViewModel @Inject constructor(
.onSuccess { preferencesStore.cacheWeather(it) }
.onFailure { Logger.warn("Failed to fetch initial weather", it, context = TAG) }
}

private fun WeatherDTO.toPreviewWeather() = toWeatherModel(
currentFee = currencyRepo.formatSatsAsFiatWithSymbol(avgFeeSats, withSpace = true) ?: currentFee,
)
}

private val DEFAULT_PREVIEW_ARTICLE = ArticleModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package to.bitkit.appwidget.config

import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -61,10 +60,7 @@ internal fun BlocksConfigContent(
)
}

ScreenColumn(
noBackground = true,
modifier = Modifier.background(Colors.Gray7)
) {
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.widgets__blocks__name),
onBackClick = onCancel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package to.bitkit.appwidget.config

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -43,10 +42,7 @@ internal fun HeadlinesConfigContent(
val prefs = state.headlinePreferences
val previewArticle = state.previewArticle

ScreenColumn(
noBackground = true,
modifier = Modifier.background(Colors.Gray7)
) {
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.widgets__news__name),
onBackClick = onCancel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package to.bitkit.appwidget.config

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -44,10 +43,7 @@ internal fun PriceConfigContent(
val prefs = state.pricePreferences
val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD

ScreenColumn(
noBackground = true,
modifier = Modifier.background(Colors.Gray7)
) {
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.widgets__price__name),
onBackClick = onCancel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package to.bitkit.appwidget.config

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -37,15 +36,11 @@ internal fun WeatherConfigContent(
onReset: () -> Unit,
onSave: () -> Unit,
onCancel: () -> Unit,
modifier: Modifier = Modifier,
) {
val prefs = state.weatherPreferences
val weather = state.previewWeather

ScreenColumn(
noBackground = true,
modifier = modifier.background(Colors.Gray7)
) {
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.widgets__weather__name),
onBackClick = onCancel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package to.bitkit.appwidget.ui.facts

import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
Expand All @@ -18,6 +20,7 @@ import to.bitkit.appwidget.ui.components.CaptionB
import to.bitkit.appwidget.ui.components.GlanceLayoutDimens
import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold
import to.bitkit.appwidget.ui.theme.GlanceTextStyles
import to.bitkit.ui.MainActivity

private val BADGE_SIZE = 32.dp
private val BADGE_RESERVED_END = 40.dp
Expand All @@ -28,8 +31,11 @@ fun FactsGlanceContent(
fact: String?,
) {
val context = LocalContext.current
val openAppIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}

GlanceWidgetScaffold {
GlanceWidgetScaffold(onClick = actionStartActivity(openAppIntent)) {
if (fact == null) {
CaptionB(text = context.getString(R.string.appwidget__loading))
return@GlanceWidgetScaffold
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package to.bitkit.appwidget.ui.weather
import android.content.Context
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.glance.GlanceId
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
Expand All @@ -14,6 +15,8 @@ import to.bitkit.appwidget.model.AppWidgetData
import to.bitkit.appwidget.model.AppWidgetEntry
import to.bitkit.appwidget.model.AppWidgetType
import to.bitkit.appwidget.ui.components.GlanceLayoutDimens
import to.bitkit.data.dto.WeatherDTO
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.ui.screens.widgets.blocks.toWeatherModel

class WeatherGlanceWidget : GlanceAppWidget() {
Expand All @@ -23,16 +26,20 @@ class WeatherGlanceWidget : GlanceAppWidget() {
)

override suspend fun provideGlance(context: Context, id: GlanceId) {
val store = EntryPointAccessors
val accessor = EntryPointAccessors
.fromApplication(context, AppWidgetEntryPoint::class.java)
.appWidgetPreferencesStore()
val store = accessor.appWidgetPreferencesStore()
val currencyRepo = accessor.currencyRepo()
val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)

provideContent {
val data by store.data.collectAsState(initial = AppWidgetData())
val currencyState by currencyRepo.currencyState.collectAsState()
val entry = data.entries.find { it.appWidgetId == appWidgetId }
?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.WEATHER)
val weather = data.cachedWeather?.toWeatherModel()
val weather = remember(data.cachedWeather, currencyState) {
data.cachedWeather?.toGlanceWeather(currencyRepo)
}

WeatherGlanceContent(
entry = entry,
Expand All @@ -48,4 +55,8 @@ class WeatherGlanceWidget : GlanceAppWidget() {
.appWidgetPreferencesStore()
.unregisterWidget(appWidgetId)
}

private fun WeatherDTO.toGlanceWeather(currencyRepo: CurrencyRepo) = toWeatherModel(
currentFee = currencyRepo.formatSatsAsFiatWithSymbol(avgFeeSats, withSpace = true) ?: currentFee,
)
}
46 changes: 29 additions & 17 deletions app/src/main/java/to/bitkit/data/widgets/PriceService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import to.bitkit.models.WidgetType
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -147,24 +146,15 @@ class PriceService @Inject constructor(
)
}

private fun formatPrice(pair: TradingPair, price: Double): String {
private fun formatPrice(
pair: TradingPair,
price: Double,
locale: Locale = Locale.getDefault(),
): String {
return runCatching {
val currency = Currency.getInstance(pair.quote)
val numberFormat = NumberFormat.getCurrencyInstance(Locale.US).apply {
this.currency = currency
maximumFractionDigits = when {
price >= 1000 -> 0
price >= 1 -> 2
else -> 6
}
}

// Format and remove currency symbol, keeping only the number with formatting
val formatted = numberFormat.format(price)
val currencySymbol = currency.symbol
formatted.replace(currencySymbol, "").trim()
formatPriceValue(price = price, locale = locale)
}.onFailure {
Logger.warn("Error formatting price for ${pair.displayName}", e = it, context = TAG)
Logger.warn("Failed to format price for '${pair.displayName}'", it, context = TAG)
}.getOrDefault(String.format(Locale.US, "%.2f", price))
}

Expand All @@ -180,3 +170,25 @@ sealed class PriceError(message: String) : AppError(message) {
class InvalidResponse(override val message: String) : PriceError(message)
class NetworkError(override val message: String) : PriceError(message)
}

private const val GROUPED_PRICE_THRESHOLD = 1_000.0
private const val STANDARD_PRICE_THRESHOLD = 1.0
private const val GROUPED_PRICE_DECIMALS = 0
private const val STANDARD_PRICE_DECIMALS = 2
private const val SMALL_PRICE_DECIMALS = 6
private const val MIN_PRICE_DECIMALS = 0

internal fun formatPriceValue(
price: Double,
locale: Locale = Locale.getDefault(),
): String {
return NumberFormat.getNumberInstance(locale).apply {
maximumFractionDigits = when {
price >= GROUPED_PRICE_THRESHOLD -> GROUPED_PRICE_DECIMALS
price >= STANDARD_PRICE_THRESHOLD -> STANDARD_PRICE_DECIMALS
else -> SMALL_PRICE_DECIMALS
}
minimumFractionDigits = MIN_PRICE_DECIMALS
isGroupingUsed = true
}.format(price)
}
3 changes: 1 addition & 2 deletions app/src/main/java/to/bitkit/data/widgets/WeatherService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ class WeatherService @Inject constructor(
}

private fun formatFeeForDisplay(sats: Long): String {
val selectedFiatValue = currencyRepo.convertSatsToFiat(sats).getOrNull()
return selectedFiatValue?.formattedWithSymbol(withSpace = true).orEmpty()
return currencyRepo.formatSatsAsFiatWithSymbol(sats, withSpace = true).orEmpty()
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ class CurrencyRepo @Inject constructor(

fun convertFiatToSats(fiat: Double, currency: String?) = convertFiatToSats(BigDecimal.valueOf(fiat), currency)

fun formatSatsAsFiatWithSymbol(sats: Long, withSpace: Boolean = false): String? {
return convertSatsToFiat(sats).getOrNull()?.formattedWithSymbol(withSpace = withSpace)
}

companion object {
private const val TAG = "CurrencyRepo"
}
Expand Down
Loading
Loading