From 9f86a9acb0ed6854da6e432029be139744b5d419 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:41:49 -0300 Subject: [PATCH 001/100] chore: add glance dependency --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70a708adc..56c80fbb7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -280,6 +280,9 @@ dependencies { // WorkManager implementation(libs.hilt.work) implementation(libs.work.runtime.ktx) + // Glance - AppWidgets + implementation(libs.glance.appwidget) + implementation(libs.glance.material3) // Ktor - Networking implementation(libs.ktor.client.core) implementation(libs.ktor.client.okhttp) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea7508a9a..455ad7e34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ agp = "8.13.2" camera = "1.5.2" detekt = "1.23.8" +glance = "1.1.1" hilt = "2.57.2" hiltAndroidx = "1.3.0" kotlin = "2.2.21" @@ -42,6 +43,8 @@ datastore-preferences = { module = "androidx.datastore:datastore-preferences", v detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.3" } firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.8.0" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } +glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } From 55b19eb47cb0e1534b8684d3d9d8f097d1e32fab Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:53:47 -0300 Subject: [PATCH 002/100] chore: data models --- .../appwidget/model/AppWidgetPreferences.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt new file mode 100644 index 000000000..a86b41b13 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -0,0 +1,74 @@ +package to.bitkit.appwidget.model + +import kotlinx.serialization.Serializable +import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO +import to.bitkit.data.dto.WeatherDTO +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.dto.price.TradingPair + +enum class AppWidgetType { + BLOCKS, + PRICE, + WEATHER, + HEADLINES, + FACTS, +} + +@Serializable +data class AppWidgetEntry( + val appWidgetId: Int, + val type: AppWidgetType, + val blocksPreferences: HomeBlocksPreferences = HomeBlocksPreferences(), + val pricePreferences: HomePricePreferences = HomePricePreferences(), + val weatherPreferences: HomeWeatherPreferences = HomeWeatherPreferences(), + val headlinesPreferences: HomeHeadlinesPreferences = HomeHeadlinesPreferences(), + val factsPreferences: HomeFactsPreferences = HomeFactsPreferences(), +) + +@Serializable +data class HomeBlocksPreferences( + val showBlock: Boolean = true, + val showTime: Boolean = true, + val showDate: Boolean = true, + val showTransactions: Boolean = false, + val showSize: Boolean = false, + val showSource: Boolean = false, +) + +@Serializable +data class HomePricePreferences( + val enabledPairs: List = listOf(TradingPair.BTC_USD), + val period: GraphPeriod = GraphPeriod.ONE_DAY, + val showSource: Boolean = false, +) + +@Serializable +data class HomeWeatherPreferences( + val showTitle: Boolean = true, + val showDescription: Boolean = false, + val showCurrentFee: Boolean = false, + val showNextBlockFee: Boolean = false, +) + +@Serializable +data class HomeHeadlinesPreferences( + val showTime: Boolean = true, + val showSource: Boolean = true, +) + +@Serializable +data class HomeFactsPreferences( + val showSource: Boolean = false, +) + +@Serializable +data class AppWidgetData( + val entries: List = emptyList(), + val cachedBlock: BlockDTO? = null, + val cachedPrice: PriceDTO? = null, + val cachedWeather: WeatherDTO? = null, + val cachedArticles: List = emptyList(), + val cachedFacts: List = emptyList(), +) From e90947361a3df3b565c14b3d55e5a7fa267c6df0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:54:40 -0300 Subject: [PATCH 003/100] chore: independent data store for home widgets and cached data --- .../appwidget/AppWidgetPreferencesStore.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt new file mode 100644 index 000000000..2c3a6c693 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -0,0 +1,98 @@ +package to.bitkit.appwidget + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.dataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import to.bitkit.appwidget.model.AppWidgetData +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO +import to.bitkit.data.dto.WeatherDTO +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.serializers.AppWidgetDataSerializer +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.appWidgetDataStore: DataStore by dataStore( + fileName = "appwidget_data.json", + serializer = AppWidgetDataSerializer, +) + +@Suppress("TooManyFunctions") +@Singleton +class AppWidgetPreferencesStore @Inject constructor( + @ApplicationContext private val context: Context, +) { + companion object { + @Volatile + private var instance: AppWidgetPreferencesStore? = null + + fun getInstance(context: Context): AppWidgetPreferencesStore = + instance ?: synchronized(this) { + instance ?: AppWidgetPreferencesStore(context.applicationContext).also { + instance = it + } + } + } + + private val store = context.appWidgetDataStore + + val data: Flow = store.data + + suspend fun registerWidget(appWidgetId: Int, type: AppWidgetType) { + store.updateData { data -> + if (data.entries.any { it.appWidgetId == appWidgetId }) return@updateData data + data.copy(entries = data.entries + AppWidgetEntry(appWidgetId = appWidgetId, type = type)) + } + } + + suspend fun unregisterWidget(appWidgetId: Int) { + store.updateData { data -> + data.copy(entries = data.entries.filter { it.appWidgetId != appWidgetId }) + } + } + + suspend fun getEntry(appWidgetId: Int): AppWidgetEntry? = + store.data.first().entries.find { it.appWidgetId == appWidgetId } + + suspend fun updateEntry(appWidgetId: Int, transform: (AppWidgetEntry) -> AppWidgetEntry) { + store.updateData { data -> + data.copy( + entries = data.entries.map { + if (it.appWidgetId == appWidgetId) transform(it) else it + }, + ) + } + } + + suspend fun getActiveWidgetTypes(): Set = + store.data.first().entries.map { it.type }.toSet() + + fun hasWidgetsOfType(type: AppWidgetType): Flow = + data.map { it.entries.any { entry -> entry.type == type } } + + suspend fun cacheBlockData(block: BlockDTO) { + store.updateData { it.copy(cachedBlock = block) } + } + + suspend fun cacheWeatherData(weather: WeatherDTO) { + store.updateData { it.copy(cachedWeather = weather) } + } + + suspend fun cachePriceData(price: PriceDTO) { + store.updateData { it.copy(cachedPrice = price) } + } + + suspend fun cacheArticles(articles: List) { + store.updateData { it.copy(cachedArticles = articles) } + } + + suspend fun cacheFacts(facts: List) { + store.updateData { it.copy(cachedFacts = facts) } + } +} From b3d1235809284e41cc740bd783d644b0948669d1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:55:33 -0300 Subject: [PATCH 004/100] feat: create repository reusing existing services --- .../appwidget/AppWidgetDataRepository.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt new file mode 100644 index 000000000..dbdabd504 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt @@ -0,0 +1,48 @@ +package to.bitkit.appwidget + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO +import to.bitkit.data.dto.WeatherDTO +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.widgets.BlocksService +import to.bitkit.data.widgets.FactsService +import to.bitkit.data.widgets.NewsService +import to.bitkit.data.widgets.PriceService +import to.bitkit.data.widgets.WeatherService +import to.bitkit.di.IoDispatcher +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AppWidgetDataRepository @Inject constructor( + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val priceService: PriceService, + private val blocksService: BlocksService, + private val weatherService: WeatherService, + private val newsService: NewsService, + private val factsService: FactsService, +) { + suspend fun fetchBlockData(): Result = withContext(ioDispatcher) { + blocksService.fetchData() + } + + suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result = + withContext(ioDispatcher) { + priceService.fetchData(period) + } + + suspend fun fetchWeatherData(): Result = withContext(ioDispatcher) { + weatherService.fetchData() + } + + suspend fun fetchHeadlines(): Result> = withContext(ioDispatcher) { + newsService.fetchData() + } + + suspend fun fetchFacts(): Result> = withContext(ioDispatcher) { + factsService.fetchData() + } +} From a3397b1219649157f31328adf5c5b340ef079aa5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:56:47 -0300 Subject: [PATCH 005/100] feat: refresh data with work manager --- .../appwidget/AppWidgetRefreshWorker.kt | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt new file mode 100644 index 000000000..2706caa58 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -0,0 +1,138 @@ +package to.bitkit.appwidget + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import androidx.glance.appwidget.updateAll +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.ui.blocks.BlocksGlanceReceiver +import to.bitkit.appwidget.ui.blocks.BlocksGlanceWidget +import to.bitkit.appwidget.ui.facts.FactsGlanceReceiver +import to.bitkit.appwidget.ui.facts.FactsGlanceWidget +import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceReceiver +import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget +import to.bitkit.appwidget.ui.price.PriceGlanceReceiver +import to.bitkit.appwidget.ui.price.PriceGlanceWidget +import to.bitkit.appwidget.ui.weather.WeatherGlanceReceiver +import to.bitkit.appwidget.ui.weather.WeatherGlanceWidget +import to.bitkit.utils.Logger +import java.util.concurrent.TimeUnit + +@HiltWorker +class AppWidgetRefreshWorker @AssistedInject constructor( + @Assisted private val appContext: Context, + @Assisted workerParams: WorkerParameters, + private val dataRepository: AppWidgetDataRepository, + private val preferencesStore: AppWidgetPreferencesStore, +) : CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val activeTypes = preferencesStore.getActiveWidgetTypes() + if (activeTypes.isEmpty()) return Result.success() + + Logger.debug("Refreshing data for widget types: $activeTypes", context = TAG) + + for (type in activeTypes) { + when (type) { + AppWidgetType.BLOCKS -> dataRepository.fetchBlockData() + .onSuccess { + preferencesStore.cacheBlockData(it) + BlocksGlanceWidget().updateAll(appContext) + } + .onFailure { Logger.warn("Failed to refresh blocks", e = it, context = TAG) } + + AppWidgetType.PRICE -> dataRepository.fetchPriceData() + .onSuccess { + preferencesStore.cachePriceData(it) + PriceGlanceWidget().updateAll(appContext) + } + .onFailure { Logger.warn("Failed to refresh price", e = it, context = TAG) } + + AppWidgetType.WEATHER -> dataRepository.fetchWeatherData() + .onSuccess { + preferencesStore.cacheWeatherData(it) + WeatherGlanceWidget().updateAll(appContext) + } + .onFailure { Logger.warn("Failed to refresh weather", e = it, context = TAG) } + + AppWidgetType.HEADLINES -> dataRepository.fetchHeadlines() + .onSuccess { + preferencesStore.cacheArticles(it) + HeadlinesGlanceWidget().updateAll(appContext) + } + .onFailure { Logger.warn("Failed to refresh headlines", e = it, context = TAG) } + + AppWidgetType.FACTS -> dataRepository.fetchFacts() + .onSuccess { + preferencesStore.cacheFacts(it) + FactsGlanceWidget().updateAll(appContext) + } + .onFailure { Logger.warn("Failed to refresh facts", e = it, context = TAG) } + } + } + + return Result.success() + } + + companion object { + private const val TAG = "AppWidgetRefreshWorker" + private const val WORK_NAME = "appwidget_refresh" + + fun enqueue(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = PeriodicWorkRequestBuilder( + repeatInterval = 15, + repeatIntervalTimeUnit = TimeUnit.MINUTES, + ).setConstraints(constraints).build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + request, + ) + } + + fun enqueueImmediate(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueue(request) + } + + fun cancelIfNoWidgets(context: Context) { + val manager = AppWidgetManager.getInstance(context) + val receivers = listOf( + BlocksGlanceReceiver::class.java, + PriceGlanceReceiver::class.java, + WeatherGlanceReceiver::class.java, + HeadlinesGlanceReceiver::class.java, + FactsGlanceReceiver::class.java, + ) + val hasAny = receivers.any { receiver -> + manager.getAppWidgetIds(ComponentName(context, receiver)).isNotEmpty() + } + if (!hasAny) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) + } + } + } +} From 2bc681f4412f71f1db60823c3d5b9217f963bfbe Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:57:13 -0300 Subject: [PATCH 006/100] feat: data serializer --- .../serializers/AppWidgetDataSerializer.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt diff --git a/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt new file mode 100644 index 000000000..2a963e7b7 --- /dev/null +++ b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt @@ -0,0 +1,26 @@ +package to.bitkit.data.serializers + +import androidx.datastore.core.Serializer +import kotlinx.serialization.SerializationException +import to.bitkit.appwidget.model.AppWidgetData +import to.bitkit.di.json +import to.bitkit.utils.Logger +import java.io.InputStream +import java.io.OutputStream + +object AppWidgetDataSerializer : Serializer { + override val defaultValue: AppWidgetData = AppWidgetData() + + override suspend fun readFrom(input: InputStream): AppWidgetData { + return try { + json.decodeFromString(input.readBytes().decodeToString()) + } catch (e: SerializationException) { + Logger.error("Failed to deserialize: $e") + defaultValue + } + } + + override suspend fun writeTo(t: AppWidgetData, output: OutputStream) { + output.write(json.encodeToString(t).encodeToByteArray()) + } +} From 8112f12ede392518beda1991dc6f33e1657216ca Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:59:48 -0300 Subject: [PATCH 007/100] feat: ui --- .../ui/blocks/BlocksGlanceContent.kt | 80 ++++++++++++ .../ui/blocks/BlocksGlanceReceiver.kt | 19 +++ .../appwidget/ui/blocks/BlocksGlanceWidget.kt | 33 +++++ .../appwidget/ui/components/GlanceDataRow.kt | 43 +++++++ .../ui/components/GlanceWidgetScaffold.kt | 33 +++++ .../appwidget/ui/facts/FactsGlanceContent.kt | 72 +++++++++++ .../appwidget/ui/facts/FactsGlanceReceiver.kt | 20 +++ .../appwidget/ui/facts/FactsGlanceWidget.kt | 30 +++++ .../ui/headlines/HeadlinesGlanceContent.kt | 87 +++++++++++++ .../ui/headlines/HeadlinesGlanceReceiver.kt | 20 +++ .../ui/headlines/HeadlinesGlanceWidget.kt | 30 +++++ .../appwidget/ui/price/PriceGlanceContent.kt | 119 ++++++++++++++++++ .../appwidget/ui/price/PriceGlanceReceiver.kt | 20 +++ .../appwidget/ui/price/PriceGlanceWidget.kt | 30 +++++ .../ui/weather/WeatherGlanceContent.kt | 103 +++++++++++++++ .../ui/weather/WeatherGlanceReceiver.kt | 20 +++ .../ui/weather/WeatherGlanceWidget.kt | 30 +++++ 17 files changed, 789 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt new file mode 100644 index 000000000..5a012736a --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt @@ -0,0 +1,80 @@ +package to.bitkit.appwidget.ui.blocks + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.GlanceDataRow +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.models.widget.BlockModel + +@Composable +fun BlocksGlanceContent( + context: Context, + block: BlockModel?, + entry: AppWidgetEntry, +) { + val prefs = entry.blocksPreferences + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + + GlanceWidgetScaffold(onClick = launchIntent) { + Text( + text = context.getString(to.bitkit.R.string.widgets__blocks__name), + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = GlanceModifier.height(8.dp)) + + if (block == null) { + Text( + text = context.getString(to.bitkit.R.string.appwidget__loading), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + ), + ) + return@GlanceWidgetScaffold + } + + if (prefs.showBlock && block.height.isNotEmpty()) { + GlanceDataRow(label = "Block", value = block.height) + } + if (prefs.showTime && block.time.isNotEmpty()) { + GlanceDataRow(label = "Time", value = block.time) + } + if (prefs.showDate && block.date.isNotEmpty()) { + GlanceDataRow(label = "Date", value = block.date) + } + if (prefs.showTransactions && block.transactionCount.isNotEmpty()) { + GlanceDataRow(label = "Txs", value = block.transactionCount) + } + if (prefs.showSize && block.size.isNotEmpty()) { + GlanceDataRow(label = "Size", value = block.size) + } + if (prefs.showSource && block.source.isNotEmpty()) { + Spacer(modifier = GlanceModifier.height(4.dp)) + Text( + text = block.source, + style = TextStyle( + color = GlanceColors.textTertiary, + fontSize = 11.sp, + ), + modifier = GlanceModifier.fillMaxWidth().padding(top = 4.dp), + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt new file mode 100644 index 000000000..7b1655e54 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt @@ -0,0 +1,19 @@ +package to.bitkit.appwidget.ui.blocks + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class BlocksGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = BlocksGlanceWidget() + + override fun onEnabled(context: android.content.Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDisabled(context: android.content.Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt new file mode 100644 index 000000000..aa5a5325a --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt @@ -0,0 +1,33 @@ +package to.bitkit.appwidget.ui.blocks + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.provideContent +import kotlinx.coroutines.flow.first +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.models.widget.BlockModel +import to.bitkit.models.widget.toBlockModel + +class BlocksGlanceWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = AppWidgetPreferencesStore.getInstance(context) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val data = store.data.first() + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.BLOCKS) + val block: BlockModel? = data.cachedBlock?.toBlockModel() + + provideContent { + BlocksGlanceContent( + context = context, + block = block, + entry = entry, + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt new file mode 100644 index 000000000..f7fba499c --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt @@ -0,0 +1,43 @@ +package to.bitkit.appwidget.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.appwidget.ui.theme.GlanceColors + +@Composable +fun GlanceDataRow( + label: String, + value: String, +) { + Row( + modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp), + horizontalAlignment = Alignment.Horizontal.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ), + ) + Text( + text = value, + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ), + ) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt new file mode 100644 index 000000000..117f1fc9e --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt @@ -0,0 +1,33 @@ +package to.bitkit.appwidget.ui.components + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceModifier +import androidx.glance.action.clickable +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.appwidget.cornerRadius +import androidx.glance.background +import androidx.glance.layout.Column +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding +import to.bitkit.appwidget.ui.theme.GlanceColors + +@Composable +fun GlanceWidgetScaffold( + onClick: Intent? = null, + content: @Composable () -> Unit, +) { + val modifier = GlanceModifier + .fillMaxSize() + .cornerRadius(16.dp) + .background(GlanceColors.cardBackgroundProvider) + .padding(16.dp) + .let { mod -> + if (onClick != null) mod.clickable(actionStartActivity(onClick)) else mod + } + + Column(modifier = modifier) { + content() + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt new file mode 100644 index 000000000..e22673dac --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt @@ -0,0 +1,72 @@ +package to.bitkit.appwidget.ui.facts + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Spacer +import androidx.glance.layout.height +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.R +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.theme.GlanceColors + +@Composable +fun FactsGlanceContent( + context: Context, + facts: List, + entry: AppWidgetEntry, +) { + val prefs = entry.factsPreferences + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + + GlanceWidgetScaffold(onClick = launchIntent) { + Text( + text = context.getString(R.string.widgets__facts__name), + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = GlanceModifier.height(8.dp)) + + if (facts.isEmpty()) { + Text( + text = context.getString(R.string.appwidget__loading), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + ), + ) + return@GlanceWidgetScaffold + } + + val fact = facts.random() + Text( + text = fact, + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ), + maxLines = 4, + ) + + if (prefs.showSource) { + Spacer(modifier = GlanceModifier.height(8.dp)) + Text( + text = context.getString(R.string.appwidget__facts__source), + style = TextStyle( + color = GlanceColors.textTertiary, + fontSize = 11.sp, + ), + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt new file mode 100644 index 000000000..304fb0f1b --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt @@ -0,0 +1,20 @@ +package to.bitkit.appwidget.ui.facts + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class FactsGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = FactsGlanceWidget() + + override fun onEnabled(context: Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt new file mode 100644 index 000000000..c3a01c482 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt @@ -0,0 +1,30 @@ +package to.bitkit.appwidget.ui.facts + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.provideContent +import kotlinx.coroutines.flow.first +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType + +class FactsGlanceWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = AppWidgetPreferencesStore.getInstance(context) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val data = store.data.first() + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.FACTS) + + provideContent { + FactsGlanceContent( + context = context, + facts = data.cachedFacts, + entry = entry, + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt new file mode 100644 index 000000000..74c92fcb5 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt @@ -0,0 +1,87 @@ +package to.bitkit.appwidget.ui.headlines + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Column +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.R +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.data.dto.ArticleDTO + +@Composable +fun HeadlinesGlanceContent( + context: Context, + articles: List, + entry: AppWidgetEntry, +) { + val prefs = entry.headlinesPreferences + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + + GlanceWidgetScaffold(onClick = launchIntent) { + Text( + text = context.getString(R.string.widgets__news__name), + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = GlanceModifier.height(8.dp)) + + if (articles.isEmpty()) { + Text( + text = context.getString(R.string.appwidget__loading), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + ), + ) + return@GlanceWidgetScaffold + } + + val displayArticles = articles.take(3) + for ((index, article) in displayArticles.withIndex()) { + Column(modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp)) { + Text( + text = article.title, + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ), + maxLines = 2, + ) + if (prefs.showTime || prefs.showSource) { + val meta = buildString { + if (prefs.showTime) append(article.publishedDate) + if (prefs.showTime && prefs.showSource) append(" ยท ") + if (prefs.showSource) append(article.publisher.title) + } + Text( + text = meta, + style = TextStyle( + color = GlanceColors.textTertiary, + fontSize = 11.sp, + ), + maxLines = 1, + ) + } + } + if (index < displayArticles.lastIndex) { + Spacer(modifier = GlanceModifier.height(4.dp)) + } + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt new file mode 100644 index 000000000..4c4c0327c --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt @@ -0,0 +1,20 @@ +package to.bitkit.appwidget.ui.headlines + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class HeadlinesGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = HeadlinesGlanceWidget() + + override fun onEnabled(context: Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt new file mode 100644 index 000000000..6fdb2aad4 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt @@ -0,0 +1,30 @@ +package to.bitkit.appwidget.ui.headlines + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.provideContent +import kotlinx.coroutines.flow.first +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType + +class HeadlinesGlanceWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = AppWidgetPreferencesStore.getInstance(context) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val data = store.data.first() + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.HEADLINES) + + provideContent { + HeadlinesGlanceContent( + context = context, + articles = data.cachedArticles, + entry = entry, + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt new file mode 100644 index 000000000..2adf27b22 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -0,0 +1,119 @@ +package to.bitkit.appwidget.ui.price + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.width +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.R +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.dto.price.PriceWidgetData + +@Composable +fun PriceGlanceContent( + context: Context, + price: PriceDTO?, + entry: AppWidgetEntry, +) { + val prefs = entry.pricePreferences + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + + GlanceWidgetScaffold(onClick = launchIntent) { + Text( + text = context.getString(R.string.widgets__price__name), + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = GlanceModifier.height(8.dp)) + + if (price == null) { + Text( + text = context.getString(R.string.appwidget__loading), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + ), + ) + return@GlanceWidgetScaffold + } + + val enabledWidgets = price.widgets.filter { it.pair in prefs.enabledPairs } + val displayWidgets = enabledWidgets.ifEmpty { price.widgets.take(1) } + + for (widget in displayWidgets) { + PriceRow(widget = widget) + Spacer(modifier = GlanceModifier.height(4.dp)) + } + + if (prefs.showSource) { + Spacer(modifier = GlanceModifier.height(4.dp)) + Text( + text = price.source, + style = TextStyle( + color = GlanceColors.textTertiary, + fontSize = 11.sp, + ), + ) + } + } +} + +@Composable +private fun PriceRow(widget: PriceWidgetData) { + Row( + modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${widget.pair.symbol}${widget.price}", + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + ) + Spacer(modifier = GlanceModifier.width(8.dp)) + Text( + text = widget.change.formatted, + style = TextStyle( + color = if (widget.change.isPositive) { + androidx.glance.color.ColorProvider( + day = GlanceColors.Green, + night = GlanceColors.Green, + ) + } else { + androidx.glance.color.ColorProvider( + day = GlanceColors.Red, + night = GlanceColors.Red, + ) + }, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ), + ) + } + Text( + text = widget.pair.displayName, + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 12.sp, + ), + ) +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt new file mode 100644 index 000000000..7b810c228 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt @@ -0,0 +1,20 @@ +package to.bitkit.appwidget.ui.price + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class PriceGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = PriceGlanceWidget() + + override fun onEnabled(context: Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt new file mode 100644 index 000000000..7c568c4e9 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -0,0 +1,30 @@ +package to.bitkit.appwidget.ui.price + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.provideContent +import kotlinx.coroutines.flow.first +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType + +class PriceGlanceWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = AppWidgetPreferencesStore.getInstance(context) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val data = store.data.first() + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) + + provideContent { + PriceGlanceContent( + context = context, + price = data.cachedPrice, + entry = entry, + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt new file mode 100644 index 000000000..1f8596f60 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt @@ -0,0 +1,103 @@ +package to.bitkit.appwidget.ui.weather + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Spacer +import androidx.glance.layout.height +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import to.bitkit.R +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.GlanceDataRow +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.data.dto.FeeCondition +import to.bitkit.data.dto.WeatherDTO + +@Composable +fun WeatherGlanceContent( + context: Context, + weather: WeatherDTO?, + entry: AppWidgetEntry, +) { + val prefs = entry.weatherPreferences + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + + GlanceWidgetScaffold(onClick = launchIntent) { + Text( + text = context.getString(R.string.widgets__weather__name), + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = GlanceModifier.height(8.dp)) + + if (weather == null) { + Text( + text = context.getString(R.string.appwidget__loading), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 13.sp, + ), + ) + return@GlanceWidgetScaffold + } + + if (prefs.showTitle) { + Text( + text = "${weather.condition.icon} ${conditionLabel(context, weather.condition)}", + style = TextStyle( + color = GlanceColors.textPrimary, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + ) + Spacer(modifier = GlanceModifier.height(4.dp)) + } + + if (prefs.showDescription) { + Text( + text = conditionDescription(context, weather.condition), + style = TextStyle( + color = GlanceColors.textSecondary, + fontSize = 12.sp, + ), + ) + Spacer(modifier = GlanceModifier.height(4.dp)) + } + + if (prefs.showCurrentFee) { + GlanceDataRow( + label = context.getString(R.string.appwidget__weather__current_fee), + value = weather.currentFee, + ) + } + + if (prefs.showNextBlockFee) { + GlanceDataRow( + label = context.getString(R.string.appwidget__weather__next_block), + value = "${weather.nextBlockFee} sat/vB", + ) + } + } +} + +private fun conditionLabel(context: Context, condition: FeeCondition): String = when (condition) { + FeeCondition.GOOD -> context.getString(R.string.appwidget__weather__good) + FeeCondition.AVERAGE -> context.getString(R.string.appwidget__weather__average) + FeeCondition.POOR -> context.getString(R.string.appwidget__weather__poor) +} + +private fun conditionDescription(context: Context, condition: FeeCondition): String = + when (condition) { + FeeCondition.GOOD -> context.getString(R.string.appwidget__weather__good_desc) + FeeCondition.AVERAGE -> context.getString(R.string.appwidget__weather__average_desc) + FeeCondition.POOR -> context.getString(R.string.appwidget__weather__poor_desc) + } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt new file mode 100644 index 000000000..66e6dc069 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt @@ -0,0 +1,20 @@ +package to.bitkit.appwidget.ui.weather + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class WeatherGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = WeatherGlanceWidget() + + override fun onEnabled(context: Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt new file mode 100644 index 000000000..6e9259684 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt @@ -0,0 +1,30 @@ +package to.bitkit.appwidget.ui.weather + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.provideContent +import kotlinx.coroutines.flow.first +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType + +class WeatherGlanceWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = AppWidgetPreferencesStore.getInstance(context) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val data = store.data.first() + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.WEATHER) + + provideContent { + WeatherGlanceContent( + context = context, + weather = data.cachedWeather, + entry = entry, + ) + } + } +} From 01db5272c4788353842aebdc006e6a45762a0e44 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 15:00:57 -0300 Subject: [PATCH 008/100] feat: config --- .../config/AppWidgetConfigActivity.kt | 81 ++++++ .../appwidget/config/AppWidgetConfigScreen.kt | 245 ++++++++++++++++++ .../config/AppWidgetConfigViewModel.kt | 238 +++++++++++++++++ 3 files changed, 564 insertions(+) create mode 100644 app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt new file mode 100644 index 000000000..3fc8b170d --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -0,0 +1,81 @@ +package to.bitkit.appwidget.config + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import dagger.hilt.android.AndroidEntryPoint +import to.bitkit.appwidget.AppWidgetRefreshWorker +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.ui.theme.AppThemeSurface + +@AndroidEntryPoint +class AppWidgetConfigActivity : ComponentActivity() { + + companion object { + const val EXTRA_WIDGET_TYPE = "extra_widget_type" + } + + private val viewModel: AppWidgetConfigViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appWidgetId = intent?.extras?.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, + ) ?: AppWidgetManager.INVALID_APPWIDGET_ID + + setResult(Activity.RESULT_CANCELED) + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + val typeName = intent?.getStringExtra(EXTRA_WIDGET_TYPE) + val type = typeName?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } + ?: resolveTypeFromProvider() + ?: AppWidgetType.BLOCKS + + viewModel.init(appWidgetId, type) + + setContent { + AppThemeSurface { + AppWidgetConfigScreen( + viewModel = viewModel, + onConfirm = { + AppWidgetRefreshWorker.enqueueImmediate(this) + val result = Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetId, + ) + setResult(Activity.RESULT_OK, result) + finish() + }, + onCancel = { finish() }, + ) + } + } + } + + private fun resolveTypeFromProvider(): AppWidgetType? { + val providerInfo = intent?.extras?.let { + val id = it.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (id != -1) AppWidgetManager.getInstance(this).getAppWidgetInfo(id) else null + } ?: return null + + val providerClass = providerInfo.provider.className + return when { + providerClass.contains("Blocks") -> AppWidgetType.BLOCKS + providerClass.contains("Price") -> AppWidgetType.PRICE + providerClass.contains("Weather") -> AppWidgetType.WEATHER + providerClass.contains("Headlines") -> AppWidgetType.HEADLINES + providerClass.contains("Facts") -> AppWidgetType.FACTS + else -> null + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt new file mode 100644 index 000000000..f829baf51 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -0,0 +1,245 @@ +package to.bitkit.appwidget.config + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import to.bitkit.R +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.TradingPair +import to.bitkit.models.widget.ArticleModel +import to.bitkit.models.widget.BlockModel +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.widgets.blocks.BlocksEditContent +import to.bitkit.ui.screens.widgets.facts.FactsEditContent +import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditContent +import to.bitkit.ui.screens.widgets.weather.WeatherEditContent +import to.bitkit.ui.theme.Colors + +@Composable +fun AppWidgetConfigScreen( + viewModel: AppWidgetConfigViewModel, + onConfirm: () -> Unit, + onCancel: () -> Unit, +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + when (state.type) { + AppWidgetType.BLOCKS -> BlocksEditContent( + onBack = onCancel, + blocksPreferences = state.blocksPreferences, + block = state.blockModel ?: BlockModel( + height = "", + time = "", + date = "", + transactionCount = "", + size = "", + source = "", + ), + onClickShowBlock = { viewModel.toggleBlocksShow(BlocksField.BLOCK) }, + onClickShowTime = { viewModel.toggleBlocksShow(BlocksField.TIME) }, + onClickShowDate = { viewModel.toggleBlocksShow(BlocksField.DATE) }, + onClickShowTransactions = { viewModel.toggleBlocksShow(BlocksField.TRANSACTIONS) }, + onClickShowSize = { viewModel.toggleBlocksShow(BlocksField.SIZE) }, + onClickShowSource = { viewModel.toggleBlocksShow(BlocksField.SOURCE) }, + onClickReset = { viewModel.resetPreferences() }, + onClickPreview = { viewModel.saveAndFinish(onConfirm) }, + ) + + AppWidgetType.WEATHER -> WeatherEditContent( + onBack = onCancel, + weather = null, + weatherPreferences = state.weatherPreferences, + onClickShowTitle = { viewModel.toggleWeatherShow(WeatherField.TITLE) }, + onClickShowDescription = { viewModel.toggleWeatherShow(WeatherField.DESCRIPTION) }, + onClickShowCurrentFee = { viewModel.toggleWeatherShow(WeatherField.CURRENT_FEE) }, + onClickShowNextBlockFee = { viewModel.toggleWeatherShow(WeatherField.NEXT_BLOCK) }, + onClickReset = { viewModel.resetPreferences() }, + onClickPreview = { viewModel.saveAndFinish(onConfirm) }, + ) + + AppWidgetType.HEADLINES -> HeadlinesEditContent( + onBack = onCancel, + headlinePreferences = state.headlinePreferences, + article = ArticleModel( + title = "", + timeAgo = "", + link = "", + publisher = "", + ), + onClickTime = { viewModel.toggleHeadlineTime() }, + onClickShowSource = { viewModel.toggleHeadlineSource() }, + onClickReset = { viewModel.resetPreferences() }, + onClickPreview = { viewModel.saveAndFinish(onConfirm) }, + ) + + AppWidgetType.FACTS -> FactsEditContent( + onBack = onCancel, + factsPreferences = state.factsPreferences, + fact = state.currentFact, + onClickShowSource = { viewModel.toggleFactsSource() }, + onClickReset = { viewModel.resetPreferences() }, + onClickPreview = { viewModel.saveAndFinish(onConfirm) }, + ) + + AppWidgetType.PRICE -> PriceConfigContent( + state = state, + onTogglePair = { viewModel.togglePricePair(it) }, + onSelectPeriod = { viewModel.selectPricePeriod(it) }, + onToggleSource = { viewModel.togglePriceSource() }, + onReset = { viewModel.resetPreferences() }, + onSave = { viewModel.saveAndFinish(onConfirm) }, + onCancel = onCancel, + ) + } +} + +@Composable +private fun PriceConfigContent( + state: AppWidgetConfigUiState, + onTogglePair: (TradingPair) -> Unit, + onSelectPeriod: (GraphPeriod) -> Unit, + onToggleSource: () -> Unit, + onReset: () -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val prefs = state.pricePreferences + ScreenColumn { + AppTopBar( + titleText = stringResource(R.string.widgets__widget__edit), + onBackClick = onCancel, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(26.dp)) + + BodyM( + text = stringResource(R.string.widgets__widget__edit_description).replace( + "{name}", + stringResource(R.string.widgets__price__name), + ), + color = Colors.White64, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + BodySSB( + text = stringResource(R.string.appwidget__price__trading_pairs), + color = Colors.White64, + ) + Spacer(modifier = Modifier.height(8.dp)) + + for (pair in TradingPair.entries) { + ConfigToggleRow( + label = pair.displayName, + isEnabled = pair in prefs.enabledPairs, + onClick = { onTogglePair(pair) }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + BodySSB( + text = stringResource(R.string.appwidget__price__period), + color = Colors.White64, + ) + Spacer(modifier = Modifier.height(8.dp)) + + for (period in GraphPeriod.entries) { + ConfigToggleRow( + label = period.value, + isEnabled = period == prefs.period, + onClick = { onSelectPeriod(period) }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + ConfigToggleRow( + label = stringResource(R.string.widgets__widget__source), + isEnabled = prefs.showSource, + onClick = onToggleSource, + ) + } + + Row( + modifier = Modifier + .padding(vertical = 21.dp, horizontal = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + SecondaryButton( + text = stringResource(R.string.common__reset), + enabled = prefs != state.pricePreferences, + fullWidth = false, + onClick = onReset, + modifier = Modifier.weight(1f), + ) + PrimaryButton( + text = stringResource(R.string.appwidget__config__confirm), + fullWidth = false, + onClick = onSave, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun ConfigToggleRow( + label: String, + isEnabled: Boolean, + onClick: () -> Unit, +) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + ) { + BodySSB( + text = label, + color = Colors.White64, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = onClick) { + Icon( + painter = painterResource(R.drawable.ic_checkmark), + contentDescription = null, + tint = if (isEnabled) Colors.Brand else Colors.White50, + modifier = Modifier.size(32.dp), + ) + } + } + HorizontalDivider() + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt new file mode 100644 index 000000000..a4dcd3644 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -0,0 +1,238 @@ +package to.bitkit.appwidget.config + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.model.HomeBlocksPreferences +import to.bitkit.appwidget.model.HomeFactsPreferences +import to.bitkit.appwidget.model.HomeHeadlinesPreferences +import to.bitkit.appwidget.model.HomePricePreferences +import to.bitkit.appwidget.model.HomeWeatherPreferences +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.TradingPair +import to.bitkit.models.widget.BlockModel +import to.bitkit.models.widget.BlocksPreferences +import to.bitkit.models.widget.FactsPreferences +import to.bitkit.models.widget.HeadlinePreferences +import to.bitkit.models.widget.PricePreferences +import to.bitkit.models.widget.WeatherPreferences +import to.bitkit.models.widget.toBlockModel +import javax.inject.Inject + +@Suppress("TooManyFunctions") +@HiltViewModel +class AppWidgetConfigViewModel @Inject constructor( + private val preferencesStore: AppWidgetPreferencesStore, +) : ViewModel() { + + private val _uiState = MutableStateFlow(AppWidgetConfigUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun init(appWidgetId: Int, type: AppWidgetType) { + viewModelScope.launch { + val entry = preferencesStore.getEntry(appWidgetId) + val data = preferencesStore.data.first() + + _uiState.update { + it.copy( + appWidgetId = appWidgetId, + type = type, + blocksPreferences = entry?.blocksPreferences?.toInApp() ?: BlocksPreferences(), + pricePreferences = entry?.pricePreferences?.toInApp() ?: PricePreferences(), + weatherPreferences = entry?.weatherPreferences?.toInApp() ?: WeatherPreferences(), + headlinePreferences = entry?.headlinesPreferences?.toInApp() + ?: HeadlinePreferences(), + factsPreferences = entry?.factsPreferences?.toInApp() ?: FactsPreferences(), + blockModel = data.cachedBlock?.toBlockModel(), + currentFact = data.cachedFacts.randomOrNull() ?: "", + ) + } + } + } + + fun toggleBlocksShow(field: BlocksField) { + _uiState.update { + val p = it.blocksPreferences + it.copy( + blocksPreferences = when (field) { + BlocksField.BLOCK -> p.copy(showBlock = !p.showBlock) + BlocksField.TIME -> p.copy(showTime = !p.showTime) + BlocksField.DATE -> p.copy(showDate = !p.showDate) + BlocksField.TRANSACTIONS -> p.copy(showTransactions = !p.showTransactions) + BlocksField.SIZE -> p.copy(showSize = !p.showSize) + BlocksField.SOURCE -> p.copy(showSource = !p.showSource) + }, + ) + } + } + + fun toggleWeatherShow(field: WeatherField) { + _uiState.update { + val p = it.weatherPreferences + it.copy( + weatherPreferences = when (field) { + WeatherField.TITLE -> p.copy(showTitle = !p.showTitle) + WeatherField.DESCRIPTION -> p.copy(showDescription = !p.showDescription) + WeatherField.CURRENT_FEE -> p.copy(showCurrentFee = !p.showCurrentFee) + WeatherField.NEXT_BLOCK -> p.copy(showNextBlockFee = !p.showNextBlockFee) + }, + ) + } + } + + fun toggleHeadlineTime() { + _uiState.update { + it.copy(headlinePreferences = it.headlinePreferences.copy(showTime = !it.headlinePreferences.showTime)) + } + } + + fun toggleHeadlineSource() { + _uiState.update { + it.copy(headlinePreferences = it.headlinePreferences.copy(showSource = !it.headlinePreferences.showSource)) + } + } + + fun toggleFactsSource() { + _uiState.update { + it.copy(factsPreferences = it.factsPreferences.copy(showSource = !it.factsPreferences.showSource)) + } + } + + fun togglePricePair(pair: TradingPair) { + _uiState.update { + val current = it.pricePreferences.enabledPairs.toMutableList() + if (pair in current) { + if (current.size > 1) current.remove(pair) + } else { + current.add(pair) + } + it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = current.sortedBy { p -> p.position })) + } + } + + fun selectPricePeriod(period: GraphPeriod) { + _uiState.update { + it.copy(pricePreferences = it.pricePreferences.copy(period = period)) + } + } + + fun togglePriceSource() { + _uiState.update { + it.copy(pricePreferences = it.pricePreferences.copy(showSource = !it.pricePreferences.showSource)) + } + } + + fun resetPreferences() { + _uiState.update { + when (it.type) { + AppWidgetType.BLOCKS -> it.copy(blocksPreferences = BlocksPreferences()) + AppWidgetType.PRICE -> it.copy(pricePreferences = PricePreferences()) + AppWidgetType.WEATHER -> it.copy(weatherPreferences = WeatherPreferences()) + AppWidgetType.HEADLINES -> it.copy(headlinePreferences = HeadlinePreferences()) + AppWidgetType.FACTS -> it.copy(factsPreferences = FactsPreferences()) + } + } + } + + fun saveAndFinish(onComplete: () -> Unit) { + viewModelScope.launch { + val state = _uiState.value + preferencesStore.registerWidget(state.appWidgetId, state.type) + preferencesStore.updateEntry(state.appWidgetId) { entry -> + entry.copy( + blocksPreferences = state.blocksPreferences.toHome(), + pricePreferences = state.pricePreferences.toHome(), + weatherPreferences = state.weatherPreferences.toHome(), + headlinesPreferences = state.headlinePreferences.toHome(), + factsPreferences = state.factsPreferences.toHome(), + ) + } + onComplete() + } + } +} + +data class AppWidgetConfigUiState( + val appWidgetId: Int = -1, + val type: AppWidgetType = AppWidgetType.BLOCKS, + val blocksPreferences: BlocksPreferences = BlocksPreferences(), + val pricePreferences: PricePreferences = PricePreferences(), + val weatherPreferences: WeatherPreferences = WeatherPreferences(), + val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), + val factsPreferences: FactsPreferences = FactsPreferences(), + val blockModel: BlockModel? = null, + val currentFact: String = "", +) + +enum class BlocksField { BLOCK, TIME, DATE, TRANSACTIONS, SIZE, SOURCE } +enum class WeatherField { TITLE, DESCRIPTION, CURRENT_FEE, NEXT_BLOCK } + +private fun HomeBlocksPreferences.toInApp() = BlocksPreferences( + showBlock = showBlock, + showTime = showTime, + showDate = showDate, + showTransactions = showTransactions, + showSize = showSize, + showSource = showSource, +) + +private fun HomePricePreferences.toInApp() = PricePreferences( + enabledPairs = enabledPairs, + period = period, + showSource = showSource, +) + +private fun HomeWeatherPreferences.toInApp() = WeatherPreferences( + showTitle = showTitle, + showDescription = showDescription, + showCurrentFee = showCurrentFee, + showNextBlockFee = showNextBlockFee, +) + +private fun HomeHeadlinesPreferences.toInApp() = HeadlinePreferences( + showTime = showTime, + showSource = showSource, +) + +private fun HomeFactsPreferences.toInApp() = FactsPreferences( + showSource = showSource, +) + +private fun BlocksPreferences.toHome() = HomeBlocksPreferences( + showBlock = showBlock, + showTime = showTime, + showDate = showDate, + showTransactions = showTransactions, + showSize = showSize, + showSource = showSource, +) + +private fun PricePreferences.toHome() = HomePricePreferences( + enabledPairs = enabledPairs, + period = period ?: GraphPeriod.ONE_DAY, + showSource = showSource, +) + +private fun WeatherPreferences.toHome() = HomeWeatherPreferences( + showTitle = showTitle, + showDescription = showDescription, + showCurrentFee = showCurrentFee, + showNextBlockFee = showNextBlockFee, +) + +private fun HeadlinePreferences.toHome() = HomeHeadlinesPreferences( + showTime = showTime, + showSource = showSource, +) + +private fun FactsPreferences.toHome() = HomeFactsPreferences( + showSource = showSource, +) From b19db436039adb928ebf9db84d68cbbf97fcd87b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 15:01:31 -0300 Subject: [PATCH 009/100] feat: resources --- .../res/layout/glance_default_loading_layout.xml | 15 +++++++++++++++ .../main/res/xml-v31/appwidget_info_blocks.xml | 12 ++++++++++++ app/src/main/res/xml/appwidget_info_blocks.xml | 12 ++++++++++++ app/src/main/res/xml/appwidget_info_facts.xml | 12 ++++++++++++ app/src/main/res/xml/appwidget_info_headlines.xml | 12 ++++++++++++ app/src/main/res/xml/appwidget_info_price.xml | 12 ++++++++++++ app/src/main/res/xml/appwidget_info_weather.xml | 12 ++++++++++++ 7 files changed, 87 insertions(+) create mode 100644 app/src/main/res/layout/glance_default_loading_layout.xml create mode 100644 app/src/main/res/xml-v31/appwidget_info_blocks.xml create mode 100644 app/src/main/res/xml/appwidget_info_blocks.xml create mode 100644 app/src/main/res/xml/appwidget_info_facts.xml create mode 100644 app/src/main/res/xml/appwidget_info_headlines.xml create mode 100644 app/src/main/res/xml/appwidget_info_price.xml create mode 100644 app/src/main/res/xml/appwidget_info_weather.xml diff --git a/app/src/main/res/layout/glance_default_loading_layout.xml b/app/src/main/res/layout/glance_default_loading_layout.xml new file mode 100644 index 000000000..b294f56e7 --- /dev/null +++ b/app/src/main/res/layout/glance_default_loading_layout.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/xml-v31/appwidget_info_blocks.xml b/app/src/main/res/xml-v31/appwidget_info_blocks.xml new file mode 100644 index 000000000..2e9c4fa65 --- /dev/null +++ b/app/src/main/res/xml-v31/appwidget_info_blocks.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/xml/appwidget_info_blocks.xml b/app/src/main/res/xml/appwidget_info_blocks.xml new file mode 100644 index 000000000..2e9c4fa65 --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_blocks.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/xml/appwidget_info_facts.xml b/app/src/main/res/xml/appwidget_info_facts.xml new file mode 100644 index 000000000..2b4491977 --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_facts.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/xml/appwidget_info_headlines.xml b/app/src/main/res/xml/appwidget_info_headlines.xml new file mode 100644 index 000000000..f45be282f --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_headlines.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml new file mode 100644 index 000000000..ca0c818ad --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/xml/appwidget_info_weather.xml b/app/src/main/res/xml/appwidget_info_weather.xml new file mode 100644 index 000000000..4c5f9cd33 --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_weather.xml @@ -0,0 +1,12 @@ + + From 32b14d2344fe45431ab48f77f6cbe2167a654dcb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 15:01:46 -0300 Subject: [PATCH 010/100] feat: overload with GraphPeriod --- app/src/main/java/to/bitkit/data/widgets/PriceService.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt index 3b859bad0..159bde54c 100644 --- a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt +++ b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt @@ -38,9 +38,12 @@ class PriceService @Inject constructor( override val refreshInterval = 1.minutes private val sourceLabel = "Bitfinex.com" - override suspend fun fetchData(): Result = runCatching { + override suspend fun fetchData(): Result { val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY + return fetchData(period) + } + suspend fun fetchData(period: GraphPeriod): Result = runCatching { val widgets = TradingPair.entries.mapNotNull { pair -> runCatching { fetchPairData(pair = pair, period = period) } .onFailure { Logger.warn(e = it, msg = "Failed to fetch ${pair.ticker}", context = TAG) } From 4dca8ce5a110b5c4872f358050a16b3a913a96ce Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 15:02:47 -0300 Subject: [PATCH 011/100] feat: manifest settings --- app/src/main/AndroidManifest.xml | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 838a05b4e..d5df8cc96 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -171,6 +171,82 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fac5d419e369610af7ad45701157f92779402b8a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 6 Apr 2026 06:31:54 -0300 Subject: [PATCH 012/100] refactor: remove duplicated strings --- app/src/main/AndroidManifest.xml | 10 +++++----- .../appwidget/config/AppWidgetConfigScreen.kt | 2 +- .../appwidget/ui/facts/FactsGlanceContent.kt | 2 +- .../appwidget/ui/weather/WeatherGlanceContent.kt | 16 ++++++++-------- app/src/main/res/values/strings.xml | 8 ++++++++ 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d5df8cc96..4046fd970 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -187,7 +187,7 @@ + android:label="@string/widgets__blocks__name"> @@ -200,7 +200,7 @@ + android:label="@string/widgets__price__name"> @@ -213,7 +213,7 @@ + android:label="@string/widgets__weather__name"> @@ -226,7 +226,7 @@ + android:label="@string/widgets__news__name"> @@ -239,7 +239,7 @@ + android:label="@string/widgets__facts__name"> diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index f829baf51..0dcbbf143 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -203,7 +203,7 @@ private fun PriceConfigContent( modifier = Modifier.weight(1f), ) PrimaryButton( - text = stringResource(R.string.appwidget__config__confirm), + text = stringResource(R.string.common__save), fullWidth = false, onClick = onSave, modifier = Modifier.weight(1f), diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt index e22673dac..4c816e603 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt @@ -61,7 +61,7 @@ fun FactsGlanceContent( if (prefs.showSource) { Spacer(modifier = GlanceModifier.height(8.dp)) Text( - text = context.getString(R.string.appwidget__facts__source), + text = context.getString(R.string.widgets__widget__source), style = TextStyle( color = GlanceColors.textTertiary, fontSize = 11.sp, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt index 1f8596f60..f925f990c 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt @@ -75,14 +75,14 @@ fun WeatherGlanceContent( if (prefs.showCurrentFee) { GlanceDataRow( - label = context.getString(R.string.appwidget__weather__current_fee), + label = context.getString(R.string.widgets__weather__current_fee), value = weather.currentFee, ) } if (prefs.showNextBlockFee) { GlanceDataRow( - label = context.getString(R.string.appwidget__weather__next_block), + label = context.getString(R.string.widgets__weather__next_block), value = "${weather.nextBlockFee} sat/vB", ) } @@ -90,14 +90,14 @@ fun WeatherGlanceContent( } private fun conditionLabel(context: Context, condition: FeeCondition): String = when (condition) { - FeeCondition.GOOD -> context.getString(R.string.appwidget__weather__good) - FeeCondition.AVERAGE -> context.getString(R.string.appwidget__weather__average) - FeeCondition.POOR -> context.getString(R.string.appwidget__weather__poor) + FeeCondition.GOOD -> context.getString(R.string.widgets__weather__condition__good__title) + FeeCondition.AVERAGE -> context.getString(R.string.widgets__weather__condition__average__title) + FeeCondition.POOR -> context.getString(R.string.widgets__weather__condition__poor__title) } private fun conditionDescription(context: Context, condition: FeeCondition): String = when (condition) { - FeeCondition.GOOD -> context.getString(R.string.appwidget__weather__good_desc) - FeeCondition.AVERAGE -> context.getString(R.string.appwidget__weather__average_desc) - FeeCondition.POOR -> context.getString(R.string.appwidget__weather__poor_desc) + FeeCondition.GOOD -> context.getString(R.string.widgets__weather__condition__good__description) + FeeCondition.AVERAGE -> context.getString(R.string.widgets__weather__condition__average__description) + FeeCondition.POOR -> context.getString(R.string.widgets__weather__condition__poor__description) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94462606a..f9ace0140 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,13 @@ + Latest Bitcoin block info + Bitcoin facts + Latest Bitcoin news + Loadingโ€ฆ + Bitcoin price tracker + Time period + Trading pairs + Bitcoin network fee weather Store your bitcoin Back up Buy some bitcoin From 1c29a8877dd9ff397b5d566ea51eba8e463f5303 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 6 Apr 2026 07:35:11 -0300 Subject: [PATCH 013/100] refactor: reuse existing colors --- .../bitkit/appwidget/ui/price/PriceGlanceContent.kt | 9 +++++---- .../java/to/bitkit/appwidget/ui/theme/GlanceColors.kt | 11 +++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 2adf27b22..c09387ce3 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -19,6 +19,7 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.ui.theme.Colors import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -95,13 +96,13 @@ private fun PriceRow(widget: PriceWidgetData) { style = TextStyle( color = if (widget.change.isPositive) { androidx.glance.color.ColorProvider( - day = GlanceColors.Green, - night = GlanceColors.Green, + day = Colors.Green, + night = Colors.Green, ) } else { androidx.glance.color.ColorProvider( - day = GlanceColors.Red, - night = GlanceColors.Red, + day = Colors.Red, + night = Colors.Red, ) }, fontSize = 13.sp, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt new file mode 100644 index 000000000..427bb161d --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt @@ -0,0 +1,11 @@ +package to.bitkit.appwidget.ui.theme + +import androidx.glance.color.ColorProvider +import to.bitkit.ui.theme.Colors + +object GlanceColors { + val cardBackgroundProvider = ColorProvider(day = Colors.Gray5, night = Colors.Gray5) + val textPrimary = ColorProvider(day = Colors.White, night = Colors.White) + val textSecondary = ColorProvider(day = Colors.White64, night = Colors.White64) + val textTertiary = ColorProvider(day = Colors.White50, night = Colors.White50) +} From ae3225815b8d27f4929b7fd07478ecf9a3546e61 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 6 Apr 2026 07:52:51 -0300 Subject: [PATCH 014/100] refactor: por the existing design system to Glance --- .../ui/blocks/BlocksGlanceContent.kt | 21 ++------ .../appwidget/ui/components/GlanceDataRow.kt | 17 ++----- .../appwidget/ui/facts/FactsGlanceContent.kt | 27 ++-------- .../ui/headlines/HeadlinesGlanceContent.kt | 26 ++-------- .../appwidget/ui/price/PriceGlanceContent.kt | 49 +++++-------------- .../appwidget/ui/theme/GlanceTextStyles.kt | 14 ++++++ .../ui/weather/WeatherGlanceContent.kt | 27 ++-------- 7 files changed, 47 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt index 5a012736a..6e893e641 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt @@ -3,19 +3,16 @@ package to.bitkit.appwidget.ui.blocks import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceDataRow import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.models.widget.BlockModel @Composable @@ -30,11 +27,7 @@ fun BlocksGlanceContent( GlanceWidgetScaffold(onClick = launchIntent) { Text( text = context.getString(to.bitkit.R.string.widgets__blocks__name), - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.bodyMSB, ) Spacer(modifier = GlanceModifier.height(8.dp)) @@ -42,10 +35,7 @@ fun BlocksGlanceContent( if (block == null) { Text( text = context.getString(to.bitkit.R.string.appwidget__loading), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - ), + style = GlanceTextStyles.captionB, ) return@GlanceWidgetScaffold } @@ -69,10 +59,7 @@ fun BlocksGlanceContent( Spacer(modifier = GlanceModifier.height(4.dp)) Text( text = block.source, - style = TextStyle( - color = GlanceColors.textTertiary, - fontSize = 11.sp, - ), + style = GlanceTextStyles.source, modifier = GlanceModifier.fillMaxWidth().padding(top = 4.dp), ) } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt index f7fba499c..192f42bf7 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt @@ -2,16 +2,13 @@ package to.bitkit.appwidget.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier import androidx.glance.layout.Alignment import androidx.glance.layout.Row import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.padding -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle -import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles @Composable fun GlanceDataRow( @@ -25,19 +22,11 @@ fun GlanceDataRow( ) { Text( text = label, - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - ), + style = GlanceTextStyles.captionB, ) Text( text = value, - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - ), + style = GlanceTextStyles.bodySSB, ) } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt index 4c816e603..e66298bfb 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt @@ -3,17 +3,14 @@ package to.bitkit.appwidget.ui.facts import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier import androidx.glance.layout.Spacer import androidx.glance.layout.height -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles @Composable fun FactsGlanceContent( @@ -27,11 +24,7 @@ fun FactsGlanceContent( GlanceWidgetScaffold(onClick = launchIntent) { Text( text = context.getString(R.string.widgets__facts__name), - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.bodyMSB, ) Spacer(modifier = GlanceModifier.height(8.dp)) @@ -39,10 +32,7 @@ fun FactsGlanceContent( if (facts.isEmpty()) { Text( text = context.getString(R.string.appwidget__loading), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - ), + style = GlanceTextStyles.captionB, ) return@GlanceWidgetScaffold } @@ -50,11 +40,7 @@ fun FactsGlanceContent( val fact = facts.random() Text( text = fact, - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - ), + style = GlanceTextStyles.bodySSB, maxLines = 4, ) @@ -62,10 +48,7 @@ fun FactsGlanceContent( Spacer(modifier = GlanceModifier.height(8.dp)) Text( text = context.getString(R.string.widgets__widget__source), - style = TextStyle( - color = GlanceColors.textTertiary, - fontSize = 11.sp, - ), + style = GlanceTextStyles.source, ) } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt index 74c92fcb5..70af2ef4d 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt @@ -3,20 +3,18 @@ package to.bitkit.appwidget.ui.headlines import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier import androidx.glance.layout.Column import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.ArticleDTO @Composable @@ -31,11 +29,7 @@ fun HeadlinesGlanceContent( GlanceWidgetScaffold(onClick = launchIntent) { Text( text = context.getString(R.string.widgets__news__name), - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.bodyMSB, ) Spacer(modifier = GlanceModifier.height(8.dp)) @@ -43,10 +37,7 @@ fun HeadlinesGlanceContent( if (articles.isEmpty()) { Text( text = context.getString(R.string.appwidget__loading), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - ), + style = GlanceTextStyles.captionB, ) return@GlanceWidgetScaffold } @@ -56,11 +47,7 @@ fun HeadlinesGlanceContent( Column(modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp)) { Text( text = article.title, - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - ), + style = GlanceTextStyles.captionB.copy(color = GlanceColors.textPrimary), maxLines = 2, ) if (prefs.showTime || prefs.showSource) { @@ -71,10 +58,7 @@ fun HeadlinesGlanceContent( } Text( text = meta, - style = TextStyle( - color = GlanceColors.textTertiary, - fontSize = 11.sp, - ), + style = GlanceTextStyles.source, maxLines = 1, ) } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index c09387ce3..0397ee895 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -3,8 +3,8 @@ package to.bitkit.appwidget.ui.price import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier +import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Row import androidx.glance.layout.Spacer @@ -12,16 +12,14 @@ import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.layout.width -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceColors -import to.bitkit.ui.theme.Colors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData +import to.bitkit.ui.theme.Colors @Composable fun PriceGlanceContent( @@ -35,11 +33,7 @@ fun PriceGlanceContent( GlanceWidgetScaffold(onClick = launchIntent) { Text( text = context.getString(R.string.widgets__price__name), - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.bodyMSB, ) Spacer(modifier = GlanceModifier.height(8.dp)) @@ -47,10 +41,7 @@ fun PriceGlanceContent( if (price == null) { Text( text = context.getString(R.string.appwidget__loading), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - ), + style = GlanceTextStyles.captionB, ) return@GlanceWidgetScaffold } @@ -67,10 +58,7 @@ fun PriceGlanceContent( Spacer(modifier = GlanceModifier.height(4.dp)) Text( text = price.source, - style = TextStyle( - color = GlanceColors.textTertiary, - fontSize = 11.sp, - ), + style = GlanceTextStyles.source, ) } } @@ -84,37 +72,22 @@ private fun PriceRow(widget: PriceWidgetData) { ) { Text( text = "${widget.pair.symbol}${widget.price}", - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.subtitle, ) Spacer(modifier = GlanceModifier.width(8.dp)) Text( text = widget.change.formatted, - style = TextStyle( + style = GlanceTextStyles.captionB.copy( color = if (widget.change.isPositive) { - androidx.glance.color.ColorProvider( - day = Colors.Green, - night = Colors.Green, - ) + ColorProvider(day = Colors.Green, night = Colors.Green) } else { - androidx.glance.color.ColorProvider( - day = Colors.Red, - night = Colors.Red, - ) + ColorProvider(day = Colors.Red, night = Colors.Red) }, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, ), ) } Text( text = widget.pair.displayName, - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 12.sp, - ), + style = GlanceTextStyles.footnoteM, ) } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt new file mode 100644 index 000000000..a4d395e96 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt @@ -0,0 +1,14 @@ +package to.bitkit.appwidget.ui.theme + +import androidx.compose.ui.unit.sp +import androidx.glance.text.FontWeight +import androidx.glance.text.TextStyle + +object GlanceTextStyles { + val subtitle = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) + val bodyMSB = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) + val bodySSB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) + val captionB = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) + val footnoteM = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) + val source = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Normal, color = GlanceColors.textTertiary) +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt index f925f990c..6c11a0f87 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt @@ -3,18 +3,15 @@ package to.bitkit.appwidget.ui.weather import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.glance.GlanceModifier import androidx.glance.layout.Spacer import androidx.glance.layout.height -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceDataRow import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.FeeCondition import to.bitkit.data.dto.WeatherDTO @@ -30,11 +27,7 @@ fun WeatherGlanceContent( GlanceWidgetScaffold(onClick = launchIntent) { Text( text = context.getString(R.string.widgets__weather__name), - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.bodyMSB, ) Spacer(modifier = GlanceModifier.height(8.dp)) @@ -42,10 +35,7 @@ fun WeatherGlanceContent( if (weather == null) { Text( text = context.getString(R.string.appwidget__loading), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 13.sp, - ), + style = GlanceTextStyles.captionB, ) return@GlanceWidgetScaffold } @@ -53,11 +43,7 @@ fun WeatherGlanceContent( if (prefs.showTitle) { Text( text = "${weather.condition.icon} ${conditionLabel(context, weather.condition)}", - style = TextStyle( - color = GlanceColors.textPrimary, - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - ), + style = GlanceTextStyles.subtitle, ) Spacer(modifier = GlanceModifier.height(4.dp)) } @@ -65,10 +51,7 @@ fun WeatherGlanceContent( if (prefs.showDescription) { Text( text = conditionDescription(context, weather.condition), - style = TextStyle( - color = GlanceColors.textSecondary, - fontSize = 12.sp, - ), + style = GlanceTextStyles.footnoteM, ) Spacer(modifier = GlanceModifier.height(4.dp)) } From 62be2c2dd4834d7cb67ff627c98e551e796da702 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 6 Apr 2026 07:53:00 -0300 Subject: [PATCH 015/100] chore: lint --- app/src/main/java/to/bitkit/di/HttpModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index f652939b3..4a1f551c6 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -18,9 +18,9 @@ import io.ktor.http.contentType import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import to.bitkit.utils.UrlValidator import to.bitkit.utils.AppError import to.bitkit.utils.Logger +import to.bitkit.utils.UrlValidator import javax.inject.Singleton import io.ktor.client.plugins.logging.Logger as KtorLogger From 9ebaffedea2005c1800ffe558927938f36d1815d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Apr 2026 14:27:38 -0300 Subject: [PATCH 016/100] refactor: remove boilerplate --- app/src/main/AndroidManifest.xml | 51 ------ .../appwidget/AppWidgetDataRepository.kt | 27 ---- .../appwidget/AppWidgetPreferencesStore.kt | 20 --- .../appwidget/AppWidgetRefreshWorker.kt | 49 +----- .../appwidget/config/AppWidgetConfigScreen.kt | 63 -------- .../config/AppWidgetConfigViewModel.kt | 147 +----------------- .../appwidget/model/AppWidgetPreferences.kt | 44 ------ .../ui/blocks/BlocksGlanceContent.kt | 67 -------- .../ui/blocks/BlocksGlanceReceiver.kt | 19 --- .../appwidget/ui/blocks/BlocksGlanceWidget.kt | 33 ---- .../appwidget/ui/facts/FactsGlanceContent.kt | 55 ------- .../appwidget/ui/facts/FactsGlanceReceiver.kt | 20 --- .../appwidget/ui/facts/FactsGlanceWidget.kt | 30 ---- .../ui/headlines/HeadlinesGlanceContent.kt | 71 --------- .../ui/headlines/HeadlinesGlanceReceiver.kt | 20 --- .../ui/headlines/HeadlinesGlanceWidget.kt | 30 ---- .../ui/weather/WeatherGlanceContent.kt | 86 ---------- .../ui/weather/WeatherGlanceReceiver.kt | 20 --- .../ui/weather/WeatherGlanceWidget.kt | 30 ---- app/src/main/res/values/strings.xml | 4 - .../res/xml-v31/appwidget_info_blocks.xml | 12 -- .../main/res/xml/appwidget_info_blocks.xml | 12 -- app/src/main/res/xml/appwidget_info_facts.xml | 12 -- .../main/res/xml/appwidget_info_headlines.xml | 12 -- .../main/res/xml/appwidget_info_weather.xml | 12 -- 25 files changed, 6 insertions(+), 940 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt delete mode 100644 app/src/main/res/xml-v31/appwidget_info_blocks.xml delete mode 100644 app/src/main/res/xml/appwidget_info_blocks.xml delete mode 100644 app/src/main/res/xml/appwidget_info_facts.xml delete mode 100644 app/src/main/res/xml/appwidget_info_headlines.xml delete mode 100644 app/src/main/res/xml/appwidget_info_weather.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4046fd970..3f842d93f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -183,19 +183,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt index dbdabd504..1bbbcd6b4 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt @@ -2,16 +2,9 @@ package to.bitkit.appwidget import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import to.bitkit.data.dto.ArticleDTO -import to.bitkit.data.dto.BlockDTO -import to.bitkit.data.dto.WeatherDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO -import to.bitkit.data.widgets.BlocksService -import to.bitkit.data.widgets.FactsService -import to.bitkit.data.widgets.NewsService import to.bitkit.data.widgets.PriceService -import to.bitkit.data.widgets.WeatherService import to.bitkit.di.IoDispatcher import javax.inject.Inject import javax.inject.Singleton @@ -20,29 +13,9 @@ import javax.inject.Singleton class AppWidgetDataRepository @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val priceService: PriceService, - private val blocksService: BlocksService, - private val weatherService: WeatherService, - private val newsService: NewsService, - private val factsService: FactsService, ) { - suspend fun fetchBlockData(): Result = withContext(ioDispatcher) { - blocksService.fetchData() - } - suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result = withContext(ioDispatcher) { priceService.fetchData(period) } - - suspend fun fetchWeatherData(): Result = withContext(ioDispatcher) { - weatherService.fetchData() - } - - suspend fun fetchHeadlines(): Result> = withContext(ioDispatcher) { - newsService.fetchData() - } - - suspend fun fetchFacts(): Result> = withContext(ioDispatcher) { - factsService.fetchData() - } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index 2c3a6c693..7e0398310 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -10,9 +10,6 @@ import kotlinx.coroutines.flow.map import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.data.dto.ArticleDTO -import to.bitkit.data.dto.BlockDTO -import to.bitkit.data.dto.WeatherDTO import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.serializers.AppWidgetDataSerializer import javax.inject.Inject @@ -23,7 +20,6 @@ private val Context.appWidgetDataStore: DataStore by dataStore( serializer = AppWidgetDataSerializer, ) -@Suppress("TooManyFunctions") @Singleton class AppWidgetPreferencesStore @Inject constructor( @ApplicationContext private val context: Context, @@ -76,23 +72,7 @@ class AppWidgetPreferencesStore @Inject constructor( fun hasWidgetsOfType(type: AppWidgetType): Flow = data.map { it.entries.any { entry -> entry.type == type } } - suspend fun cacheBlockData(block: BlockDTO) { - store.updateData { it.copy(cachedBlock = block) } - } - - suspend fun cacheWeatherData(weather: WeatherDTO) { - store.updateData { it.copy(cachedWeather = weather) } - } - suspend fun cachePriceData(price: PriceDTO) { store.updateData { it.copy(cachedPrice = price) } } - - suspend fun cacheArticles(articles: List) { - store.updateData { it.copy(cachedArticles = articles) } - } - - suspend fun cacheFacts(facts: List) { - store.updateData { it.copy(cachedFacts = facts) } - } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 2706caa58..ea186e5e6 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -16,16 +16,8 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.appwidget.ui.blocks.BlocksGlanceReceiver -import to.bitkit.appwidget.ui.blocks.BlocksGlanceWidget -import to.bitkit.appwidget.ui.facts.FactsGlanceReceiver -import to.bitkit.appwidget.ui.facts.FactsGlanceWidget -import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceReceiver -import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget import to.bitkit.appwidget.ui.price.PriceGlanceReceiver import to.bitkit.appwidget.ui.price.PriceGlanceWidget -import to.bitkit.appwidget.ui.weather.WeatherGlanceReceiver -import to.bitkit.appwidget.ui.weather.WeatherGlanceWidget import to.bitkit.utils.Logger import java.util.concurrent.TimeUnit @@ -45,40 +37,12 @@ class AppWidgetRefreshWorker @AssistedInject constructor( for (type in activeTypes) { when (type) { - AppWidgetType.BLOCKS -> dataRepository.fetchBlockData() - .onSuccess { - preferencesStore.cacheBlockData(it) - BlocksGlanceWidget().updateAll(appContext) - } - .onFailure { Logger.warn("Failed to refresh blocks", e = it, context = TAG) } - AppWidgetType.PRICE -> dataRepository.fetchPriceData() .onSuccess { preferencesStore.cachePriceData(it) PriceGlanceWidget().updateAll(appContext) } .onFailure { Logger.warn("Failed to refresh price", e = it, context = TAG) } - - AppWidgetType.WEATHER -> dataRepository.fetchWeatherData() - .onSuccess { - preferencesStore.cacheWeatherData(it) - WeatherGlanceWidget().updateAll(appContext) - } - .onFailure { Logger.warn("Failed to refresh weather", e = it, context = TAG) } - - AppWidgetType.HEADLINES -> dataRepository.fetchHeadlines() - .onSuccess { - preferencesStore.cacheArticles(it) - HeadlinesGlanceWidget().updateAll(appContext) - } - .onFailure { Logger.warn("Failed to refresh headlines", e = it, context = TAG) } - - AppWidgetType.FACTS -> dataRepository.fetchFacts() - .onSuccess { - preferencesStore.cacheFacts(it) - FactsGlanceWidget().updateAll(appContext) - } - .onFailure { Logger.warn("Failed to refresh facts", e = it, context = TAG) } } } @@ -120,16 +84,9 @@ class AppWidgetRefreshWorker @AssistedInject constructor( fun cancelIfNoWidgets(context: Context) { val manager = AppWidgetManager.getInstance(context) - val receivers = listOf( - BlocksGlanceReceiver::class.java, - PriceGlanceReceiver::class.java, - WeatherGlanceReceiver::class.java, - HeadlinesGlanceReceiver::class.java, - FactsGlanceReceiver::class.java, - ) - val hasAny = receivers.any { receiver -> - manager.getAppWidgetIds(ComponentName(context, receiver)).isNotEmpty() - } + val hasAny = manager.getAppWidgetIds( + ComponentName(context, PriceGlanceReceiver::class.java), + ).isNotEmpty() if (!hasAny) { WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) } diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 0dcbbf143..77551a9ae 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -25,18 +25,12 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair -import to.bitkit.models.widget.ArticleModel -import to.bitkit.models.widget.BlockModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.widgets.blocks.BlocksEditContent -import to.bitkit.ui.screens.widgets.facts.FactsEditContent -import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditContent -import to.bitkit.ui.screens.widgets.weather.WeatherEditContent import to.bitkit.ui.theme.Colors @Composable @@ -48,63 +42,6 @@ fun AppWidgetConfigScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() when (state.type) { - AppWidgetType.BLOCKS -> BlocksEditContent( - onBack = onCancel, - blocksPreferences = state.blocksPreferences, - block = state.blockModel ?: BlockModel( - height = "", - time = "", - date = "", - transactionCount = "", - size = "", - source = "", - ), - onClickShowBlock = { viewModel.toggleBlocksShow(BlocksField.BLOCK) }, - onClickShowTime = { viewModel.toggleBlocksShow(BlocksField.TIME) }, - onClickShowDate = { viewModel.toggleBlocksShow(BlocksField.DATE) }, - onClickShowTransactions = { viewModel.toggleBlocksShow(BlocksField.TRANSACTIONS) }, - onClickShowSize = { viewModel.toggleBlocksShow(BlocksField.SIZE) }, - onClickShowSource = { viewModel.toggleBlocksShow(BlocksField.SOURCE) }, - onClickReset = { viewModel.resetPreferences() }, - onClickPreview = { viewModel.saveAndFinish(onConfirm) }, - ) - - AppWidgetType.WEATHER -> WeatherEditContent( - onBack = onCancel, - weather = null, - weatherPreferences = state.weatherPreferences, - onClickShowTitle = { viewModel.toggleWeatherShow(WeatherField.TITLE) }, - onClickShowDescription = { viewModel.toggleWeatherShow(WeatherField.DESCRIPTION) }, - onClickShowCurrentFee = { viewModel.toggleWeatherShow(WeatherField.CURRENT_FEE) }, - onClickShowNextBlockFee = { viewModel.toggleWeatherShow(WeatherField.NEXT_BLOCK) }, - onClickReset = { viewModel.resetPreferences() }, - onClickPreview = { viewModel.saveAndFinish(onConfirm) }, - ) - - AppWidgetType.HEADLINES -> HeadlinesEditContent( - onBack = onCancel, - headlinePreferences = state.headlinePreferences, - article = ArticleModel( - title = "", - timeAgo = "", - link = "", - publisher = "", - ), - onClickTime = { viewModel.toggleHeadlineTime() }, - onClickShowSource = { viewModel.toggleHeadlineSource() }, - onClickReset = { viewModel.resetPreferences() }, - onClickPreview = { viewModel.saveAndFinish(onConfirm) }, - ) - - AppWidgetType.FACTS -> FactsEditContent( - onBack = onCancel, - factsPreferences = state.factsPreferences, - fact = state.currentFact, - onClickShowSource = { viewModel.toggleFactsSource() }, - onClickReset = { viewModel.resetPreferences() }, - onClickPreview = { viewModel.saveAndFinish(onConfirm) }, - ) - AppWidgetType.PRICE -> PriceConfigContent( state = state, onTogglePair = { viewModel.togglePricePair(it) }, diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index a4dcd3644..6838dd44b 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -6,28 +6,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.appwidget.model.HomeBlocksPreferences -import to.bitkit.appwidget.model.HomeFactsPreferences -import to.bitkit.appwidget.model.HomeHeadlinesPreferences import to.bitkit.appwidget.model.HomePricePreferences -import to.bitkit.appwidget.model.HomeWeatherPreferences import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair -import to.bitkit.models.widget.BlockModel -import to.bitkit.models.widget.BlocksPreferences -import to.bitkit.models.widget.FactsPreferences -import to.bitkit.models.widget.HeadlinePreferences import to.bitkit.models.widget.PricePreferences -import to.bitkit.models.widget.WeatherPreferences -import to.bitkit.models.widget.toBlockModel import javax.inject.Inject -@Suppress("TooManyFunctions") @HiltViewModel class AppWidgetConfigViewModel @Inject constructor( private val preferencesStore: AppWidgetPreferencesStore, @@ -39,73 +27,17 @@ class AppWidgetConfigViewModel @Inject constructor( fun init(appWidgetId: Int, type: AppWidgetType) { viewModelScope.launch { val entry = preferencesStore.getEntry(appWidgetId) - val data = preferencesStore.data.first() _uiState.update { it.copy( appWidgetId = appWidgetId, type = type, - blocksPreferences = entry?.blocksPreferences?.toInApp() ?: BlocksPreferences(), pricePreferences = entry?.pricePreferences?.toInApp() ?: PricePreferences(), - weatherPreferences = entry?.weatherPreferences?.toInApp() ?: WeatherPreferences(), - headlinePreferences = entry?.headlinesPreferences?.toInApp() - ?: HeadlinePreferences(), - factsPreferences = entry?.factsPreferences?.toInApp() ?: FactsPreferences(), - blockModel = data.cachedBlock?.toBlockModel(), - currentFact = data.cachedFacts.randomOrNull() ?: "", ) } } } - fun toggleBlocksShow(field: BlocksField) { - _uiState.update { - val p = it.blocksPreferences - it.copy( - blocksPreferences = when (field) { - BlocksField.BLOCK -> p.copy(showBlock = !p.showBlock) - BlocksField.TIME -> p.copy(showTime = !p.showTime) - BlocksField.DATE -> p.copy(showDate = !p.showDate) - BlocksField.TRANSACTIONS -> p.copy(showTransactions = !p.showTransactions) - BlocksField.SIZE -> p.copy(showSize = !p.showSize) - BlocksField.SOURCE -> p.copy(showSource = !p.showSource) - }, - ) - } - } - - fun toggleWeatherShow(field: WeatherField) { - _uiState.update { - val p = it.weatherPreferences - it.copy( - weatherPreferences = when (field) { - WeatherField.TITLE -> p.copy(showTitle = !p.showTitle) - WeatherField.DESCRIPTION -> p.copy(showDescription = !p.showDescription) - WeatherField.CURRENT_FEE -> p.copy(showCurrentFee = !p.showCurrentFee) - WeatherField.NEXT_BLOCK -> p.copy(showNextBlockFee = !p.showNextBlockFee) - }, - ) - } - } - - fun toggleHeadlineTime() { - _uiState.update { - it.copy(headlinePreferences = it.headlinePreferences.copy(showTime = !it.headlinePreferences.showTime)) - } - } - - fun toggleHeadlineSource() { - _uiState.update { - it.copy(headlinePreferences = it.headlinePreferences.copy(showSource = !it.headlinePreferences.showSource)) - } - } - - fun toggleFactsSource() { - _uiState.update { - it.copy(factsPreferences = it.factsPreferences.copy(showSource = !it.factsPreferences.showSource)) - } - } - fun togglePricePair(pair: TradingPair) { _uiState.update { val current = it.pricePreferences.enabledPairs.toMutableList() @@ -131,15 +63,7 @@ class AppWidgetConfigViewModel @Inject constructor( } fun resetPreferences() { - _uiState.update { - when (it.type) { - AppWidgetType.BLOCKS -> it.copy(blocksPreferences = BlocksPreferences()) - AppWidgetType.PRICE -> it.copy(pricePreferences = PricePreferences()) - AppWidgetType.WEATHER -> it.copy(weatherPreferences = WeatherPreferences()) - AppWidgetType.HEADLINES -> it.copy(headlinePreferences = HeadlinePreferences()) - AppWidgetType.FACTS -> it.copy(factsPreferences = FactsPreferences()) - } - } + _uiState.update { it.copy(pricePreferences = PricePreferences()) } } fun saveAndFinish(onComplete: () -> Unit) { @@ -147,13 +71,7 @@ class AppWidgetConfigViewModel @Inject constructor( val state = _uiState.value preferencesStore.registerWidget(state.appWidgetId, state.type) preferencesStore.updateEntry(state.appWidgetId) { entry -> - entry.copy( - blocksPreferences = state.blocksPreferences.toHome(), - pricePreferences = state.pricePreferences.toHome(), - weatherPreferences = state.weatherPreferences.toHome(), - headlinesPreferences = state.headlinePreferences.toHome(), - factsPreferences = state.factsPreferences.toHome(), - ) + entry.copy(pricePreferences = state.pricePreferences.toHome()) } onComplete() } @@ -162,26 +80,8 @@ class AppWidgetConfigViewModel @Inject constructor( data class AppWidgetConfigUiState( val appWidgetId: Int = -1, - val type: AppWidgetType = AppWidgetType.BLOCKS, - val blocksPreferences: BlocksPreferences = BlocksPreferences(), + val type: AppWidgetType = AppWidgetType.PRICE, val pricePreferences: PricePreferences = PricePreferences(), - val weatherPreferences: WeatherPreferences = WeatherPreferences(), - val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), - val factsPreferences: FactsPreferences = FactsPreferences(), - val blockModel: BlockModel? = null, - val currentFact: String = "", -) - -enum class BlocksField { BLOCK, TIME, DATE, TRANSACTIONS, SIZE, SOURCE } -enum class WeatherField { TITLE, DESCRIPTION, CURRENT_FEE, NEXT_BLOCK } - -private fun HomeBlocksPreferences.toInApp() = BlocksPreferences( - showBlock = showBlock, - showTime = showTime, - showDate = showDate, - showTransactions = showTransactions, - showSize = showSize, - showSource = showSource, ) private fun HomePricePreferences.toInApp() = PricePreferences( @@ -190,49 +90,8 @@ private fun HomePricePreferences.toInApp() = PricePreferences( showSource = showSource, ) -private fun HomeWeatherPreferences.toInApp() = WeatherPreferences( - showTitle = showTitle, - showDescription = showDescription, - showCurrentFee = showCurrentFee, - showNextBlockFee = showNextBlockFee, -) - -private fun HomeHeadlinesPreferences.toInApp() = HeadlinePreferences( - showTime = showTime, - showSource = showSource, -) - -private fun HomeFactsPreferences.toInApp() = FactsPreferences( - showSource = showSource, -) - -private fun BlocksPreferences.toHome() = HomeBlocksPreferences( - showBlock = showBlock, - showTime = showTime, - showDate = showDate, - showTransactions = showTransactions, - showSize = showSize, - showSource = showSource, -) - private fun PricePreferences.toHome() = HomePricePreferences( enabledPairs = enabledPairs, period = period ?: GraphPeriod.ONE_DAY, showSource = showSource, ) - -private fun WeatherPreferences.toHome() = HomeWeatherPreferences( - showTitle = showTitle, - showDescription = showDescription, - showCurrentFee = showCurrentFee, - showNextBlockFee = showNextBlockFee, -) - -private fun HeadlinePreferences.toHome() = HomeHeadlinesPreferences( - showTime = showTime, - showSource = showSource, -) - -private fun FactsPreferences.toHome() = HomeFactsPreferences( - showSource = showSource, -) diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt index a86b41b13..28ae92061 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -1,40 +1,19 @@ package to.bitkit.appwidget.model import kotlinx.serialization.Serializable -import to.bitkit.data.dto.ArticleDTO -import to.bitkit.data.dto.BlockDTO -import to.bitkit.data.dto.WeatherDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.TradingPair enum class AppWidgetType { - BLOCKS, PRICE, - WEATHER, - HEADLINES, - FACTS, } @Serializable data class AppWidgetEntry( val appWidgetId: Int, val type: AppWidgetType, - val blocksPreferences: HomeBlocksPreferences = HomeBlocksPreferences(), val pricePreferences: HomePricePreferences = HomePricePreferences(), - val weatherPreferences: HomeWeatherPreferences = HomeWeatherPreferences(), - val headlinesPreferences: HomeHeadlinesPreferences = HomeHeadlinesPreferences(), - val factsPreferences: HomeFactsPreferences = HomeFactsPreferences(), -) - -@Serializable -data class HomeBlocksPreferences( - val showBlock: Boolean = true, - val showTime: Boolean = true, - val showDate: Boolean = true, - val showTransactions: Boolean = false, - val showSize: Boolean = false, - val showSource: Boolean = false, ) @Serializable @@ -44,31 +23,8 @@ data class HomePricePreferences( val showSource: Boolean = false, ) -@Serializable -data class HomeWeatherPreferences( - val showTitle: Boolean = true, - val showDescription: Boolean = false, - val showCurrentFee: Boolean = false, - val showNextBlockFee: Boolean = false, -) - -@Serializable -data class HomeHeadlinesPreferences( - val showTime: Boolean = true, - val showSource: Boolean = true, -) - -@Serializable -data class HomeFactsPreferences( - val showSource: Boolean = false, -) - @Serializable data class AppWidgetData( val entries: List = emptyList(), - val cachedBlock: BlockDTO? = null, val cachedPrice: PriceDTO? = null, - val cachedWeather: WeatherDTO? = null, - val cachedArticles: List = emptyList(), - val cachedFacts: List = emptyList(), ) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt deleted file mode 100644 index 6e893e641..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt +++ /dev/null @@ -1,67 +0,0 @@ -package to.bitkit.appwidget.ui.blocks - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import androidx.glance.GlanceModifier -import androidx.glance.layout.Spacer -import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height -import androidx.glance.layout.padding -import androidx.glance.text.Text -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.ui.components.GlanceDataRow -import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceTextStyles -import to.bitkit.models.widget.BlockModel - -@Composable -fun BlocksGlanceContent( - context: Context, - block: BlockModel?, - entry: AppWidgetEntry, -) { - val prefs = entry.blocksPreferences - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - - GlanceWidgetScaffold(onClick = launchIntent) { - Text( - text = context.getString(to.bitkit.R.string.widgets__blocks__name), - style = GlanceTextStyles.bodyMSB, - ) - - Spacer(modifier = GlanceModifier.height(8.dp)) - - if (block == null) { - Text( - text = context.getString(to.bitkit.R.string.appwidget__loading), - style = GlanceTextStyles.captionB, - ) - return@GlanceWidgetScaffold - } - - if (prefs.showBlock && block.height.isNotEmpty()) { - GlanceDataRow(label = "Block", value = block.height) - } - if (prefs.showTime && block.time.isNotEmpty()) { - GlanceDataRow(label = "Time", value = block.time) - } - if (prefs.showDate && block.date.isNotEmpty()) { - GlanceDataRow(label = "Date", value = block.date) - } - if (prefs.showTransactions && block.transactionCount.isNotEmpty()) { - GlanceDataRow(label = "Txs", value = block.transactionCount) - } - if (prefs.showSize && block.size.isNotEmpty()) { - GlanceDataRow(label = "Size", value = block.size) - } - if (prefs.showSource && block.source.isNotEmpty()) { - Spacer(modifier = GlanceModifier.height(4.dp)) - Text( - text = block.source, - style = GlanceTextStyles.source, - modifier = GlanceModifier.fillMaxWidth().padding(top = 4.dp), - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt deleted file mode 100644 index 7b1655e54..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt +++ /dev/null @@ -1,19 +0,0 @@ -package to.bitkit.appwidget.ui.blocks - -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import to.bitkit.appwidget.AppWidgetRefreshWorker - -class BlocksGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = BlocksGlanceWidget() - - override fun onEnabled(context: android.content.Context) { - super.onEnabled(context) - AppWidgetRefreshWorker.enqueue(context) - } - - override fun onDisabled(context: android.content.Context) { - super.onDisabled(context) - AppWidgetRefreshWorker.cancelIfNoWidgets(context) - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt deleted file mode 100644 index aa5a5325a..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt +++ /dev/null @@ -1,33 +0,0 @@ -package to.bitkit.appwidget.ui.blocks - -import android.content.Context -import androidx.glance.GlanceId -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.provideContent -import kotlinx.coroutines.flow.first -import to.bitkit.appwidget.AppWidgetPreferencesStore -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.models.widget.BlockModel -import to.bitkit.models.widget.toBlockModel - -class BlocksGlanceWidget : GlanceAppWidget() { - - override suspend fun provideGlance(context: Context, id: GlanceId) { - val store = AppWidgetPreferencesStore.getInstance(context) - val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val data = store.data.first() - val entry = data.entries.find { it.appWidgetId == appWidgetId } - ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.BLOCKS) - val block: BlockModel? = data.cachedBlock?.toBlockModel() - - provideContent { - BlocksGlanceContent( - context = context, - block = block, - entry = entry, - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt deleted file mode 100644 index e66298bfb..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceContent.kt +++ /dev/null @@ -1,55 +0,0 @@ -package to.bitkit.appwidget.ui.facts - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import androidx.glance.GlanceModifier -import androidx.glance.layout.Spacer -import androidx.glance.layout.height -import androidx.glance.text.Text -import to.bitkit.R -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceTextStyles - -@Composable -fun FactsGlanceContent( - context: Context, - facts: List, - entry: AppWidgetEntry, -) { - val prefs = entry.factsPreferences - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - - GlanceWidgetScaffold(onClick = launchIntent) { - Text( - text = context.getString(R.string.widgets__facts__name), - style = GlanceTextStyles.bodyMSB, - ) - - Spacer(modifier = GlanceModifier.height(8.dp)) - - if (facts.isEmpty()) { - Text( - text = context.getString(R.string.appwidget__loading), - style = GlanceTextStyles.captionB, - ) - return@GlanceWidgetScaffold - } - - val fact = facts.random() - Text( - text = fact, - style = GlanceTextStyles.bodySSB, - maxLines = 4, - ) - - if (prefs.showSource) { - Spacer(modifier = GlanceModifier.height(8.dp)) - Text( - text = context.getString(R.string.widgets__widget__source), - style = GlanceTextStyles.source, - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt deleted file mode 100644 index 304fb0f1b..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceReceiver.kt +++ /dev/null @@ -1,20 +0,0 @@ -package to.bitkit.appwidget.ui.facts - -import android.content.Context -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import to.bitkit.appwidget.AppWidgetRefreshWorker - -class FactsGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = FactsGlanceWidget() - - override fun onEnabled(context: Context) { - super.onEnabled(context) - AppWidgetRefreshWorker.enqueue(context) - } - - override fun onDisabled(context: Context) { - super.onDisabled(context) - AppWidgetRefreshWorker.cancelIfNoWidgets(context) - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt deleted file mode 100644 index c3a01c482..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/facts/FactsGlanceWidget.kt +++ /dev/null @@ -1,30 +0,0 @@ -package to.bitkit.appwidget.ui.facts - -import android.content.Context -import androidx.glance.GlanceId -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.provideContent -import kotlinx.coroutines.flow.first -import to.bitkit.appwidget.AppWidgetPreferencesStore -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.model.AppWidgetType - -class FactsGlanceWidget : GlanceAppWidget() { - - override suspend fun provideGlance(context: Context, id: GlanceId) { - val store = AppWidgetPreferencesStore.getInstance(context) - val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val data = store.data.first() - val entry = data.entries.find { it.appWidgetId == appWidgetId } - ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.FACTS) - - provideContent { - FactsGlanceContent( - context = context, - facts = data.cachedFacts, - entry = entry, - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt deleted file mode 100644 index 70af2ef4d..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt +++ /dev/null @@ -1,71 +0,0 @@ -package to.bitkit.appwidget.ui.headlines - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import androidx.glance.GlanceModifier -import androidx.glance.layout.Column -import androidx.glance.layout.Spacer -import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height -import androidx.glance.layout.padding -import androidx.glance.text.Text -import to.bitkit.R -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceColors -import to.bitkit.appwidget.ui.theme.GlanceTextStyles -import to.bitkit.data.dto.ArticleDTO - -@Composable -fun HeadlinesGlanceContent( - context: Context, - articles: List, - entry: AppWidgetEntry, -) { - val prefs = entry.headlinesPreferences - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - - GlanceWidgetScaffold(onClick = launchIntent) { - Text( - text = context.getString(R.string.widgets__news__name), - style = GlanceTextStyles.bodyMSB, - ) - - Spacer(modifier = GlanceModifier.height(8.dp)) - - if (articles.isEmpty()) { - Text( - text = context.getString(R.string.appwidget__loading), - style = GlanceTextStyles.captionB, - ) - return@GlanceWidgetScaffold - } - - val displayArticles = articles.take(3) - for ((index, article) in displayArticles.withIndex()) { - Column(modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp)) { - Text( - text = article.title, - style = GlanceTextStyles.captionB.copy(color = GlanceColors.textPrimary), - maxLines = 2, - ) - if (prefs.showTime || prefs.showSource) { - val meta = buildString { - if (prefs.showTime) append(article.publishedDate) - if (prefs.showTime && prefs.showSource) append(" ยท ") - if (prefs.showSource) append(article.publisher.title) - } - Text( - text = meta, - style = GlanceTextStyles.source, - maxLines = 1, - ) - } - } - if (index < displayArticles.lastIndex) { - Spacer(modifier = GlanceModifier.height(4.dp)) - } - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt deleted file mode 100644 index 4c4c0327c..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt +++ /dev/null @@ -1,20 +0,0 @@ -package to.bitkit.appwidget.ui.headlines - -import android.content.Context -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import to.bitkit.appwidget.AppWidgetRefreshWorker - -class HeadlinesGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = HeadlinesGlanceWidget() - - override fun onEnabled(context: Context) { - super.onEnabled(context) - AppWidgetRefreshWorker.enqueue(context) - } - - override fun onDisabled(context: Context) { - super.onDisabled(context) - AppWidgetRefreshWorker.cancelIfNoWidgets(context) - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt deleted file mode 100644 index 6fdb2aad4..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt +++ /dev/null @@ -1,30 +0,0 @@ -package to.bitkit.appwidget.ui.headlines - -import android.content.Context -import androidx.glance.GlanceId -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.provideContent -import kotlinx.coroutines.flow.first -import to.bitkit.appwidget.AppWidgetPreferencesStore -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.model.AppWidgetType - -class HeadlinesGlanceWidget : GlanceAppWidget() { - - override suspend fun provideGlance(context: Context, id: GlanceId) { - val store = AppWidgetPreferencesStore.getInstance(context) - val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val data = store.data.first() - val entry = data.entries.find { it.appWidgetId == appWidgetId } - ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.HEADLINES) - - provideContent { - HeadlinesGlanceContent( - context = context, - articles = data.cachedArticles, - entry = entry, - ) - } - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt deleted file mode 100644 index 6c11a0f87..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceContent.kt +++ /dev/null @@ -1,86 +0,0 @@ -package to.bitkit.appwidget.ui.weather - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import androidx.glance.GlanceModifier -import androidx.glance.layout.Spacer -import androidx.glance.layout.height -import androidx.glance.text.Text -import to.bitkit.R -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.ui.components.GlanceDataRow -import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold -import to.bitkit.appwidget.ui.theme.GlanceTextStyles -import to.bitkit.data.dto.FeeCondition -import to.bitkit.data.dto.WeatherDTO - -@Composable -fun WeatherGlanceContent( - context: Context, - weather: WeatherDTO?, - entry: AppWidgetEntry, -) { - val prefs = entry.weatherPreferences - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - - GlanceWidgetScaffold(onClick = launchIntent) { - Text( - text = context.getString(R.string.widgets__weather__name), - style = GlanceTextStyles.bodyMSB, - ) - - Spacer(modifier = GlanceModifier.height(8.dp)) - - if (weather == null) { - Text( - text = context.getString(R.string.appwidget__loading), - style = GlanceTextStyles.captionB, - ) - return@GlanceWidgetScaffold - } - - if (prefs.showTitle) { - Text( - text = "${weather.condition.icon} ${conditionLabel(context, weather.condition)}", - style = GlanceTextStyles.subtitle, - ) - Spacer(modifier = GlanceModifier.height(4.dp)) - } - - if (prefs.showDescription) { - Text( - text = conditionDescription(context, weather.condition), - style = GlanceTextStyles.footnoteM, - ) - Spacer(modifier = GlanceModifier.height(4.dp)) - } - - if (prefs.showCurrentFee) { - GlanceDataRow( - label = context.getString(R.string.widgets__weather__current_fee), - value = weather.currentFee, - ) - } - - if (prefs.showNextBlockFee) { - GlanceDataRow( - label = context.getString(R.string.widgets__weather__next_block), - value = "${weather.nextBlockFee} sat/vB", - ) - } - } -} - -private fun conditionLabel(context: Context, condition: FeeCondition): String = when (condition) { - FeeCondition.GOOD -> context.getString(R.string.widgets__weather__condition__good__title) - FeeCondition.AVERAGE -> context.getString(R.string.widgets__weather__condition__average__title) - FeeCondition.POOR -> context.getString(R.string.widgets__weather__condition__poor__title) -} - -private fun conditionDescription(context: Context, condition: FeeCondition): String = - when (condition) { - FeeCondition.GOOD -> context.getString(R.string.widgets__weather__condition__good__description) - FeeCondition.AVERAGE -> context.getString(R.string.widgets__weather__condition__average__description) - FeeCondition.POOR -> context.getString(R.string.widgets__weather__condition__poor__description) - } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt deleted file mode 100644 index 66e6dc069..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceReceiver.kt +++ /dev/null @@ -1,20 +0,0 @@ -package to.bitkit.appwidget.ui.weather - -import android.content.Context -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import to.bitkit.appwidget.AppWidgetRefreshWorker - -class WeatherGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = WeatherGlanceWidget() - - override fun onEnabled(context: Context) { - super.onEnabled(context) - AppWidgetRefreshWorker.enqueue(context) - } - - override fun onDisabled(context: Context) { - super.onDisabled(context) - AppWidgetRefreshWorker.cancelIfNoWidgets(context) - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt deleted file mode 100644 index 6e9259684..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/weather/WeatherGlanceWidget.kt +++ /dev/null @@ -1,30 +0,0 @@ -package to.bitkit.appwidget.ui.weather - -import android.content.Context -import androidx.glance.GlanceId -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.provideContent -import kotlinx.coroutines.flow.first -import to.bitkit.appwidget.AppWidgetPreferencesStore -import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.model.AppWidgetType - -class WeatherGlanceWidget : GlanceAppWidget() { - - override suspend fun provideGlance(context: Context, id: GlanceId) { - val store = AppWidgetPreferencesStore.getInstance(context) - val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val data = store.data.first() - val entry = data.entries.find { it.appWidgetId == appWidgetId } - ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.WEATHER) - - provideContent { - WeatherGlanceContent( - context = context, - weather = data.cachedWeather, - entry = entry, - ) - } - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92d69dfb5..69fb686b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,13 +1,9 @@ - Latest Bitcoin block info - Bitcoin facts - Latest Bitcoin news Loadingโ€ฆ Bitcoin price tracker Time period Trading pairs - Bitcoin network fee weather Store your bitcoin Back up Buy some bitcoin diff --git a/app/src/main/res/xml-v31/appwidget_info_blocks.xml b/app/src/main/res/xml-v31/appwidget_info_blocks.xml deleted file mode 100644 index 2e9c4fa65..000000000 --- a/app/src/main/res/xml-v31/appwidget_info_blocks.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/app/src/main/res/xml/appwidget_info_blocks.xml b/app/src/main/res/xml/appwidget_info_blocks.xml deleted file mode 100644 index 2e9c4fa65..000000000 --- a/app/src/main/res/xml/appwidget_info_blocks.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/app/src/main/res/xml/appwidget_info_facts.xml b/app/src/main/res/xml/appwidget_info_facts.xml deleted file mode 100644 index 2b4491977..000000000 --- a/app/src/main/res/xml/appwidget_info_facts.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/app/src/main/res/xml/appwidget_info_headlines.xml b/app/src/main/res/xml/appwidget_info_headlines.xml deleted file mode 100644 index f45be282f..000000000 --- a/app/src/main/res/xml/appwidget_info_headlines.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/app/src/main/res/xml/appwidget_info_weather.xml b/app/src/main/res/xml/appwidget_info_weather.xml deleted file mode 100644 index 4c5f9cd33..000000000 --- a/app/src/main/res/xml/appwidget_info_weather.xml +++ /dev/null @@ -1,12 +0,0 @@ - - From abf1cfdb16d526324247a4a83d5495316ef97836 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 9 Apr 2026 14:50:18 -0300 Subject: [PATCH 017/100] feat: port price widget to Glance --- .../config/AppWidgetConfigActivity.kt | 20 +----- .../appwidget/ui/price/PriceGlanceContent.kt | 72 ++++++++++++++----- .../appwidget/ui/price/PriceGlanceWidget.kt | 33 +++++++++ .../appwidget/ui/price/SparklineBitmap.kt | 69 ++++++++++++++++++ .../java/to/bitkit/ui/components/SheetHost.kt | 2 +- .../res/layout/appwidget_preview_price.xml | 61 ++++++++++++++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/xml/appwidget_info_price.xml | 1 + 8 files changed, 220 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt create mode 100644 app/src/main/res/layout/appwidget_preview_price.xml diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index 3fc8b170d..b2a39c370 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -38,8 +38,7 @@ class AppWidgetConfigActivity : ComponentActivity() { val typeName = intent?.getStringExtra(EXTRA_WIDGET_TYPE) val type = typeName?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } - ?: resolveTypeFromProvider() - ?: AppWidgetType.BLOCKS + ?: AppWidgetType.PRICE viewModel.init(appWidgetId, type) @@ -61,21 +60,4 @@ class AppWidgetConfigActivity : ComponentActivity() { } } } - - private fun resolveTypeFromProvider(): AppWidgetType? { - val providerInfo = intent?.extras?.let { - val id = it.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - if (id != -1) AppWidgetManager.getInstance(this).getAppWidgetInfo(id) else null - } ?: return null - - val providerClass = providerInfo.provider.className - return when { - providerClass.contains("Blocks") -> AppWidgetType.BLOCKS - providerClass.contains("Price") -> AppWidgetType.PRICE - providerClass.contains("Weather") -> AppWidgetType.WEATHER - providerClass.contains("Headlines") -> AppWidgetType.HEADLINES - providerClass.contains("Facts") -> AppWidgetType.FACTS - else -> null - } - } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 0397ee895..8f6e556c6 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -1,16 +1,21 @@ package to.bitkit.appwidget.ui.price import android.content.Context +import android.graphics.Bitmap 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.color.ColorProvider import androidx.glance.layout.Alignment +import androidx.glance.layout.ContentScale import androidx.glance.layout.Row import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding +import androidx.glance.layout.size import androidx.glance.layout.width import androidx.glance.text.Text import to.bitkit.R @@ -26,17 +31,27 @@ fun PriceGlanceContent( context: Context, price: PriceDTO?, entry: AppWidgetEntry, + chartBitmap: Bitmap? = null, ) { val prefs = entry.pricePreferences val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) GlanceWidgetScaffold(onClick = launchIntent) { - Text( - text = context.getString(R.string.widgets__price__name), - style = GlanceTextStyles.bodyMSB, - ) - - Spacer(modifier = GlanceModifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.padding(bottom = 8.dp), + ) { + Image( + provider = ImageProvider(R.drawable.widget_chart_line), + contentDescription = null, + modifier = GlanceModifier.size(32.dp), + ) + Spacer(modifier = GlanceModifier.width(16.dp)) + Text( + text = context.getString(R.string.widgets__price__name), + style = GlanceTextStyles.bodyMSB, + ) + } if (price == null) { Text( @@ -51,16 +66,34 @@ fun PriceGlanceContent( for (widget in displayWidgets) { PriceRow(widget = widget) - Spacer(modifier = GlanceModifier.height(4.dp)) } - if (prefs.showSource) { - Spacer(modifier = GlanceModifier.height(4.dp)) - Text( - text = price.source, - style = GlanceTextStyles.source, + if (chartBitmap != null) { + Spacer(modifier = GlanceModifier.height(8.dp)) + Image( + provider = ImageProvider(chartBitmap), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = GlanceModifier.fillMaxWidth().height(80.dp), ) } + + if (prefs.showSource) { + Spacer(modifier = GlanceModifier.height(8.dp)) + Row( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.Horizontal.CenterHorizontally, + ) { + Text( + text = context.getString(R.string.widgets__widget__source), + style = GlanceTextStyles.source, + ) + Text( + text = price.source, + style = GlanceTextStyles.source, + ) + } + } } } @@ -71,10 +104,10 @@ private fun PriceRow(widget: PriceWidgetData) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "${widget.pair.symbol}${widget.price}", - style = GlanceTextStyles.subtitle, + text = widget.pair.displayName, + style = GlanceTextStyles.footnoteM, ) - Spacer(modifier = GlanceModifier.width(8.dp)) + Spacer(modifier = GlanceModifier.defaultWeight()) Text( text = widget.change.formatted, style = GlanceTextStyles.captionB.copy( @@ -85,9 +118,10 @@ private fun PriceRow(widget: PriceWidgetData) { }, ), ) + Spacer(modifier = GlanceModifier.width(16.dp)) + Text( + text = widget.price, + style = GlanceTextStyles.bodySSB, + ) } - Text( - text = widget.pair.displayName, - style = GlanceTextStyles.footnoteM, - ) } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index 7c568c4e9..cbb17ca4f 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -1,6 +1,8 @@ package to.bitkit.appwidget.ui.price import android.content.Context +import android.graphics.Bitmap +import androidx.compose.ui.graphics.toArgb import androidx.glance.GlanceId import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager @@ -9,6 +11,8 @@ import kotlinx.coroutines.flow.first import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.ui.theme.Colors class PriceGlanceWidget : GlanceAppWidget() { @@ -19,12 +23,41 @@ class PriceGlanceWidget : GlanceAppWidget() { val entry = data.entries.find { it.appWidgetId == appWidgetId } ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) + val chartBitmap = buildChartBitmap(data.cachedPrice, entry) + provideContent { PriceGlanceContent( context = context, price = data.cachedPrice, entry = entry, + chartBitmap = chartBitmap, ) } } + + private fun buildChartBitmap(price: PriceDTO?, entry: AppWidgetEntry): Bitmap? { + val prefs = entry.pricePreferences + val enabledWidgets = price?.widgets?.filter { it.pair in prefs.enabledPairs } + val chartData = enabledWidgets?.firstOrNull() ?: price?.widgets?.firstOrNull() + ?: return null + if (chartData.pastValues.size < 2) return null + + val lineColor = if (chartData.change.isPositive) { + Colors.Green.toArgb() + } else { + Colors.Red.toArgb() + } + + return renderSparklineBitmap( + values = chartData.pastValues, + width = CHART_WIDTH, + height = CHART_HEIGHT, + lineColor = lineColor, + ) + } + + companion object { + private const val CHART_WIDTH = 600 + private const val CHART_HEIGHT = 200 + } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt new file mode 100644 index 000000000..85a60b0c7 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt @@ -0,0 +1,69 @@ +package to.bitkit.appwidget.ui.price + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Shader +import androidx.annotation.ColorInt +import androidx.core.graphics.createBitmap + +fun renderSparklineBitmap( + values: List, + width: Int, + height: Int, + @ColorInt lineColor: Int, +): Bitmap { + val bitmap = createBitmap(width, height) + if (values.size < 2) return bitmap + + val canvas = Canvas(bitmap) + val minValue = values.min() + val maxValue = values.max() + val range = (maxValue - minValue).coerceAtLeast(1.0) + + val padding = 4f + val drawWidth = width - padding * 2 + val drawHeight = height - padding * 2 + val stepX = drawWidth / (values.size - 1) + + fun xAt(index: Int) = padding + index * stepX + fun yAt(value: Double) = padding + drawHeight - ((value - minValue) / range * drawHeight).toFloat() + + val linePath = Path().apply { + moveTo(xAt(0), yAt(values[0])) + for (i in 1 until values.size) { + lineTo(xAt(i), yAt(values[i])) + } + } + + val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = lineColor + style = Paint.Style.STROKE + strokeWidth = 3f + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + } + canvas.drawPath(linePath, linePaint) + + val fillPath = Path(linePath).apply { + lineTo(xAt(values.size - 1), height.toFloat()) + lineTo(xAt(0), height.toFloat()) + close() + } + + val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + shader = LinearGradient( + 0f, padding, + 0f, height.toFloat(), + (lineColor and 0x00FFFFFF) or 0xCC000000.toInt(), + (lineColor and 0x00FFFFFF) or 0x4D000000.toInt(), + Shader.TileMode.CLAMP, + ) + style = Paint.Style.FILL + } + canvas.drawPath(fillPath, fillPaint) + + return bitmap +} diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index d181c611b..784888ba8 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.screens.wallets.receive.ReceiveRoute +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.sheets.SendRoute diff --git a/app/src/main/res/layout/appwidget_preview_price.xml b/app/src/main/res/layout/appwidget_preview_price.xml new file mode 100644 index 000000000..0f9b18602 --- /dev/null +++ b/app/src/main/res/layout/appwidget_preview_price.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f30d09dd8..97308bd87 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,7 @@ #FF4400 + #FF2A2A2A #FFF1EE #EF886A #03DAC5 diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index ca0c818ad..73947984a 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -7,6 +7,7 @@ android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen" android:initialLayout="@layout/glance_default_loading_layout" + android:previewLayout="@layout/appwidget_preview_price" android:description="@string/appwidget__price__description" android:configure="to.bitkit.appwidget.config.AppWidgetConfigActivity" android:updatePeriodMillis="0" /> From 8c23053b9085ae446e6f7f52f202063497123d5d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 10:59:20 -0300 Subject: [PATCH 018/100] feat: update widget instantly instead of enqueue --- .../appwidget/AppWidgetRefreshWorker.kt | 13 -------- .../config/AppWidgetConfigActivity.kt | 2 +- .../appwidget/config/AppWidgetConfigScreen.kt | 6 +++- .../config/AppWidgetConfigViewModel.kt | 30 +++++++++++++++---- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index ea186e5e6..19de3f085 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -9,7 +9,6 @@ import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters @@ -70,18 +69,6 @@ class AppWidgetRefreshWorker @AssistedInject constructor( ) } - fun enqueueImmediate(context: Context) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - val request = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .build() - - WorkManager.getInstance(context).enqueue(request) - } - fun cancelIfNoWidgets(context: Context) { val manager = AppWidgetManager.getInstance(context) val hasAny = manager.getAppWidgetIds( diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index b2a39c370..d870429b0 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -47,7 +47,7 @@ class AppWidgetConfigActivity : ComponentActivity() { AppWidgetConfigScreen( viewModel = viewModel, onConfirm = { - AppWidgetRefreshWorker.enqueueImmediate(this) + AppWidgetRefreshWorker.enqueue(this) val result = Intent().putExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId, diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 77551a9ae..f39c33e19 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -39,6 +40,7 @@ fun AppWidgetConfigScreen( onConfirm: () -> Unit, onCancel: () -> Unit, ) { + val context = LocalContext.current val state by viewModel.uiState.collectAsStateWithLifecycle() when (state.type) { @@ -48,7 +50,7 @@ fun AppWidgetConfigScreen( onSelectPeriod = { viewModel.selectPricePeriod(it) }, onToggleSource = { viewModel.togglePriceSource() }, onReset = { viewModel.resetPreferences() }, - onSave = { viewModel.saveAndFinish(onConfirm) }, + onSave = { viewModel.saveAndFinish(context, onConfirm) }, onCancel = onCancel, ) } @@ -141,6 +143,8 @@ private fun PriceConfigContent( ) PrimaryButton( text = stringResource(R.string.common__save), + isLoading = state.isSaving, + enabled = !state.isSaving, fullWidth = false, onClick = onSave, modifier = Modifier.weight(1f), diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 6838dd44b..9cb8f8be9 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -1,5 +1,7 @@ package to.bitkit.appwidget.config +import android.content.Context +import androidx.glance.appwidget.updateAll import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -8,19 +10,27 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import to.bitkit.appwidget.AppWidgetDataRepository import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.appwidget.model.HomePricePreferences +import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences +import to.bitkit.utils.Logger import javax.inject.Inject @HiltViewModel class AppWidgetConfigViewModel @Inject constructor( private val preferencesStore: AppWidgetPreferencesStore, + private val dataRepository: AppWidgetDataRepository, ) : ViewModel() { + companion object { + private const val TAG = "AppWidgetConfigViewModel" + } + private val _uiState = MutableStateFlow(AppWidgetConfigUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -66,13 +76,22 @@ class AppWidgetConfigViewModel @Inject constructor( _uiState.update { it.copy(pricePreferences = PricePreferences()) } } - fun saveAndFinish(onComplete: () -> Unit) { + fun saveAndFinish(context: Context, onComplete: () -> Unit) { viewModelScope.launch { - val state = _uiState.value - preferencesStore.registerWidget(state.appWidgetId, state.type) - preferencesStore.updateEntry(state.appWidgetId) { entry -> - entry.copy(pricePreferences = state.pricePreferences.toHome()) + val appWidgetId = _uiState.value.appWidgetId + val pricePreferences = _uiState.value.pricePreferences + _uiState.update { it.copy(isSaving = true) } + preferencesStore.registerWidget(appWidgetId, AppWidgetType.PRICE) + preferencesStore.updateEntry(appWidgetId) { entry -> + entry.copy(pricePreferences = pricePreferences.toHome()) } + dataRepository.fetchPriceData(pricePreferences.period ?: GraphPeriod.ONE_DAY) + .onSuccess { + preferencesStore.cachePriceData(it) + PriceGlanceWidget().updateAll(context) + } + .onFailure { Logger.warn("Failed to fetch initial price data", e = it, context = TAG) } + _uiState.update { it.copy(isSaving = false) } onComplete() } } @@ -82,6 +101,7 @@ data class AppWidgetConfigUiState( val appWidgetId: Int = -1, val type: AppWidgetType = AppWidgetType.PRICE, val pricePreferences: PricePreferences = PricePreferences(), + val isSaving: Boolean = false, ) private fun HomePricePreferences.toInApp() = PricePreferences( From 50d6372be8069051c1f7bbf464782f8acc31d0e2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 11:16:36 -0300 Subject: [PATCH 019/100] refactor: implement spacer components --- .../appwidget/config/AppWidgetConfigScreen.kt | 15 ++++++------ .../appwidget/ui/components/GlanceSpacer.kt | 24 +++++++++++++++++++ .../appwidget/ui/price/PriceGlanceContent.kt | 15 ++++++------ 3 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceSpacer.kt diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index f39c33e19..5722ea354 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -3,9 +3,7 @@ package to.bitkit.appwidget.config import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -30,6 +28,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.Colors @@ -79,7 +78,7 @@ private fun PriceConfigContent( .weight(1f) .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(26.dp)) + VerticalSpacer(26.dp) BodyM( text = stringResource(R.string.widgets__widget__edit_description).replace( @@ -89,13 +88,13 @@ private fun PriceConfigContent( color = Colors.White64, ) - Spacer(modifier = Modifier.height(32.dp)) + VerticalSpacer(32.dp) BodySSB( text = stringResource(R.string.appwidget__price__trading_pairs), color = Colors.White64, ) - Spacer(modifier = Modifier.height(8.dp)) + VerticalSpacer(8.dp) for (pair in TradingPair.entries) { ConfigToggleRow( @@ -105,12 +104,12 @@ private fun PriceConfigContent( ) } - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) BodySSB( text = stringResource(R.string.appwidget__price__period), color = Colors.White64, ) - Spacer(modifier = Modifier.height(8.dp)) + VerticalSpacer(8.dp) for (period in GraphPeriod.entries) { ConfigToggleRow( @@ -120,7 +119,7 @@ private fun PriceConfigContent( ) } - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) ConfigToggleRow( label = stringResource(R.string.widgets__widget__source), isEnabled = prefs.showSource, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceSpacer.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceSpacer.kt new file mode 100644 index 000000000..a7a571ea2 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceSpacer.kt @@ -0,0 +1,24 @@ +package to.bitkit.appwidget.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp +import androidx.glance.GlanceModifier +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.width + +@Composable +fun VerticalSpacer(height: Dp) { + Spacer(modifier = GlanceModifier.height(height)) +} + +@Composable +fun HorizontalSpacer(width: Dp) { + Spacer(modifier = GlanceModifier.width(width)) +} + +@Composable +fun FillWidth() { + Spacer(modifier = GlanceModifier.fillMaxWidth()) +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 8f6e556c6..7b057f5c3 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -11,16 +11,17 @@ import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.ContentScale import androidx.glance.layout.Row -import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.layout.size -import androidx.glance.layout.width import androidx.glance.text.Text import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.ui.components.FillWidth import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.components.HorizontalSpacer +import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -46,7 +47,7 @@ fun PriceGlanceContent( contentDescription = null, modifier = GlanceModifier.size(32.dp), ) - Spacer(modifier = GlanceModifier.width(16.dp)) + HorizontalSpacer(16.dp) Text( text = context.getString(R.string.widgets__price__name), style = GlanceTextStyles.bodyMSB, @@ -69,7 +70,7 @@ fun PriceGlanceContent( } if (chartBitmap != null) { - Spacer(modifier = GlanceModifier.height(8.dp)) + VerticalSpacer(8.dp) Image( provider = ImageProvider(chartBitmap), contentDescription = null, @@ -79,7 +80,7 @@ fun PriceGlanceContent( } if (prefs.showSource) { - Spacer(modifier = GlanceModifier.height(8.dp)) + VerticalSpacer(8.dp) Row( modifier = GlanceModifier.fillMaxWidth(), horizontalAlignment = Alignment.Horizontal.CenterHorizontally, @@ -107,7 +108,7 @@ private fun PriceRow(widget: PriceWidgetData) { text = widget.pair.displayName, style = GlanceTextStyles.footnoteM, ) - Spacer(modifier = GlanceModifier.defaultWeight()) + FillWidth() Text( text = widget.change.formatted, style = GlanceTextStyles.captionB.copy( @@ -118,7 +119,7 @@ private fun PriceRow(widget: PriceWidgetData) { }, ), ) - Spacer(modifier = GlanceModifier.width(16.dp)) + HorizontalSpacer(16.dp) Text( text = widget.price, style = GlanceTextStyles.bodySSB, From f7a37e73801e0b29923d15f727c2ad06cf5b9241 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 11:23:13 -0300 Subject: [PATCH 020/100] chore: lint --- .../java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index d870429b0..76883eddd 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -29,7 +29,7 @@ class AppWidgetConfigActivity : ComponentActivity() { AppWidgetManager.INVALID_APPWIDGET_ID, ) ?: AppWidgetManager.INVALID_APPWIDGET_ID - setResult(Activity.RESULT_CANCELED) + setResult(RESULT_CANCELED) if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish() From 4bc89ea434d9b860c74209d1e7af63487c4ce0d7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 11:40:23 -0300 Subject: [PATCH 021/100] fix: horizontal spacer --- .../appwidget/ui/price/PriceGlanceContent.kt | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 7b057f5c3..a6dc32f27 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -9,16 +9,15 @@ import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment +import androidx.glance.layout.Box import androidx.glance.layout.ContentScale import androidx.glance.layout.Row import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding -import androidx.glance.layout.size import androidx.glance.text.Text import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry -import to.bitkit.appwidget.ui.components.FillWidth import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.components.VerticalSpacer @@ -38,22 +37,6 @@ fun PriceGlanceContent( val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) GlanceWidgetScaffold(onClick = launchIntent) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = GlanceModifier.padding(bottom = 8.dp), - ) { - Image( - provider = ImageProvider(R.drawable.widget_chart_line), - contentDescription = null, - modifier = GlanceModifier.size(32.dp), - ) - HorizontalSpacer(16.dp) - Text( - text = context.getString(R.string.widgets__price__name), - style = GlanceTextStyles.bodyMSB, - ) - } - if (price == null) { Text( text = context.getString(R.string.appwidget__loading), @@ -100,29 +83,34 @@ fun PriceGlanceContent( @Composable private fun PriceRow(widget: PriceWidgetData) { - Row( + Box( modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, + contentAlignment = Alignment.CenterStart, ) { Text( text = widget.pair.displayName, style = GlanceTextStyles.footnoteM, ) - FillWidth() - Text( - text = widget.change.formatted, - style = GlanceTextStyles.captionB.copy( - color = if (widget.change.isPositive) { - ColorProvider(day = Colors.Green, night = Colors.Green) - } else { - ColorProvider(day = Colors.Red, night = Colors.Red) - }, - ), - ) - HorizontalSpacer(16.dp) - Text( - text = widget.price, - style = GlanceTextStyles.bodySSB, - ) + Row( + modifier = GlanceModifier.fillMaxWidth(), + horizontalAlignment = Alignment.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = widget.change.formatted, + style = GlanceTextStyles.captionB.copy( + color = if (widget.change.isPositive) { + ColorProvider(day = Colors.Green, night = Colors.Green) + } else { + ColorProvider(day = Colors.Red, night = Colors.Red) + }, + ), + ) + HorizontalSpacer(16.dp) + Text( + text = "${widget.pair.symbol}${widget.price}", + style = GlanceTextStyles.bodySSB, + ) + } } } From f9368fff7c5030697fd0a727ccc91141f5da13fe Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 11:53:14 -0300 Subject: [PATCH 022/100] refactor: remove source --- .../appwidget/config/AppWidgetConfigScreen.kt | 9 --------- .../config/AppWidgetConfigViewModel.kt | 8 -------- .../appwidget/model/AppWidgetPreferences.kt | 1 - .../appwidget/ui/price/PriceGlanceContent.kt | 17 ----------------- .../appwidget/ui/theme/GlanceTextStyles.kt | 1 - 5 files changed, 36 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 5722ea354..d24c83666 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -47,7 +47,6 @@ fun AppWidgetConfigScreen( state = state, onTogglePair = { viewModel.togglePricePair(it) }, onSelectPeriod = { viewModel.selectPricePeriod(it) }, - onToggleSource = { viewModel.togglePriceSource() }, onReset = { viewModel.resetPreferences() }, onSave = { viewModel.saveAndFinish(context, onConfirm) }, onCancel = onCancel, @@ -60,7 +59,6 @@ private fun PriceConfigContent( state: AppWidgetConfigUiState, onTogglePair: (TradingPair) -> Unit, onSelectPeriod: (GraphPeriod) -> Unit, - onToggleSource: () -> Unit, onReset: () -> Unit, onSave: () -> Unit, onCancel: () -> Unit, @@ -118,13 +116,6 @@ private fun PriceConfigContent( onClick = { onSelectPeriod(period) }, ) } - - VerticalSpacer(16.dp) - ConfigToggleRow( - label = stringResource(R.string.widgets__widget__source), - isEnabled = prefs.showSource, - onClick = onToggleSource, - ) } Row( diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 9cb8f8be9..b078ff919 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -66,12 +66,6 @@ class AppWidgetConfigViewModel @Inject constructor( } } - fun togglePriceSource() { - _uiState.update { - it.copy(pricePreferences = it.pricePreferences.copy(showSource = !it.pricePreferences.showSource)) - } - } - fun resetPreferences() { _uiState.update { it.copy(pricePreferences = PricePreferences()) } } @@ -107,11 +101,9 @@ data class AppWidgetConfigUiState( private fun HomePricePreferences.toInApp() = PricePreferences( enabledPairs = enabledPairs, period = period, - showSource = showSource, ) private fun PricePreferences.toHome() = HomePricePreferences( enabledPairs = enabledPairs, period = period ?: GraphPeriod.ONE_DAY, - showSource = showSource, ) diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt index 28ae92061..1d26ebba5 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -20,7 +20,6 @@ data class AppWidgetEntry( data class HomePricePreferences( val enabledPairs: List = listOf(TradingPair.BTC_USD), val period: GraphPeriod = GraphPeriod.ONE_DAY, - val showSource: Boolean = false, ) @Serializable diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index a6dc32f27..a0aa0ce92 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -61,23 +61,6 @@ fun PriceGlanceContent( modifier = GlanceModifier.fillMaxWidth().height(80.dp), ) } - - if (prefs.showSource) { - VerticalSpacer(8.dp) - Row( - modifier = GlanceModifier.fillMaxWidth(), - horizontalAlignment = Alignment.Horizontal.CenterHorizontally, - ) { - Text( - text = context.getString(R.string.widgets__widget__source), - style = GlanceTextStyles.source, - ) - Text( - text = price.source, - style = GlanceTextStyles.source, - ) - } - } } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt index a4d395e96..1b160f989 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt @@ -10,5 +10,4 @@ object GlanceTextStyles { val bodySSB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) val captionB = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) val footnoteM = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) - val source = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Normal, color = GlanceColors.textTertiary) } From 705c754bae3b69869649c30ab9125b2c46cfb15b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 11:57:12 -0300 Subject: [PATCH 023/100] fix: move chart to bottom --- .../appwidget/ui/price/PriceGlanceContent.kt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index a0aa0ce92..7b54a6ae2 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -10,8 +10,10 @@ import androidx.glance.ImageProvider import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box +import androidx.glance.layout.Column import androidx.glance.layout.ContentScale import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding @@ -20,7 +22,6 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer -import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -48,18 +49,26 @@ fun PriceGlanceContent( val enabledWidgets = price.widgets.filter { it.pair in prefs.enabledPairs } val displayWidgets = enabledWidgets.ifEmpty { price.widgets.take(1) } - for (widget in displayWidgets) { - PriceRow(widget = widget) - } + Box(modifier = GlanceModifier.fillMaxWidth().fillMaxHeight()) { + Column(modifier = GlanceModifier.fillMaxWidth()) { + for (widget in displayWidgets) { + PriceRow(widget = widget) + } + } - if (chartBitmap != null) { - VerticalSpacer(8.dp) - Image( - provider = ImageProvider(chartBitmap), - contentDescription = null, - contentScale = ContentScale.FillBounds, - modifier = GlanceModifier.fillMaxWidth().height(80.dp), - ) + if (chartBitmap != null) { + Box( + modifier = GlanceModifier.fillMaxWidth().fillMaxHeight(), + contentAlignment = Alignment.BottomCenter, + ) { + Image( + provider = ImageProvider(chartBitmap), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = GlanceModifier.fillMaxWidth().height(80.dp), + ) + } + } } } } From 8aa87b04e7e42860258d039dd8aff7a6851caa8d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 12:00:21 -0300 Subject: [PATCH 024/100] refactor: remove context parameter --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 4 ++-- .../java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 7b54a6ae2..09b13b04b 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -1,12 +1,12 @@ package to.bitkit.appwidget.ui.price -import android.content.Context import android.graphics.Bitmap 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.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box @@ -29,11 +29,11 @@ import to.bitkit.ui.theme.Colors @Composable fun PriceGlanceContent( - context: Context, price: PriceDTO?, entry: AppWidgetEntry, chartBitmap: Bitmap? = null, ) { + val context = LocalContext.current val prefs = entry.pricePreferences val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index cbb17ca4f..d21f54154 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -27,7 +27,6 @@ class PriceGlanceWidget : GlanceAppWidget() { provideContent { PriceGlanceContent( - context = context, price = data.cachedPrice, entry = entry, chartBitmap = chartBitmap, From 0c9a97c23ec40c6741339e7f23bdd3d459011447 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 12:05:54 -0300 Subject: [PATCH 025/100] feat: corner radius --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 09b13b04b..c274fb3c5 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -7,6 +7,7 @@ import androidx.glance.GlanceModifier import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box @@ -65,7 +66,10 @@ fun PriceGlanceContent( provider = ImageProvider(chartBitmap), contentDescription = null, contentScale = ContentScale.FillBounds, - modifier = GlanceModifier.fillMaxWidth().height(80.dp), + modifier = GlanceModifier + .fillMaxWidth() + .height(80.dp) + .cornerRadius(8.dp), ) } } From d3699fce2948ccb98e7fdec92c04e4718c5bed07 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 14:00:23 -0300 Subject: [PATCH 026/100] feat: make the widget responsive --- .../to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 5 ++++- .../to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt | 9 +++++++++ app/src/main/res/xml/appwidget_info_price.xml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index c274fb3c5..7ec702772 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -7,6 +7,7 @@ import androidx.glance.GlanceModifier import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.LocalSize import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment @@ -38,6 +39,8 @@ fun PriceGlanceContent( val prefs = entry.pricePreferences val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val showChart = LocalSize.current.height >= 160.dp + GlanceWidgetScaffold(onClick = launchIntent) { if (price == null) { Text( @@ -57,7 +60,7 @@ fun PriceGlanceContent( } } - if (chartBitmap != null) { + if (showChart && chartBitmap != null) { Box( modifier = GlanceModifier.fillMaxWidth().fillMaxHeight(), contentAlignment = Alignment.BottomCenter, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index d21f54154..a9903d096 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -3,9 +3,12 @@ package to.bitkit.appwidget.ui.price import android.content.Context import android.graphics.Bitmap import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import androidx.glance.GlanceId import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.provideContent import kotlinx.coroutines.flow.first import to.bitkit.appwidget.AppWidgetPreferencesStore @@ -16,6 +19,10 @@ import to.bitkit.ui.theme.Colors class PriceGlanceWidget : GlanceAppWidget() { + override val sizeMode = SizeMode.Responsive( + setOf(COMPACT, EXPANDED), + ) + override suspend fun provideGlance(context: Context, id: GlanceId) { val store = AppWidgetPreferencesStore.getInstance(context) val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) @@ -58,5 +65,7 @@ class PriceGlanceWidget : GlanceAppWidget() { companion object { private const val CHART_WIDTH = 600 private const val CHART_HEIGHT = 200 + val COMPACT = DpSize(180.dp, 80.dp) + val EXPANDED = DpSize(180.dp, 180.dp) } } diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index 73947984a..28be39405 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -1,7 +1,7 @@ Date: Fri, 10 Apr 2026 14:39:05 -0300 Subject: [PATCH 027/100] fix: push value to the left --- .../appwidget/ui/price/PriceGlanceContent.kt | 87 ++++++++----------- gradle/libs.versions.toml | 2 +- 2 files changed, 38 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 7ec702772..f4462ecf6 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -11,19 +11,19 @@ import androidx.glance.LocalSize import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment -import androidx.glance.layout.Box -import androidx.glance.layout.Column import androidx.glance.layout.ContentScale import androidx.glance.layout.Row -import androidx.glance.layout.fillMaxHeight +import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.text.Text +import androidx.glance.unit.Dimension import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer +import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -38,7 +38,6 @@ fun PriceGlanceContent( val context = LocalContext.current val prefs = entry.pricePreferences val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - val showChart = LocalSize.current.height >= 160.dp GlanceWidgetScaffold(onClick = launchIntent) { @@ -53,63 +52,51 @@ fun PriceGlanceContent( val enabledWidgets = price.widgets.filter { it.pair in prefs.enabledPairs } val displayWidgets = enabledWidgets.ifEmpty { price.widgets.take(1) } - Box(modifier = GlanceModifier.fillMaxWidth().fillMaxHeight()) { - Column(modifier = GlanceModifier.fillMaxWidth()) { - for (widget in displayWidgets) { - PriceRow(widget = widget) - } - } + for (widget in displayWidgets) { + PriceRow(widget = widget) + } - if (showChart && chartBitmap != null) { - Box( - modifier = GlanceModifier.fillMaxWidth().fillMaxHeight(), - contentAlignment = Alignment.BottomCenter, - ) { - Image( - provider = ImageProvider(chartBitmap), - contentDescription = null, - contentScale = ContentScale.FillBounds, - modifier = GlanceModifier - .fillMaxWidth() - .height(80.dp) - .cornerRadius(8.dp), - ) - } - } + if (showChart && chartBitmap != null) { + VerticalSpacer(8.dp) + Image( + provider = ImageProvider(chartBitmap), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = GlanceModifier + .fillMaxWidth() + .height(80.dp) + .cornerRadius(8.dp), + ) } } } +@Suppress("RestrictedApi") @Composable private fun PriceRow(widget: PriceWidgetData) { - Box( - modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp), - contentAlignment = Alignment.CenterStart, + Row( + modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = widget.pair.displayName, style = GlanceTextStyles.footnoteM, + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), + ) + Text( + text = widget.change.formatted, + style = GlanceTextStyles.captionB.copy( + color = if (widget.change.isPositive) { + ColorProvider(day = Colors.Green, night = Colors.Green) + } else { + ColorProvider(day = Colors.Red, night = Colors.Red) + }, + ), + ) + HorizontalSpacer(16.dp) + Text( + text = "${widget.pair.symbol}${widget.price}", + style = GlanceTextStyles.bodySSB, ) - Row( - modifier = GlanceModifier.fillMaxWidth(), - horizontalAlignment = Alignment.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = widget.change.formatted, - style = GlanceTextStyles.captionB.copy( - color = if (widget.change.isPositive) { - ColorProvider(day = Colors.Green, night = Colors.Green) - } else { - ColorProvider(day = Colors.Red, night = Colors.Red) - }, - ), - ) - HorizontalSpacer(16.dp) - Text( - text = "${widget.pair.symbol}${widget.price}", - style = GlanceTextStyles.bodySSB, - ) - } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69e34ca26..82bda5cec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ agp = "8.13.2" camera = "1.5.2" detekt = "1.23.8" -glance = "1.1.1" +glance = "1.2.0-rc01" hilt = "2.57.2" hiltAndroidx = "1.3.0" kotlin = "2.2.21" From 4656e8a7b2281362d5c41ac3f6f04aed411f69cb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 14:43:02 -0300 Subject: [PATCH 028/100] feat: make chart expandable --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index f4462ecf6..077fa7d4e 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -12,6 +12,7 @@ import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.ContentScale +import androidx.glance.layout.HeightModifier import androidx.glance.layout.Row import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxWidth @@ -28,7 +29,7 @@ import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors - +@Suppress("RestrictedApi") @Composable fun PriceGlanceContent( price: PriceDTO?, @@ -65,7 +66,8 @@ fun PriceGlanceContent( modifier = GlanceModifier .fillMaxWidth() .height(80.dp) - .cornerRadius(8.dp), + .cornerRadius(8.dp) + .then(HeightModifier(Dimension.Expand)), ) } } From 618315844775224e3774f9118ebbb7ea5936b9ed Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 14:50:06 -0300 Subject: [PATCH 029/100] feat: remove click event --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 077fa7d4e..6985ed503 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -38,10 +38,9 @@ fun PriceGlanceContent( ) { val context = LocalContext.current val prefs = entry.pricePreferences - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) val showChart = LocalSize.current.height >= 160.dp - GlanceWidgetScaffold(onClick = launchIntent) { + GlanceWidgetScaffold { if (price == null) { Text( text = context.getString(R.string.appwidget__loading), From e85dcd0725b4e1592b0a7f654edbdc6aad1ce5a2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 15:02:44 -0300 Subject: [PATCH 030/100] feat: fill height with the chart --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 6985ed503..85e7e0126 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -24,7 +24,6 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer -import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -57,14 +56,12 @@ fun PriceGlanceContent( } if (showChart && chartBitmap != null) { - VerticalSpacer(8.dp) Image( provider = ImageProvider(chartBitmap), contentDescription = null, contentScale = ContentScale.FillBounds, modifier = GlanceModifier .fillMaxWidth() - .height(80.dp) .cornerRadius(8.dp) .then(HeightModifier(Dimension.Expand)), ) From 940c027edae7d150341675e01f3da6c328b86281 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 10 Apr 2026 15:03:43 -0300 Subject: [PATCH 031/100] feat: padding --- .../main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 85e7e0126..306c89224 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -63,6 +63,7 @@ fun PriceGlanceContent( modifier = GlanceModifier .fillMaxWidth() .cornerRadius(8.dp) + .padding(top = 8.dp) .then(HeightModifier(Dimension.Expand)), ) } From 362eda402ee607d2f715e1f00c427bd3fb914834 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 20 Apr 2026 07:43:17 -0300 Subject: [PATCH 032/100] feat: display period --- .../appwidget/ui/price/PriceGlanceContent.kt | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 306c89224..e8143437c 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -11,12 +11,13 @@ import androidx.glance.LocalSize import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment +import androidx.glance.layout.Box import androidx.glance.layout.ContentScale import androidx.glance.layout.HeightModifier import androidx.glance.layout.Row import androidx.glance.layout.WidthModifier +import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.text.Text import androidx.glance.unit.Dimension @@ -28,6 +29,7 @@ import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors + @Suppress("RestrictedApi") @Composable fun PriceGlanceContent( @@ -56,16 +58,32 @@ fun PriceGlanceContent( } if (showChart && chartBitmap != null) { - Image( - provider = ImageProvider(chartBitmap), - contentDescription = null, - contentScale = ContentScale.FillBounds, + val chartWidget = displayWidgets.first() + val chartColor = if (chartWidget.change.isPositive) Colors.Green else Colors.Red + Box( modifier = GlanceModifier .fillMaxWidth() - .cornerRadius(8.dp) .padding(top = 8.dp) .then(HeightModifier(Dimension.Expand)), - ) + contentAlignment = Alignment.BottomStart, + ) { + Image( + provider = ImageProvider(chartBitmap), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = GlanceModifier + .fillMaxWidth() + .fillMaxHeight() + .cornerRadius(8.dp), + ) + Text( + text = chartWidget.period.value, + style = GlanceTextStyles.captionB.copy( + color = ColorProvider(day = chartColor, night = chartColor), + ), + modifier = GlanceModifier.padding(7.dp), + ) + } } } } From 4b46176b5ec277d515dd130deace0677699f0b7b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 20 Apr 2026 08:28:33 -0300 Subject: [PATCH 033/100] feat: edit widget on click --- .../config/AppWidgetConfigViewModel.kt | 6 ++---- .../appwidget/ui/price/PriceGlanceContent.kt | 10 +++++++++- .../appwidget/ui/price/PriceGlanceWidget.kt | 17 +++++++++++------ .../java/to/bitkit/viewmodels/AppViewModel.kt | 1 + 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index b078ff919..32b6c6307 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -80,11 +80,9 @@ class AppWidgetConfigViewModel @Inject constructor( entry.copy(pricePreferences = pricePreferences.toHome()) } dataRepository.fetchPriceData(pricePreferences.period ?: GraphPeriod.ONE_DAY) - .onSuccess { - preferencesStore.cachePriceData(it) - PriceGlanceWidget().updateAll(context) - } + .onSuccess { preferencesStore.cachePriceData(it) } .onFailure { Logger.warn("Failed to fetch initial price data", e = it, context = TAG) } + PriceGlanceWidget().updateAll(context) _uiState.update { it.copy(isSaving = false) } onComplete() } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index e8143437c..2bde57360 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -1,5 +1,7 @@ package to.bitkit.appwidget.ui.price +import android.appwidget.AppWidgetManager +import android.content.Intent import android.graphics.Bitmap import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp @@ -22,7 +24,9 @@ import androidx.glance.layout.padding import androidx.glance.text.Text import androidx.glance.unit.Dimension import to.bitkit.R +import to.bitkit.appwidget.config.AppWidgetConfigActivity import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles @@ -40,8 +44,12 @@ fun PriceGlanceContent( val context = LocalContext.current val prefs = entry.pricePreferences val showChart = LocalSize.current.height >= 160.dp + val configIntent = Intent(context, AppWidgetConfigActivity::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, entry.appWidgetId) + putExtra(AppWidgetConfigActivity.EXTRA_WIDGET_TYPE, AppWidgetType.PRICE.name) + } - GlanceWidgetScaffold { + GlanceWidgetScaffold(onClick = configIntent) { if (price == null) { Text( text = context.getString(R.string.appwidget__loading), diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index a9903d096..b3585ae8a 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -2,6 +2,9 @@ package to.bitkit.appwidget.ui.price import android.content.Context import android.graphics.Bitmap +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -10,8 +13,8 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.provideContent -import kotlinx.coroutines.flow.first import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.PriceDTO @@ -26,13 +29,15 @@ class PriceGlanceWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { val store = AppWidgetPreferencesStore.getInstance(context) val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val data = store.data.first() - val entry = data.entries.find { it.appWidgetId == appWidgetId } - ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) - - val chartBitmap = buildChartBitmap(data.cachedPrice, entry) provideContent { + val data by store.data.collectAsState(initial = AppWidgetData()) + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) + val chartBitmap = remember(data.cachedPrice, entry.pricePreferences) { + buildChartBitmap(data.cachedPrice, entry) + } + PriceGlanceContent( price = data.cachedPrice, entry = entry, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 3d56a3329..1e48e7e19 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -2444,6 +2444,7 @@ class AppViewModel @Inject constructor( } fun handleDeeplinkIntent(intent: Intent) { + if (intent.action != Intent.ACTION_VIEW) return intent.data?.let { uri -> Logger.debug("Received deeplink: $uri") processDeeplink(uri) From c0a328f756e3c653882f7aee564f0b1d9c9deda3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 07:06:14 -0300 Subject: [PATCH 034/100] feat: remove header from preview --- .../res/layout/appwidget_preview_price.xml | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/layout/appwidget_preview_price.xml b/app/src/main/res/layout/appwidget_preview_price.xml index 0f9b18602..d9c6aed28 100644 --- a/app/src/main/res/layout/appwidget_preview_price.xml +++ b/app/src/main/res/layout/appwidget_preview_price.xml @@ -1,31 +1,42 @@ + android:padding="16dp" + tools:ignore="HardcodedText"> + android:paddingVertical="2dp"> - + + + + android:textSize="15sp" /> + + + + + + + + + From 0e0cdeed32e0bc4c9c3b320ee3004be1f9a54d50 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 07:19:21 -0300 Subject: [PATCH 035/100] feat: launch AppWidgetConfigActivity on a new task to don't return no MainActivity on finish --- app/src/main/AndroidManifest.xml | 3 +++ .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 1 + 2 files changed, 4 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3f842d93f..5f5a72c05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -176,7 +176,10 @@ diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 2bde57360..764d88329 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -45,6 +45,7 @@ fun PriceGlanceContent( val prefs = entry.pricePreferences val showChart = LocalSize.current.height >= 160.dp val configIntent = Intent(context, AppWidgetConfigActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, entry.appWidgetId) putExtra(AppWidgetConfigActivity.EXTRA_WIDGET_TYPE, AppWidgetType.PRICE.name) } From 2762e5caa24ec52fe87bb13b52b0cf31268fecc8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 08:15:06 -0300 Subject: [PATCH 036/100] fix: match glance text size with Compose --- .../to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 7 ++++--- .../java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt | 1 + .../bitkit/domain/commands/NotifyPaymentReceivedHandler.kt | 2 +- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 2 +- app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 2 +- app/src/main/java/to/bitkit/services/CoreService.kt | 2 +- app/src/main/java/to/bitkit/services/LightningService.kt | 2 +- app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt | 2 +- .../to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt | 2 +- .../ui/settings/lightning/LightningConnectionsScreen.kt | 2 +- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 11 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 764d88329..48aad8455 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -29,6 +29,7 @@ import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer +import to.bitkit.appwidget.ui.theme.GlanceColors import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData @@ -106,12 +107,12 @@ private fun PriceRow(widget: PriceWidgetData) { ) { Text( text = widget.pair.displayName, - style = GlanceTextStyles.footnoteM, + style = GlanceTextStyles.bodySB.copy(color = GlanceColors.textSecondary), modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), ) Text( text = widget.change.formatted, - style = GlanceTextStyles.captionB.copy( + style = GlanceTextStyles.bodySB.copy( color = if (widget.change.isPositive) { ColorProvider(day = Colors.Green, night = Colors.Green) } else { @@ -122,7 +123,7 @@ private fun PriceRow(widget: PriceWidgetData) { HorizontalSpacer(16.dp) Text( text = "${widget.pair.symbol}${widget.price}", - style = GlanceTextStyles.bodySSB, + style = GlanceTextStyles.bodySB, ) } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt index 1b160f989..512241bc0 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt @@ -8,6 +8,7 @@ object GlanceTextStyles { val subtitle = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val bodyMSB = TextStyle(fontSize = 17.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) val bodySSB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) + val bodySB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val captionB = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) val footnoteM = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) } diff --git a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt index 1992e08cd..629b84842 100644 --- a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt +++ b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt @@ -17,8 +17,8 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.NotificationDetails import to.bitkit.models.PrimaryDisplay import to.bitkit.models.formatToModernDisplay -import to.bitkit.repositories.ActivityRepo import to.bitkit.models.msatCeilOf +import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.utils.Logger import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index fe9a86efc..6572848a8 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -26,7 +26,6 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.toUserMessage import to.bitkit.models.BITCOIN_SYMBOL import to.bitkit.models.BlocktankNotificationType -import to.bitkit.models.msatCeilOf import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived import to.bitkit.models.BlocktankNotificationType.incomingHtlc import to.bitkit.models.BlocktankNotificationType.mutualClose @@ -36,6 +35,7 @@ import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.NotificationDetails +import to.bitkit.models.msatCeilOf import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 199e2ff3e..dde2ee0a4 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -32,8 +32,8 @@ import to.bitkit.ext.toHex import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS import to.bitkit.models.AddressModel import to.bitkit.models.BalanceState -import to.bitkit.models.msatFloorOf import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING +import to.bitkit.models.msatFloorOf import to.bitkit.models.toDerivationPath import to.bitkit.services.CoreService import to.bitkit.usecases.DeriveBalanceStateUseCase diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 4349eced4..527dff93d 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -72,11 +72,11 @@ import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.env.Env import to.bitkit.ext.amountSats -import to.bitkit.models.msatFloorOf import to.bitkit.ext.channelId import to.bitkit.ext.create import to.bitkit.ext.latestSpendingTxid import to.bitkit.models.addressTypeFromAddress +import to.bitkit.models.msatFloorOf import to.bitkit.models.toCoreNetwork import to.bitkit.utils.AppError import to.bitkit.utils.Logger diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 74e1a2f3a..2885defad 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -49,8 +49,8 @@ import to.bitkit.env.Env import to.bitkit.ext.totalNextOutboundHtlcLimitSats import to.bitkit.ext.uByteList import to.bitkit.ext.uri -import to.bitkit.models.msatFloorOf import to.bitkit.models.OpenChannelResult +import to.bitkit.models.msatFloorOf import to.bitkit.models.toAddressType import to.bitkit.utils.AppError import to.bitkit.utils.LdkError diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 1b6f70840..43b87cd91 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -50,11 +50,11 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.formatToString import to.bitkit.ext.uri -import to.bitkit.models.msatFloorOf import to.bitkit.models.NodeLifecycleState import to.bitkit.models.NodePeer import to.bitkit.models.alias import to.bitkit.models.formatToModernDisplay +import to.bitkit.models.msatFloorOf import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 0b93407ac..a41243741 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -60,8 +60,8 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText -import to.bitkit.models.msatFloorOf import to.bitkit.models.Toast +import to.bitkit.models.msatFloorOf import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index 89f42eb0f..4c1913954 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -47,8 +47,8 @@ import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails -import to.bitkit.models.msatFloorOf import to.bitkit.models.formatToModernDisplay +import to.bitkit.models.msatFloorOf import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 1e48e7e19..a3376cf68 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -91,7 +91,6 @@ import to.bitkit.ext.toUserMessage import to.bitkit.ext.totalValue import to.bitkit.ext.watchUntil import to.bitkit.models.FeeRate -import to.bitkit.models.msatFloorOf import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType @@ -100,6 +99,7 @@ import to.bitkit.models.Suggestion import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType +import to.bitkit.models.msatFloorOf import to.bitkit.models.safe import to.bitkit.models.toActivityFilter import to.bitkit.models.toLdkNetwork From 9ae6fa6c5cbb5f60e31462ae7dea1c7edc3edeb9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 08:36:12 -0300 Subject: [PATCH 037/100] refactor: extract text components --- .../appwidget/ui/components/GlanceDataRow.kt | 32 --------- .../appwidget/ui/components/GlanceText.kt | 71 +++++++++++++++++++ .../appwidget/ui/price/PriceGlanceContent.kt | 38 ++++------ 3 files changed, 85 insertions(+), 56 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt deleted file mode 100644 index 192f42bf7..000000000 --- a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceDataRow.kt +++ /dev/null @@ -1,32 +0,0 @@ -package to.bitkit.appwidget.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import androidx.glance.GlanceModifier -import androidx.glance.layout.Alignment -import androidx.glance.layout.Row -import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.padding -import androidx.glance.text.Text -import to.bitkit.appwidget.ui.theme.GlanceTextStyles - -@Composable -fun GlanceDataRow( - label: String, - value: String, -) { - Row( - modifier = GlanceModifier.fillMaxWidth().padding(vertical = 2.dp), - horizontalAlignment = Alignment.Horizontal.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = label, - style = GlanceTextStyles.captionB, - ) - Text( - text = value, - style = GlanceTextStyles.bodySSB, - ) - } -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt new file mode 100644 index 000000000..1d1c5bb17 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt @@ -0,0 +1,71 @@ +package to.bitkit.appwidget.ui.components + +import androidx.compose.runtime.Composable +import androidx.glance.GlanceModifier +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import to.bitkit.appwidget.ui.theme.GlanceTextStyles + +@Composable +fun Subtitle( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines) +} + +@Composable +fun BodyMSB( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.bodyMSB.withColor(color), maxLines = maxLines) +} + +@Composable +fun BodySSB( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.bodySSB.withColor(color), maxLines = maxLines) +} + +@Composable +fun BodySB( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.bodySB.withColor(color), maxLines = maxLines) +} + +@Composable +fun CaptionB( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.captionB.withColor(color), maxLines = maxLines) +} + +@Composable +fun FootnoteM( + text: String, + modifier: GlanceModifier = GlanceModifier, + color: ColorProvider? = null, + maxLines: Int = Int.MAX_VALUE, +) { + Text(text = text, modifier = modifier, style = GlanceTextStyles.footnoteM.withColor(color), maxLines = maxLines) +} + +private fun TextStyle.withColor(color: ColorProvider?): TextStyle = + if (color != null) copy(color = color) else this diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 48aad8455..fcac105be 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -21,16 +21,16 @@ import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.padding -import androidx.glance.text.Text import androidx.glance.unit.Dimension import to.bitkit.R import to.bitkit.appwidget.config.AppWidgetConfigActivity import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.ui.components.BodySB +import to.bitkit.appwidget.ui.components.CaptionB import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.theme.GlanceColors -import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors @@ -53,10 +53,7 @@ fun PriceGlanceContent( GlanceWidgetScaffold(onClick = configIntent) { if (price == null) { - Text( - text = context.getString(R.string.appwidget__loading), - style = GlanceTextStyles.captionB, - ) + CaptionB(text = context.getString(R.string.appwidget__loading)) return@GlanceWidgetScaffold } @@ -86,11 +83,9 @@ fun PriceGlanceContent( .fillMaxHeight() .cornerRadius(8.dp), ) - Text( + CaptionB( text = chartWidget.period.value, - style = GlanceTextStyles.captionB.copy( - color = ColorProvider(day = chartColor, night = chartColor), - ), + color = ColorProvider(day = chartColor, night = chartColor), modifier = GlanceModifier.padding(7.dp), ) } @@ -105,25 +100,20 @@ private fun PriceRow(widget: PriceWidgetData) { modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( + BodySB( text = widget.pair.displayName, - style = GlanceTextStyles.bodySB.copy(color = GlanceColors.textSecondary), + color = GlanceColors.textSecondary, modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), ) - Text( + BodySB( text = widget.change.formatted, - style = GlanceTextStyles.bodySB.copy( - color = if (widget.change.isPositive) { - ColorProvider(day = Colors.Green, night = Colors.Green) - } else { - ColorProvider(day = Colors.Red, night = Colors.Red) - }, - ), + color = if (widget.change.isPositive) { + ColorProvider(day = Colors.Green, night = Colors.Green) + } else { + ColorProvider(day = Colors.Red, night = Colors.Red) + }, ) HorizontalSpacer(16.dp) - Text( - text = "${widget.pair.symbol}${widget.price}", - style = GlanceTextStyles.bodySB, - ) + BodySB(text = "${widget.pair.symbol}${widget.price}") } } From a11f6e37c1642dd41bb83a5794d2ff0211ea5199 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 08:42:47 -0300 Subject: [PATCH 038/100] feat: make the chart smooth --- .../appwidget/ui/price/SparklineBitmap.kt | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt index 85a60b0c7..9ef97d78e 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt @@ -9,6 +9,8 @@ import android.graphics.Shader import androidx.annotation.ColorInt import androidx.core.graphics.createBitmap +private const val SMOOTHING = 0.2f + fun renderSparklineBitmap( values: List, width: Int, @@ -28,16 +30,14 @@ fun renderSparklineBitmap( val drawHeight = height - padding * 2 val stepX = drawWidth / (values.size - 1) - fun xAt(index: Int) = padding + index * stepX - fun yAt(value: Double) = padding + drawHeight - ((value - minValue) / range * drawHeight).toFloat() - - val linePath = Path().apply { - moveTo(xAt(0), yAt(values[0])) - for (i in 1 until values.size) { - lineTo(xAt(i), yAt(values[i])) - } + val points = values.mapIndexed { i, v -> + val x = padding + i * stepX + val y = padding + drawHeight - ((v - minValue) / range * drawHeight).toFloat() + x to y } + val linePath = buildSmoothPath(points) + val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = lineColor style = Paint.Style.STROKE @@ -48,8 +48,8 @@ fun renderSparklineBitmap( canvas.drawPath(linePath, linePaint) val fillPath = Path(linePath).apply { - lineTo(xAt(values.size - 1), height.toFloat()) - lineTo(xAt(0), height.toFloat()) + lineTo(points.last().first, height.toFloat()) + lineTo(points.first().first, height.toFloat()) close() } @@ -58,7 +58,7 @@ fun renderSparklineBitmap( 0f, padding, 0f, height.toFloat(), (lineColor and 0x00FFFFFF) or 0xCC000000.toInt(), - (lineColor and 0x00FFFFFF) or 0x4D000000.toInt(), + (lineColor and 0x00FFFFFF) or 0x4D000000, Shader.TileMode.CLAMP, ) style = Paint.Style.FILL @@ -67,3 +67,20 @@ fun renderSparklineBitmap( return bitmap } + +private fun buildSmoothPath(points: List>): Path = Path().apply { + moveTo(points[0].first, points[0].second) + for (i in 0 until points.size - 1) { + val p0 = points[(i - 1).coerceAtLeast(0)] + val p1 = points[i] + val p2 = points[i + 1] + val p3 = points[(i + 2).coerceAtMost(points.lastIndex)] + + val cp1x = p1.first + (p2.first - p0.first) * SMOOTHING + val cp1y = p1.second + (p2.second - p0.second) * SMOOTHING + val cp2x = p2.first - (p3.first - p1.first) * SMOOTHING + val cp2y = p2.second - (p3.second - p1.second) * SMOOTHING + + cubicTo(cp1x, cp1y, cp2x, cp2y, p2.first, p2.second) + } +} From 757f3701fabf9c6d7d84d948afc450a35b0da788 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 08:44:09 -0300 Subject: [PATCH 039/100] refactor: rename bitmap component --- .../ui/price/{SparklineBitmap.kt => LineChartBitmap.kt} | 2 +- .../main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/to/bitkit/appwidget/ui/price/{SparklineBitmap.kt => LineChartBitmap.kt} (98%) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt similarity index 98% rename from app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt rename to app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt index 9ef97d78e..e1c20dad8 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/SparklineBitmap.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt @@ -11,7 +11,7 @@ import androidx.core.graphics.createBitmap private const val SMOOTHING = 0.2f -fun renderSparklineBitmap( +fun renderLineChartBitmap( values: List, width: Int, height: Int, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index b3585ae8a..2cd5d2201 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -59,7 +59,7 @@ class PriceGlanceWidget : GlanceAppWidget() { Colors.Red.toArgb() } - return renderSparklineBitmap( + return renderLineChartBitmap( values = chartData.pastValues, width = CHART_WIDTH, height = CHART_HEIGHT, From 75b210ee4aee5b6a9ab785e1dba25c00e61b1c61 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 10:41:15 -0300 Subject: [PATCH 040/100] fix: reset enabled state --- .../java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index d24c83666..50abba6e9 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -24,6 +24,7 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.PrimaryButton @@ -126,7 +127,7 @@ private fun PriceConfigContent( ) { SecondaryButton( text = stringResource(R.string.common__reset), - enabled = prefs != state.pricePreferences, + enabled = prefs != PricePreferences(), fullWidth = false, onClick = onReset, modifier = Modifier.weight(1f), From b9dad83051b63f7c5f26550a5cdd07700ded92b6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 10:49:46 -0300 Subject: [PATCH 041/100] refactor: code cleanup --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index fcac105be..1b4c40b39 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -57,10 +57,10 @@ fun PriceGlanceContent( return@GlanceWidgetScaffold } - val enabledWidgets = price.widgets.filter { it.pair in prefs.enabledPairs } - val displayWidgets = enabledWidgets.ifEmpty { price.widgets.take(1) } + val enabledPairs = price.widgets.filter { it.pair in prefs.enabledPairs } + val displayWidgets = enabledPairs.ifEmpty { price.widgets.take(1) } - for (widget in displayWidgets) { + displayWidgets.forEach { widget -> PriceRow(widget = widget) } From bec35f70baecb149a53b2291306afcce8874b381 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 11:29:57 -0300 Subject: [PATCH 042/100] refactor: stability annotation --- .../to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt | 2 ++ .../java/to/bitkit/appwidget/model/AppWidgetPreferences.kt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 32b6c6307..33abe43e8 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -1,6 +1,7 @@ package to.bitkit.appwidget.config import android.content.Context +import androidx.compose.runtime.Stable import androidx.glance.appwidget.updateAll import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -89,6 +90,7 @@ class AppWidgetConfigViewModel @Inject constructor( } } +@Stable data class AppWidgetConfigUiState( val appWidgetId: Int = -1, val type: AppWidgetType = AppWidgetType.PRICE, diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt index 1d26ebba5..50dbb0bbf 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -1,5 +1,6 @@ package to.bitkit.appwidget.model +import androidx.compose.runtime.Stable import kotlinx.serialization.Serializable import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO @@ -9,6 +10,7 @@ enum class AppWidgetType { PRICE, } +@Stable @Serializable data class AppWidgetEntry( val appWidgetId: Int, @@ -16,12 +18,14 @@ data class AppWidgetEntry( val pricePreferences: HomePricePreferences = HomePricePreferences(), ) +@Stable @Serializable data class HomePricePreferences( val enabledPairs: List = listOf(TradingPair.BTC_USD), val period: GraphPeriod = GraphPeriod.ONE_DAY, ) +@Stable @Serializable data class AppWidgetData( val entries: List = emptyList(), From dc023306fd442ec14decd699096b44c3e11f558e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 14:27:38 -0300 Subject: [PATCH 043/100] refactor: move updateAll to activity level --- .../to/bitkit/appwidget/config/AppWidgetConfigActivity.kt | 5 ++++- .../to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 6 ++---- .../bitkit/appwidget/config/AppWidgetConfigViewModel.kt | 8 ++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index 76883eddd..049d383e2 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -7,9 +7,11 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.glance.appwidget.updateAll import dagger.hilt.android.AndroidEntryPoint import to.bitkit.appwidget.AppWidgetRefreshWorker import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.ui.theme.AppThemeSurface @AndroidEntryPoint @@ -47,7 +49,8 @@ class AppWidgetConfigActivity : ComponentActivity() { AppWidgetConfigScreen( viewModel = viewModel, onConfirm = { - AppWidgetRefreshWorker.enqueue(this) + PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity) + AppWidgetRefreshWorker.enqueue(this@AppWidgetConfigActivity) val result = Intent().putExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId, diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 50abba6e9..b1af62ac2 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -37,10 +36,9 @@ import to.bitkit.ui.theme.Colors @Composable fun AppWidgetConfigScreen( viewModel: AppWidgetConfigViewModel, - onConfirm: () -> Unit, + onConfirm: suspend () -> Unit, onCancel: () -> Unit, ) { - val context = LocalContext.current val state by viewModel.uiState.collectAsStateWithLifecycle() when (state.type) { @@ -49,7 +47,7 @@ fun AppWidgetConfigScreen( onTogglePair = { viewModel.togglePricePair(it) }, onSelectPeriod = { viewModel.selectPricePeriod(it) }, onReset = { viewModel.resetPreferences() }, - onSave = { viewModel.saveAndFinish(context, onConfirm) }, + onSave = { viewModel.saveAndFinish(onConfirm) }, onCancel = onCancel, ) } diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 33abe43e8..1818537ad 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -1,8 +1,6 @@ package to.bitkit.appwidget.config -import android.content.Context import androidx.compose.runtime.Stable -import androidx.glance.appwidget.updateAll import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -15,7 +13,6 @@ import to.bitkit.appwidget.AppWidgetDataRepository import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.appwidget.model.HomePricePreferences -import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences @@ -71,7 +68,7 @@ class AppWidgetConfigViewModel @Inject constructor( _uiState.update { it.copy(pricePreferences = PricePreferences()) } } - fun saveAndFinish(context: Context, onComplete: () -> Unit) { + fun saveAndFinish(onComplete: suspend () -> Unit) { viewModelScope.launch { val appWidgetId = _uiState.value.appWidgetId val pricePreferences = _uiState.value.pricePreferences @@ -83,9 +80,8 @@ class AppWidgetConfigViewModel @Inject constructor( dataRepository.fetchPriceData(pricePreferences.period ?: GraphPeriod.ONE_DAY) .onSuccess { preferencesStore.cachePriceData(it) } .onFailure { Logger.warn("Failed to fetch initial price data", e = it, context = TAG) } - PriceGlanceWidget().updateAll(context) - _uiState.update { it.copy(isSaving = false) } onComplete() + _uiState.update { it.copy(isSaving = false) } } } } From 3258330ef1cfa916e5a453775c5099b61ac83351 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 21 Apr 2026 15:12:32 -0300 Subject: [PATCH 044/100] fear: chart preview mockup --- app/src/main/res/drawable/chart_preview.xml | 22 +++++++++++++++++++ .../res/layout/appwidget_preview_price.xml | 10 +++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/src/main/res/drawable/chart_preview.xml diff --git a/app/src/main/res/drawable/chart_preview.xml b/app/src/main/res/drawable/chart_preview.xml new file mode 100644 index 000000000..ee3b28e75 --- /dev/null +++ b/app/src/main/res/drawable/chart_preview.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/appwidget_preview_price.xml b/app/src/main/res/layout/appwidget_preview_price.xml index d9c6aed28..69637f743 100644 --- a/app/src/main/res/layout/appwidget_preview_price.xml +++ b/app/src/main/res/layout/appwidget_preview_price.xml @@ -100,4 +100,14 @@ android:textColor="#FFFFFFFF" android:textSize="15sp" /> + + From 3352dd500b2d5ccb79ea2859fda6129eb29eda3e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 09:11:05 -0300 Subject: [PATCH 045/100] fix: drop singleInstance on widget config activity --- app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9acd56e8a..ef4b31e79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -183,7 +183,6 @@ android:name=".appwidget.config.AppWidgetConfigActivity" android:exported="true" android:excludeFromRecents="true" - android:launchMode="singleInstance" android:screenOrientation="portrait" android:taskAffinity="" android:theme="@style/Theme.App"> From 74e367cd5ff78634743050c87aea671dcfb24ed3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 09:25:56 -0300 Subject: [PATCH 046/100] fix: cache price data per graph period in widget worker --- .../bitkit/appwidget/AppWidgetPreferencesStore.kt | 11 +++++++++-- .../to/bitkit/appwidget/AppWidgetRefreshWorker.kt | 15 ++++++++++----- .../appwidget/config/AppWidgetConfigViewModel.kt | 7 ++++--- .../appwidget/model/AppWidgetPreferences.kt | 2 +- .../appwidget/ui/price/PriceGlanceWidget.kt | 7 ++++--- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index 7e0398310..46a0f0d84 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.map import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.serializers.AppWidgetDataSerializer import javax.inject.Inject @@ -69,10 +70,16 @@ class AppWidgetPreferencesStore @Inject constructor( suspend fun getActiveWidgetTypes(): Set = store.data.first().entries.map { it.type }.toSet() + suspend fun getActivePricePeriods(): Set = + store.data.first().entries + .filter { it.type == AppWidgetType.PRICE } + .map { it.pricePreferences.period } + .toSet() + fun hasWidgetsOfType(type: AppWidgetType): Flow = data.map { it.entries.any { entry -> entry.type == type } } - suspend fun cachePriceData(price: PriceDTO) { - store.updateData { it.copy(cachedPrice = price) } + suspend fun cachePriceData(period: GraphPeriod, price: PriceDTO) { + store.updateData { it.copy(cachedPrices = it.cachedPrices + (period to price)) } } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 19de3f085..30106f13a 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -36,12 +36,17 @@ class AppWidgetRefreshWorker @AssistedInject constructor( for (type in activeTypes) { when (type) { - AppWidgetType.PRICE -> dataRepository.fetchPriceData() - .onSuccess { - preferencesStore.cachePriceData(it) - PriceGlanceWidget().updateAll(appContext) + AppWidgetType.PRICE -> { + val periods = preferencesStore.getActivePricePeriods() + periods.forEach { period -> + dataRepository.fetchPriceData(period) + .onSuccess { preferencesStore.cachePriceData(period, it) } + .onFailure { + Logger.warn("Failed to refresh price for '$period'", it, context = TAG) + } } - .onFailure { Logger.warn("Failed to refresh price", e = it, context = TAG) } + PriceGlanceWidget().updateAll(appContext) + } } } diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 1818537ad..75954e3a6 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -77,9 +77,10 @@ class AppWidgetConfigViewModel @Inject constructor( preferencesStore.updateEntry(appWidgetId) { entry -> entry.copy(pricePreferences = pricePreferences.toHome()) } - dataRepository.fetchPriceData(pricePreferences.period ?: GraphPeriod.ONE_DAY) - .onSuccess { preferencesStore.cachePriceData(it) } - .onFailure { Logger.warn("Failed to fetch initial price data", e = it, context = TAG) } + val period = pricePreferences.period ?: GraphPeriod.ONE_DAY + dataRepository.fetchPriceData(period) + .onSuccess { preferencesStore.cachePriceData(period, it) } + .onFailure { Logger.warn("Failed to fetch initial price data", it, context = TAG) } onComplete() _uiState.update { it.copy(isSaving = false) } } diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt index 50dbb0bbf..0aedb2ed0 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -29,5 +29,5 @@ data class HomePricePreferences( @Serializable data class AppWidgetData( val entries: List = emptyList(), - val cachedPrice: PriceDTO? = null, + val cachedPrices: Map = emptyMap(), ) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index 2cd5d2201..a8c0b6b45 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -34,12 +34,13 @@ class PriceGlanceWidget : GlanceAppWidget() { val data by store.data.collectAsState(initial = AppWidgetData()) val entry = data.entries.find { it.appWidgetId == appWidgetId } ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) - val chartBitmap = remember(data.cachedPrice, entry.pricePreferences) { - buildChartBitmap(data.cachedPrice, entry) + val price = data.cachedPrices[entry.pricePreferences.period] + val chartBitmap = remember(price, entry.pricePreferences) { + buildChartBitmap(price, entry) } PriceGlanceContent( - price = data.cachedPrice, + price = price, entry = entry, chartBitmap = chartBitmap, ) From a84f9738ef71969490fc6c768450968def0e0506 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 09:33:23 -0300 Subject: [PATCH 047/100] refactor: move companion object to top --- .../appwidget/AppWidgetRefreshWorker.kt | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 30106f13a..94f6a7d85 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -18,7 +18,8 @@ import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.appwidget.ui.price.PriceGlanceReceiver import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.utils.Logger -import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration @HiltWorker class AppWidgetRefreshWorker @AssistedInject constructor( @@ -28,31 +29,6 @@ class AppWidgetRefreshWorker @AssistedInject constructor( private val preferencesStore: AppWidgetPreferencesStore, ) : CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result { - val activeTypes = preferencesStore.getActiveWidgetTypes() - if (activeTypes.isEmpty()) return Result.success() - - Logger.debug("Refreshing data for widget types: $activeTypes", context = TAG) - - for (type in activeTypes) { - when (type) { - AppWidgetType.PRICE -> { - val periods = preferencesStore.getActivePricePeriods() - periods.forEach { period -> - dataRepository.fetchPriceData(period) - .onSuccess { preferencesStore.cachePriceData(period, it) } - .onFailure { - Logger.warn("Failed to refresh price for '$period'", it, context = TAG) - } - } - PriceGlanceWidget().updateAll(appContext) - } - } - } - - return Result.success() - } - companion object { private const val TAG = "AppWidgetRefreshWorker" private const val WORK_NAME = "appwidget_refresh" @@ -62,10 +38,9 @@ class AppWidgetRefreshWorker @AssistedInject constructor( .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val request = PeriodicWorkRequestBuilder( - repeatInterval = 15, - repeatIntervalTimeUnit = TimeUnit.MINUTES, - ).setConstraints(constraints).build() + val request = PeriodicWorkRequestBuilder(15.minutes.toJavaDuration()) + .setConstraints(constraints) + .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( WORK_NAME, @@ -84,4 +59,29 @@ class AppWidgetRefreshWorker @AssistedInject constructor( } } } + + override suspend fun doWork(): Result { + val activeTypes = preferencesStore.getActiveWidgetTypes() + if (activeTypes.isEmpty()) return Result.success() + + Logger.debug("Refreshing data for widget types: $activeTypes", context = TAG) + + for (type in activeTypes) { + when (type) { + AppWidgetType.PRICE -> { + val periods = preferencesStore.getActivePricePeriods() + periods.forEach { period -> + dataRepository.fetchPriceData(period) + .onSuccess { preferencesStore.cachePriceData(period, it) } + .onFailure { + Logger.warn("Failed to refresh price for '$period'", it, context = TAG) + } + } + PriceGlanceWidget().updateAll(appContext) + } + } + } + + return Result.success() + } } From 76d5bd469c724a2c901444c325e520521c22f3e2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 09:44:21 -0300 Subject: [PATCH 048/100] fix: check all widget types before cancelling refresh worker --- .../to/bitkit/appwidget/AppWidgetRefreshWorker.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 94f6a7d85..d68066443 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -3,6 +3,7 @@ package to.bitkit.appwidget import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context +import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.updateAll import androidx.hilt.work.HiltWorker import androidx.work.Constraints @@ -51,13 +52,17 @@ class AppWidgetRefreshWorker @AssistedInject constructor( fun cancelIfNoWidgets(context: Context) { val manager = AppWidgetManager.getInstance(context) - val hasAny = manager.getAppWidgetIds( - ComponentName(context, PriceGlanceReceiver::class.java), - ).isNotEmpty() + val hasAny = AppWidgetType.entries.any { type -> + manager.getAppWidgetIds(ComponentName(context, receiverClassFor(type))).isNotEmpty() + } if (!hasAny) { WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) } } + + private fun receiverClassFor(type: AppWidgetType): Class = when (type) { + AppWidgetType.PRICE -> PriceGlanceReceiver::class.java + } } override suspend fun doWork(): Result { From 14817e482dbd2983e4bb6635902fb1ea2f188b65 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 09:59:30 -0300 Subject: [PATCH 049/100] fix: clear widget preferences on instance deletion --- .../appwidget/ui/price/PriceGlanceReceiver.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt index 7b810c228..c4ddcdf77 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt @@ -3,6 +3,10 @@ package to.bitkit.appwidget.ui.price import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.AppWidgetRefreshWorker class PriceGlanceReceiver : GlanceAppWidgetReceiver() { @@ -13,6 +17,19 @@ class PriceGlanceReceiver : GlanceAppWidgetReceiver() { AppWidgetRefreshWorker.enqueue(context) } + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + val pendingResult = goAsync() + val store = AppWidgetPreferencesStore.getInstance(context) + CoroutineScope(Dispatchers.IO).launch { + try { + appWidgetIds.forEach { store.unregisterWidget(it) } + } finally { + pendingResult.finish() + } + } + } + override fun onDisabled(context: Context) { super.onDisabled(context) AppWidgetRefreshWorker.cancelIfNoWidgets(context) From 2667245c1d4c7e1110629642c3675b097073630f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 10:03:56 -0300 Subject: [PATCH 050/100] refactor: replace with runCatching --- .../to/bitkit/data/serializers/AppWidgetDataSerializer.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt index 2a963e7b7..e4a705993 100644 --- a/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt +++ b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt @@ -1,7 +1,6 @@ package to.bitkit.data.serializers import androidx.datastore.core.Serializer -import kotlinx.serialization.SerializationException import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.di.json import to.bitkit.utils.Logger @@ -12,10 +11,10 @@ object AppWidgetDataSerializer : Serializer { override val defaultValue: AppWidgetData = AppWidgetData() override suspend fun readFrom(input: InputStream): AppWidgetData { - return try { + return runCatching { json.decodeFromString(input.readBytes().decodeToString()) - } catch (e: SerializationException) { - Logger.error("Failed to deserialize: $e") + }.getOrElse { + Logger.error("Failed to deserialize", it) defaultValue } } From ee6ecc287ca3adaddbee654b600b19b0d1046f05 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 10:07:44 -0300 Subject: [PATCH 051/100] doc: changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299beaf0f..5cfa6925a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Home screen widgets foundation with Glance, including price widget as the first implementation #895 + ## [2.2.0] - 2026-04-07 ### Fixed From b806ac2ffc668858638d278fe092f73413e7c270 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 10:19:07 -0300 Subject: [PATCH 052/100] chore: lint --- .../to/bitkit/data/serializers/AppWidgetDataSerializer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt index e4a705993..62b3b08fb 100644 --- a/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt +++ b/app/src/main/java/to/bitkit/data/serializers/AppWidgetDataSerializer.kt @@ -8,13 +8,15 @@ import java.io.InputStream import java.io.OutputStream object AppWidgetDataSerializer : Serializer { + private const val TAG = "AppWidgetDataSerializer" + override val defaultValue: AppWidgetData = AppWidgetData() override suspend fun readFrom(input: InputStream): AppWidgetData { return runCatching { json.decodeFromString(input.readBytes().decodeToString()) }.getOrElse { - Logger.error("Failed to deserialize", it) + Logger.error("Failed to deserialize AppWidgetData", it, context = TAG) defaultValue } } From 83a6955358d1a94dc37b6ffe3abaf0cd11093b96 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 11:11:24 -0300 Subject: [PATCH 053/100] chore: lint --- .../bitkit/appwidget/ui/price/PriceGlanceWidget.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index a8c0b6b45..d1f26748a 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -22,6 +22,13 @@ import to.bitkit.ui.theme.Colors class PriceGlanceWidget : GlanceAppWidget() { + companion object { + private const val CHART_WIDTH = 600 + private const val CHART_HEIGHT = 200 + val COMPACT = DpSize(180.dp, 80.dp) + val EXPANDED = DpSize(180.dp, 180.dp) + } + override val sizeMode = SizeMode.Responsive( setOf(COMPACT, EXPANDED), ) @@ -67,11 +74,4 @@ class PriceGlanceWidget : GlanceAppWidget() { lineColor = lineColor, ) } - - companion object { - private const val CHART_WIDTH = 600 - private const val CHART_HEIGHT = 200 - val COMPACT = DpSize(180.dp, 80.dp) - val EXPANDED = DpSize(180.dp, 180.dp) - } } From fcd4ba5f604f66c303e803ec2025fdcc77852516 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 11:12:34 -0300 Subject: [PATCH 054/100] chore: lint --- app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index d68066443..aaa8d1e95 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -69,7 +69,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor( val activeTypes = preferencesStore.getActiveWidgetTypes() if (activeTypes.isEmpty()) return Result.success() - Logger.debug("Refreshing data for widget types: $activeTypes", context = TAG) + Logger.debug("Refreshing data for widget types: '$activeTypes'", context = TAG) for (type in activeTypes) { when (type) { From a335c67bf2b3605fae2e73cc2f544a4b113197da Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 22 Apr 2026 11:35:17 -0300 Subject: [PATCH 055/100] refactor: use Hilt EntryPoint for AppWidgetPreferencesStore --- .../appwidget/AppWidgetPreferencesStore.kt | 21 ++++++++----------- .../appwidget/ui/price/PriceGlanceReceiver.kt | 7 +++++-- .../appwidget/ui/price/PriceGlanceWidget.kt | 7 +++++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index 46a0f0d84..88b8e865c 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -3,7 +3,10 @@ package to.bitkit.appwidget import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.dataStore +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -21,22 +24,16 @@ private val Context.appWidgetDataStore: DataStore by dataStore( serializer = AppWidgetDataSerializer, ) +@EntryPoint +@InstallIn(SingletonComponent::class) +interface AppWidgetEntryPoint { + fun appWidgetPreferencesStore(): AppWidgetPreferencesStore +} + @Singleton class AppWidgetPreferencesStore @Inject constructor( @ApplicationContext private val context: Context, ) { - companion object { - @Volatile - private var instance: AppWidgetPreferencesStore? = null - - fun getInstance(context: Context): AppWidgetPreferencesStore = - instance ?: synchronized(this) { - instance ?: AppWidgetPreferencesStore(context.applicationContext).also { - instance = it - } - } - } - private val store = context.appWidgetDataStore val data: Flow = store.data diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt index c4ddcdf77..880d28001 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceReceiver.kt @@ -3,10 +3,11 @@ package to.bitkit.appwidget.ui.price import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver +import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import to.bitkit.appwidget.AppWidgetPreferencesStore +import to.bitkit.appwidget.AppWidgetEntryPoint import to.bitkit.appwidget.AppWidgetRefreshWorker class PriceGlanceReceiver : GlanceAppWidgetReceiver() { @@ -20,7 +21,9 @@ class PriceGlanceReceiver : GlanceAppWidgetReceiver() { override fun onDeleted(context: Context, appWidgetIds: IntArray) { super.onDeleted(context, appWidgetIds) val pendingResult = goAsync() - val store = AppWidgetPreferencesStore.getInstance(context) + val store = EntryPointAccessors + .fromApplication(context, AppWidgetEntryPoint::class.java) + .appWidgetPreferencesStore() CoroutineScope(Dispatchers.IO).launch { try { appWidgetIds.forEach { store.unregisterWidget(it) } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index d1f26748a..6f2781dca 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -13,7 +13,8 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.provideContent -import to.bitkit.appwidget.AppWidgetPreferencesStore +import dagger.hilt.android.EntryPointAccessors +import to.bitkit.appwidget.AppWidgetEntryPoint import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType @@ -34,7 +35,9 @@ class PriceGlanceWidget : GlanceAppWidget() { ) override suspend fun provideGlance(context: Context, id: GlanceId) { - val store = AppWidgetPreferencesStore.getInstance(context) + val store = EntryPointAccessors + .fromApplication(context, AppWidgetEntryPoint::class.java) + .appWidgetPreferencesStore() val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) provideContent { From 47c2ca86dab5d03721d54b28f88d5137b2b12ddb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 09:54:36 -0300 Subject: [PATCH 056/100] feat: implement v61 wide and compact layouts for price OS and internal cards --- .../appwidget/ui/price/LineChartBitmap.kt | 20 -- .../appwidget/ui/price/PriceGlanceContent.kt | 142 ++++++++------ .../appwidget/ui/price/PriceGlanceWidget.kt | 35 ++-- .../appwidget/ui/theme/GlanceTextStyles.kt | 4 + .../ui/screens/widgets/price/PriceCard.kt | 177 ++++++++---------- app/src/main/res/xml/appwidget_info_price.xml | 6 +- 6 files changed, 191 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt index e1c20dad8..cb6a17173 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt @@ -2,10 +2,8 @@ package to.bitkit.appwidget.ui.price import android.graphics.Bitmap import android.graphics.Canvas -import android.graphics.LinearGradient import android.graphics.Paint import android.graphics.Path -import android.graphics.Shader import androidx.annotation.ColorInt import androidx.core.graphics.createBitmap @@ -47,24 +45,6 @@ fun renderLineChartBitmap( } canvas.drawPath(linePath, linePaint) - val fillPath = Path(linePath).apply { - lineTo(points.last().first, height.toFloat()) - lineTo(points.first().first, height.toFloat()) - close() - } - - val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - shader = LinearGradient( - 0f, padding, - 0f, height.toFloat(), - (lineColor and 0x00FFFFFF) or 0xCC000000.toInt(), - (lineColor and 0x00FFFFFF) or 0x4D000000, - Shader.TileMode.CLAMP, - ) - style = Paint.Style.FILL - } - canvas.drawPath(fillPath, fillPaint) - return bitmap } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 1b4c40b39..ea3f9f290 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager import android.content.Intent import android.graphics.Bitmap import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier import androidx.glance.Image @@ -15,36 +16,36 @@ import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.ContentScale -import androidx.glance.layout.HeightModifier import androidx.glance.layout.Row import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.padding +import androidx.glance.layout.height +import androidx.glance.text.Text import androidx.glance.unit.Dimension import to.bitkit.R import to.bitkit.appwidget.config.AppWidgetConfigActivity import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.appwidget.ui.components.BodySB import to.bitkit.appwidget.ui.components.CaptionB import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer -import to.bitkit.appwidget.ui.theme.GlanceColors -import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.appwidget.ui.components.VerticalSpacer +import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors +private val WIDE_LAYOUT_MIN_WIDTH = 280.dp + @Suppress("RestrictedApi") @Composable fun PriceGlanceContent( - price: PriceDTO?, + widget: PriceWidgetData?, + priceAvailable: Boolean, entry: AppWidgetEntry, chartBitmap: Bitmap? = null, ) { val context = LocalContext.current - val prefs = entry.pricePreferences - val showChart = LocalSize.current.height >= 160.dp val configIntent = Intent(context, AppWidgetConfigActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, entry.appWidgetId) @@ -52,68 +53,101 @@ fun PriceGlanceContent( } GlanceWidgetScaffold(onClick = configIntent) { - if (price == null) { + if (!priceAvailable || widget == null) { CaptionB(text = context.getString(R.string.appwidget__loading)) return@GlanceWidgetScaffold } - val enabledPairs = price.widgets.filter { it.pair in prefs.enabledPairs } - val displayWidgets = enabledPairs.ifEmpty { price.widgets.take(1) } - - displayWidgets.forEach { widget -> - PriceRow(widget = widget) - } - - if (showChart && chartBitmap != null) { - val chartWidget = displayWidgets.first() - val chartColor = if (chartWidget.change.isPositive) Colors.Green else Colors.Red - Box( - modifier = GlanceModifier - .fillMaxWidth() - .padding(top = 8.dp) - .then(HeightModifier(Dimension.Expand)), - contentAlignment = Alignment.BottomStart, - ) { - Image( - provider = ImageProvider(chartBitmap), - contentDescription = null, - contentScale = ContentScale.FillBounds, - modifier = GlanceModifier - .fillMaxWidth() - .fillMaxHeight() - .cornerRadius(8.dp), - ) - CaptionB( - text = chartWidget.period.value, - color = ColorProvider(day = chartColor, night = chartColor), - modifier = GlanceModifier.padding(7.dp), - ) - } + if (LocalSize.current.width >= WIDE_LAYOUT_MIN_WIDTH) { + WideContent(widget = widget, chartBitmap = chartBitmap) + } else { + CompactContent(widget = widget, chartBitmap = chartBitmap) } } } @Suppress("RestrictedApi") @Composable -private fun PriceRow(widget: PriceWidgetData) { +private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { + val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red + Row( - modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp), + modifier = GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - BodySB( - text = widget.pair.displayName, - color = GlanceColors.textSecondary, + Text( + text = "${widget.pair.displayName} ${widget.period.value}".uppercase(), + style = GlanceTextStyles.captionUp, modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), ) - BodySB( + HorizontalSpacer(16.dp) + Text( text = widget.change.formatted, - color = if (widget.change.isPositive) { - ColorProvider(day = Colors.Green, night = Colors.Green) - } else { - ColorProvider(day = Colors.Red, night = Colors.Red) - }, + style = GlanceTextStyles.headlineChange22.copy( + color = ColorProvider(day = changeColor, night = changeColor), + ), + ) + } + VerticalSpacer(4.dp) + Text( + text = "${widget.pair.symbol} ${widget.price}", + style = GlanceTextStyles.headline34, + modifier = GlanceModifier.fillMaxWidth(), + ) + VerticalSpacer(8.dp) + ChartBox(chartBitmap = chartBitmap, height = 48.dp) +} + +@Suppress("RestrictedApi") +@Composable +private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { + val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red + + Row(modifier = GlanceModifier.fillMaxWidth()) { + Text( + text = widget.pair.displayName.uppercase(), + style = GlanceTextStyles.captionUp, + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), + ) + Text( + text = widget.period.value.uppercase(), + style = GlanceTextStyles.captionUp, + ) + } + VerticalSpacer(8.dp) + Text( + text = "${widget.pair.symbol} ${widget.price}", + style = GlanceTextStyles.title22, + modifier = GlanceModifier.fillMaxWidth(), + ) + VerticalSpacer(8.dp) + Text( + text = widget.change.formatted, + style = GlanceTextStyles.bodySSB.copy( + color = ColorProvider(day = changeColor, night = changeColor), + ), + ) + VerticalSpacer(16.dp) + ChartBox(chartBitmap = chartBitmap, height = 64.dp) +} + +@Suppress("RestrictedApi") +@Composable +private fun ChartBox(chartBitmap: Bitmap?, height: Dp) { + if (chartBitmap == null) return + Box( + modifier = GlanceModifier + .fillMaxWidth() + .height(height), + ) { + Image( + provider = ImageProvider(chartBitmap), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = GlanceModifier + .fillMaxWidth() + .fillMaxHeight() + .cornerRadius(8.dp), ) - HorizontalSpacer(16.dp) - BodySB(text = "${widget.pair.symbol}${widget.price}") } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index 6f2781dca..3236abf33 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -19,6 +19,7 @@ import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors class PriceGlanceWidget : GlanceAppWidget() { @@ -26,12 +27,12 @@ class PriceGlanceWidget : GlanceAppWidget() { companion object { private const val CHART_WIDTH = 600 private const val CHART_HEIGHT = 200 - val COMPACT = DpSize(180.dp, 80.dp) - val EXPANDED = DpSize(180.dp, 180.dp) + val COMPACT = DpSize(163.dp, 192.dp) + val WIDE = DpSize(343.dp, 152.dp) } override val sizeMode = SizeMode.Responsive( - setOf(COMPACT, EXPANDED), + setOf(COMPACT, WIDE), ) override suspend fun provideGlance(context: Context, id: GlanceId) { @@ -45,33 +46,39 @@ class PriceGlanceWidget : GlanceAppWidget() { val entry = data.entries.find { it.appWidgetId == appWidgetId } ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.PRICE) val price = data.cachedPrices[entry.pricePreferences.period] - val chartBitmap = remember(price, entry.pricePreferences) { - buildChartBitmap(price, entry) + val widget = remember(price, entry.pricePreferences) { + resolveWidget(price, entry) + } + val chartBitmap = remember(widget) { + widget?.let { buildChartBitmap(it) } } PriceGlanceContent( - price = price, + widget = widget, + priceAvailable = price != null, entry = entry, chartBitmap = chartBitmap, ) } } - private fun buildChartBitmap(price: PriceDTO?, entry: AppWidgetEntry): Bitmap? { - val prefs = entry.pricePreferences - val enabledWidgets = price?.widgets?.filter { it.pair in prefs.enabledPairs } - val chartData = enabledWidgets?.firstOrNull() ?: price?.widgets?.firstOrNull() - ?: return null - if (chartData.pastValues.size < 2) return null + private fun resolveWidget(price: PriceDTO?, entry: AppWidgetEntry): PriceWidgetData? { + val widgets = price?.widgets ?: return null + val enabledPairs = entry.pricePreferences.enabledPairs + return widgets.firstOrNull { it.pair in enabledPairs } ?: widgets.firstOrNull() + } + + private fun buildChartBitmap(widget: PriceWidgetData): Bitmap? { + if (widget.pastValues.size < 2) return null - val lineColor = if (chartData.change.isPositive) { + val lineColor = if (widget.change.isPositive) { Colors.Green.toArgb() } else { Colors.Red.toArgb() } return renderLineChartBitmap( - values = chartData.pastValues, + values = widget.pastValues, width = CHART_WIDTH, height = CHART_HEIGHT, lineColor = lineColor, diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt index 512241bc0..cc7b9f026 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt @@ -10,5 +10,9 @@ object GlanceTextStyles { val bodySSB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) val bodySB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val captionB = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) + val captionUp = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) + val title22 = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) + val headline34 = TextStyle(fontSize = 34.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) + val headlineChange22 = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Bold) val footnoteM = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 3fb365b2a..8665dddd4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ShapeDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -27,8 +28,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import ir.ehsannarmani.compose_charts.LineChart import ir.ehsannarmani.compose_charts.models.DividerProperties import ir.ehsannarmani.compose_charts.models.DrawStyle @@ -45,7 +48,7 @@ import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyMSB -import to.bitkit.ui.components.BodySB +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -55,25 +58,30 @@ fun PriceCard( modifier: Modifier = Modifier, showWidgetTitle: Boolean, pricePreferences: PricePreferences, - priceDTO: PriceDTO + priceDTO: PriceDTO, ) { + val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { + priceDTO.widgets.firstOrNull { it.pair in pricePreferences.enabledPairs } + ?: priceDTO.widgets.firstOrNull() + } ?: return + Box( modifier = modifier .clip(shape = MaterialTheme.shapes.medium) - .background(Colors.White10) + .background(Colors.White10), ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), ) { if (showWidgetTitle) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(bottom = 8.dp) - .testTag("price_card_widget_title_row") + .testTag("price_card_widget_title_row"), ) { Icon( painter = painterResource(R.drawable.widget_chart_line), @@ -81,64 +89,59 @@ fun PriceCard( modifier = Modifier .size(32.dp) .testTag("price_card_widget_title_icon"), - tint = Color.Unspecified + tint = Color.Unspecified, ) Spacer(modifier = Modifier.width(16.dp)) BodyMSB( text = stringResource(R.string.widgets__price__name), - modifier = Modifier.testTag("price_card_widget_title_text") + modifier = Modifier.testTag("price_card_widget_title_text"), ) } } - val enabledPairs = remember(pricePreferences.enabledPairs, priceDTO.widgets) { - priceDTO.widgets.filter { widgetData -> widgetData.pair in pricePreferences.enabledPairs } - } - - enabledPairs.map { widgetData -> - Row( + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_pair_row_${widgetData.pair.displayName}"), + verticalAlignment = Alignment.CenterVertically, + ) { + Caption13Up( + text = "${widgetData.pair.displayName} ${widgetData.period.value}", + color = Colors.White64, modifier = Modifier - .fillMaxWidth() - .testTag("price_card_pair_row_${widgetData.pair.displayName}"), - horizontalArrangement = Arrangement.SpaceBetween - ) { - BodySB( - text = widgetData.pair.displayName, - color = Colors.White64, - modifier = Modifier - .weight(1f) - .testTag("PriceWidgetRow-${widgetData.pair.displayName}") - ) - - BodySB( - text = widgetData.change.formatted, - color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, - modifier = Modifier.testTag("price_card_pair_change_${widgetData.pair}") - ) - - Spacer(modifier = Modifier.width(16.dp)) - - BodySB( - text = widgetData.price, - color = Colors.White, - modifier = Modifier.testTag("price_card_pair_price_${widgetData.pair}") - ) - } + .weight(1f) + .testTag("PriceWidgetRow-${widgetData.pair.displayName}"), + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = widgetData.change.formatted, + color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, + fontSize = 22.sp, + lineHeight = 26.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.testTag("price_card_pair_change_${widgetData.pair}"), + ) } - val chartData = remember(enabledPairs, pricePreferences.period) { - enabledPairs.firstOrNull() ?: priceDTO.widgets.firstOrNull() - } + Text( + text = "${widgetData.pair.symbol} ${widgetData.price}", + color = Colors.White, + fontSize = 34.sp, + lineHeight = 34.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_pair_price_${widgetData.pair}"), + ) - chartData?.let { firstPriceData -> - ChartComponent( - widgetData = firstPriceData, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.63.dp) - .testTag("price_card_chart") - ) - } + ChartComponent( + widgetData = widgetData, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(top = 8.dp) + .testTag("price_card_chart"), + ) if (pricePreferences.showSource) { Spacer(modifier = Modifier.height(8.dp)) @@ -147,17 +150,17 @@ fun PriceCard( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() - .testTag("PriceWidgetSource") + .testTag("PriceWidgetSource"), ) { CaptionB( text = stringResource(R.string.widgets__widget__source), color = Colors.White64, - modifier = Modifier.testTag("source_label") + modifier = Modifier.testTag("source_label"), ) CaptionB( text = priceDTO.source, color = Colors.White64, - modifier = Modifier.testTag("source_text") + modifier = Modifier.testTag("source_text"), ) } } @@ -168,7 +171,7 @@ fun PriceCard( @Composable fun ChartComponent( widgetData: PriceWidgetData, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val baseColor = if (widgetData.change.isPositive) Colors.Green else Colors.Red @@ -180,9 +183,7 @@ fun ChartComponent( } Box( - modifier = modifier - .height(96.dp) - .clip(ShapeDefaults.Small) + modifier = modifier.clip(ShapeDefaults.Small), ) { LineChart( modifier = Modifier.fillMaxSize(), @@ -192,44 +193,36 @@ fun ChartComponent( label = widgetData.pair.displayName, values = widgetData.pastValues, color = SolidColor(baseColor), - firstGradientFillColor = baseColor.copy(alpha = 0.8f), - secondGradientFillColor = baseColor.copy(alpha = 0.3f), strokeAnimationSpec = tween(1000, easing = EaseInOutCubic), gradientAnimationDelay = 1000, drawStyle = DrawStyle.Stroke(width = 1.dp), - curvedEdges = true - ) + curvedEdges = true, + ), ) }, labelProperties = LabelProperties( - enabled = false + enabled = false, ), labelHelperProperties = LabelHelperProperties( - enabled = false + enabled = false, ), gridProperties = GridProperties( - enabled = false + enabled = false, ), indicatorProperties = HorizontalIndicatorProperties( - enabled = false + enabled = false, ), dividerProperties = DividerProperties( - enabled = false + enabled = false, ), minValue = minValue, - maxValue = maxValue - ) - - CaptionB( - text = widgetData.period.value, - color = baseColor, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(7.dp) + maxValue = maxValue, ) } } +private val SAMPLE_PAST_VALUES = listOf(1.0, 2.0, 1.5, 3.0, 2.5, 4.0) + @Preview(showBackground = true) @Composable private fun FullBlockCardPreview() { @@ -238,13 +231,13 @@ private fun FullBlockCardPreview() { modifier = Modifier .fillMaxSize() .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), ) { PriceCard( modifier = Modifier.fillMaxWidth(), showWidgetTitle = true, pricePreferences = PricePreferences( - showSource = true + showSource = true, ), priceDTO = PriceDTO( source = "Bitfinex.com", @@ -253,34 +246,14 @@ private fun FullBlockCardPreview() { pair = TradingPair.BTC_USD, change = Change( isPositive = true, - formatted = "$ 20,326" - ), - price = "$20,326", - pastValues = listOf( - 1.0, - 2.0, - 3.0, - 4.0, - ), - period = GraphPeriod.ONE_DAY, - ), - PriceWidgetData( - pair = TradingPair.BTC_USD, - change = Change( - isPositive = false, - formatted = "โ‚ฌ 20,326" - ), - price = "โ‚ฌ 20,326", - pastValues = listOf( - 1.0, - 2.0, - 3.0, - 4.0, + formatted = "+1.24%", ), + price = "75,326", + pastValues = SAMPLE_PAST_VALUES, period = GraphPeriod.ONE_DAY, ), ), - ) + ), ) } } diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index 28be39405..e0c3ece22 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -1,8 +1,8 @@ Date: Mon, 27 Apr 2026 10:27:59 -0300 Subject: [PATCH 057/100] chore: update comma rule --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b7fef0e82..ce97e3168 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,7 +194,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS use `remember` for expensive Compose computations - ALWAYS declare `modifier: Modifier = Modifier,` as the FIRST optional parameter in composable declarations - ALWAYS pass `modifier = ...` as the LAST argument in composable calls -- ALWAYS add trailing commas in multi-line declarations; NEVER add a trailing comma to `modifier = ...` at call sites +- ALWAYS add trailing commas in multi-line declarations, EXCEPT after a `modifier = ...` last argument โ€” never add a trailing comma there, whether the modifier is a single call (`modifier = Modifier.weight(1f)`) or a chain (`modifier = Modifier.fillMaxWidth().testTag("foo")`) - ALWAYS use `navController.navigateTo(route)` for simple navigation; NEVER use raw `navController.navigate(route)` โ€” `navigateTo` prevents duplicate destinations - ALWAYS prefer `VerticalSpacer`, `HorizontalSpacer`, `FillHeight` and `FillWidth` over `Spacer` when applicable - PREFER declaring small dependant classes, constants, interfaces or top-level functions in the same file with the core class where these are used From ebac006337cc0acb8d4ca8c8202479871c236d24 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 10:31:32 -0300 Subject: [PATCH 058/100] feat: implement edit screen v61 and remove multi pairs support --- .../appwidget/config/AppWidgetConfigScreen.kt | 89 ++++---- .../config/AppWidgetConfigViewModel.kt | 10 +- .../screens/widgets/price/PriceEditScreen.kt | 215 +++++------------- .../screens/widgets/price/PriceViewModel.kt | 18 +- app/src/main/res/values/strings.xml | 8 +- 5 files changed, 109 insertions(+), 231 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index b1af62ac2..fa521ce11 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -1,5 +1,6 @@ package to.bitkit.appwidget.config +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,7 +11,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -24,8 +24,8 @@ import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences -import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer @@ -44,7 +44,7 @@ fun AppWidgetConfigScreen( when (state.type) { AppWidgetType.PRICE -> PriceConfigContent( state = state, - onTogglePair = { viewModel.togglePricePair(it) }, + onSelectPair = { viewModel.selectPricePair(it) }, onSelectPeriod = { viewModel.selectPricePeriod(it) }, onReset = { viewModel.resetPreferences() }, onSave = { viewModel.saveAndFinish(onConfirm) }, @@ -56,16 +56,18 @@ fun AppWidgetConfigScreen( @Composable private fun PriceConfigContent( state: AppWidgetConfigUiState, - onTogglePair: (TradingPair) -> Unit, + onSelectPair: (TradingPair) -> Unit, onSelectPeriod: (GraphPeriod) -> Unit, onReset: () -> Unit, onSave: () -> Unit, onCancel: () -> Unit, ) { val prefs = state.pricePreferences + val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD + ScreenColumn { AppTopBar( - titleText = stringResource(R.string.widgets__widget__edit), + titleText = stringResource(R.string.widgets__price__name), onBackClick = onCancel, ) @@ -73,62 +75,52 @@ private fun PriceConfigContent( modifier = Modifier .padding(horizontal = 16.dp) .weight(1f) - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) ) { - VerticalSpacer(26.dp) - - BodyM( - text = stringResource(R.string.widgets__widget__edit_description).replace( - "{name}", - stringResource(R.string.widgets__price__name), - ), - color = Colors.White64, - ) - - VerticalSpacer(32.dp) + VerticalSpacer(16.dp) - BodySSB( - text = stringResource(R.string.appwidget__price__trading_pairs), + Caption13Up( + text = stringResource(R.string.appwidget__price__currency), color = Colors.White64, + modifier = Modifier.padding(bottom = 16.dp) ) - VerticalSpacer(8.dp) for (pair in TradingPair.entries) { - ConfigToggleRow( + SelectableRow( label = pair.displayName, - isEnabled = pair in prefs.enabledPairs, - onClick = { onTogglePair(pair) }, + isSelected = pair == selectedPair, + onClick = { onSelectPair(pair) }, ) } VerticalSpacer(16.dp) - BodySSB( - text = stringResource(R.string.appwidget__price__period), + Caption13Up( + text = stringResource(R.string.appwidget__price__timeframe), color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp) ) - VerticalSpacer(8.dp) for (period in GraphPeriod.entries) { - ConfigToggleRow( - label = period.value, - isEnabled = period == prefs.period, + SelectableRow( + label = period.label(), + isSelected = period == prefs.period, onClick = { onSelectPeriod(period) }, ) } } Row( - modifier = Modifier - .padding(vertical = 21.dp, horizontal = 16.dp) - .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() ) { SecondaryButton( text = stringResource(R.string.common__reset), enabled = prefs != PricePreferences(), fullWidth = false, onClick = onReset, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) PrimaryButton( text = stringResource(R.string.common__save), @@ -136,16 +128,16 @@ private fun PriceConfigContent( enabled = !state.isSaving, fullWidth = false, onClick = onSave, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) } } } @Composable -private fun ConfigToggleRow( +private fun SelectableRow( label: String, - isEnabled: Boolean, + isSelected: Boolean, onClick: () -> Unit, ) { Column { @@ -153,23 +145,34 @@ private fun ConfigToggleRow( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 12.dp) - .fillMaxWidth(), + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 14.dp) ) { BodySSB( text = label, - color = Colors.White64, - modifier = Modifier.weight(1f), + color = if (isSelected) Colors.White else Colors.White64, + modifier = Modifier.weight(1f) ) - IconButton(onClick = onClick) { + if (isSelected) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, - tint = if (isEnabled) Colors.Brand else Colors.White50, - modifier = Modifier.size(32.dp), + tint = Colors.Brand, + modifier = Modifier.size(32.dp) ) } } HorizontalDivider() } } + +@Composable +private fun GraphPeriod.label(): String = stringResource( + when (this) { + GraphPeriod.ONE_DAY -> R.string.appwidget__price__day + GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week + GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month + GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year + }, +) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 75954e3a6..9e8f1f03e 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -46,15 +46,9 @@ class AppWidgetConfigViewModel @Inject constructor( } } - fun togglePricePair(pair: TradingPair) { + fun selectPricePair(pair: TradingPair) { _uiState.update { - val current = it.pricePreferences.enabledPairs.toMutableList() - if (pair in current) { - if (current.size > 1) current.remove(pair) - } else { - current.add(pair) - } - it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = current.sortedBy { p -> p.position })) + it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = listOf(pair))) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index 92b98f47d..4c74b7484 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -1,6 +1,7 @@ package to.bitkit.ui.screens.widgets.price import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,7 +14,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,17 +28,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import to.bitkit.R -import to.bitkit.data.dto.price.Change import to.bitkit.data.dto.price.GraphPeriod -import to.bitkit.data.dto.price.PriceDTO -import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences -import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer @@ -55,8 +50,6 @@ fun PriceEditScreen( navigatePreview: () -> Unit, ) { val customPreferences by viewModel.customPreferences.collectAsStateWithLifecycle() - val currentPrice by viewModel.currentPrice.collectAsStateWithLifecycle() - val allPeriodsUsd by viewModel.allPeriodsUsd.collectAsStateWithLifecycle() val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() PriceEditContent( @@ -64,39 +57,26 @@ fun PriceEditScreen( preferences = customPreferences, onClickReset = { viewModel.resetCustomPreferences() }, onClickPreview = navigatePreview, - allPeriodsUsd = allPeriodsUsd, - priceModel = currentPrice ?: PriceDTO( - widgets = listOf(), - source = "" - ), - onClickTradingPair = { pair -> - viewModel.toggleTradingPair(pair = pair) - }, - onClickGraph = { period -> - viewModel.setPeriod(period = period) - }, + onSelectTradingPair = { viewModel.selectTradingPair(pair = it) }, + onSelectPeriod = { viewModel.setPeriod(period = it) }, isLoading = isLoading, - onClickSource = { - viewModel.toggleShowSource() - } ) } @Composable fun PriceEditContent( onBack: () -> Unit, - priceModel: PriceDTO, - allPeriodsUsd: ImmutableList, onClickReset: () -> Unit, - onClickGraph: (GraphPeriod) -> Unit, - onClickTradingPair: (TradingPair) -> Unit, + onSelectPeriod: (GraphPeriod) -> Unit, + onSelectTradingPair: (TradingPair) -> Unit, onClickPreview: () -> Unit, - onClickSource: () -> Unit, preferences: PricePreferences, isLoading: Boolean, ) { + val selectedPair = preferences.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD + ScreenColumn( - modifier = Modifier.testTag("weather_edit_screen") + modifier = Modifier.testTag("price_edit_screen") ) { Box( modifier = Modifier @@ -112,45 +92,37 @@ fun PriceEditContent( ) { VerticalSpacer(82.dp) - BodyM( - text = stringResource(R.string.widgets__widget__edit_description).replace( - "{name}", - stringResource(R.string.widgets__price__name) - ), + Caption13Up( + text = stringResource(R.string.appwidget__price__currency), color = Colors.White64, - modifier = Modifier.testTag("edit_description") + modifier = Modifier.padding(bottom = 16.dp) ) - VerticalSpacer(32.dp) - - priceModel.widgets.map { data -> - PriceEditOptionRow( - label = data.pair.displayName, - value = data.price, - isEnabled = data.pair in preferences.enabledPairs, - onClick = { - onClickTradingPair(data.pair) - }, - testTagPrefix = data.pair.displayName, + for (pair in TradingPair.entries) { + SelectableRow( + label = pair.displayName, + isSelected = pair == selectedPair, + onClick = { onSelectTradingPair(pair) }, + testTagPrefix = pair.displayName, ) } - allPeriodsUsd.map { priceData -> - PriceChartOptionRow( - widgetData = priceData, - isEnabled = priceData.period == preferences.period, - onClick = onClickGraph, - testTagPrefix = priceData.period.value, - ) - } + VerticalSpacer(16.dp) - PriceEditOptionRow( - label = stringResource(R.string.widgets__widget__source), - value = priceModel.source, - isEnabled = preferences.showSource, - onClick = onClickSource, - testTagPrefix = "showSource", + Caption13Up( + text = stringResource(R.string.appwidget__price__timeframe), + color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp) ) + + for (period in GraphPeriod.entries) { + SelectableRow( + label = period.label(), + isSelected = period == preferences.period, + onClick = { onSelectPeriod(period) }, + testTagPrefix = period.value, + ) + } } Column { @@ -162,10 +134,10 @@ fun PriceEditContent( Brush.verticalGradient( colors = listOf( MaterialTheme.colorScheme.background, - Color.Transparent + Color.Transparent, ), - tileMode = TileMode.Decal - ) + tileMode = TileMode.Decal, + ), ) ) } @@ -202,10 +174,9 @@ fun PriceEditContent( } @Composable -private fun PriceEditOptionRow( +private fun SelectableRow( label: String, - value: String, - isEnabled: Boolean, + isSelected: Boolean, onClick: () -> Unit, testTagPrefix: String, ) { @@ -214,34 +185,23 @@ private fun PriceEditOptionRow( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 16.dp) .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 14.dp) .testTag("${testTagPrefix}_setting_row") ) { BodySSB( text = label, - color = Colors.White64, + color = if (isSelected) Colors.White else Colors.White64, modifier = Modifier .weight(1f) .testTag("${testTagPrefix}_label") ) - - if (value.isNotEmpty()) { - BodySSB( - text = value, - color = Colors.White, - modifier = Modifier.testTag("${testTagPrefix}_text") - ) - } - - IconButton( - onClick = onClick, - modifier = Modifier.testTag("WidgetEditField-$testTagPrefix") - ) { + if (isSelected) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, - tint = if (isEnabled) Colors.Brand else Colors.White50, + tint = Colors.Brand, modifier = Modifier .size(32.dp) .testTag("${testTagPrefix}_toggle_icon") @@ -256,96 +216,27 @@ private fun PriceEditOptionRow( } @Composable -private fun PriceChartOptionRow( - widgetData: PriceWidgetData, - isEnabled: Boolean, - onClick: (GraphPeriod) -> Unit, - testTagPrefix: String, -) { - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(vertical = 21.dp) - .fillMaxWidth() - .testTag("${testTagPrefix}_setting_row") - ) { - ChartComponent( - widgetData = widgetData, - modifier = Modifier.weight(1f) - ) +private fun GraphPeriod.label(): String = stringResource( + when (this) { + GraphPeriod.ONE_DAY -> R.string.appwidget__price__day + GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week + GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month + GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year + }, +) - IconButton( - onClick = { onClick(widgetData.period) }, - modifier = Modifier.testTag("WidgetEditField-$testTagPrefix") - ) { - Icon( - painter = painterResource(R.drawable.ic_checkmark), - contentDescription = null, - tint = if (isEnabled) Colors.Brand else Colors.White50, - modifier = Modifier - .size(32.dp) - .testTag("${testTagPrefix}_toggle_icon"), - ) - } - } - - HorizontalDivider( - modifier = Modifier.testTag("${testTagPrefix}_divider") - ) - } -} - -@Suppress("MagicNumber") @Preview(showSystemUi = true) @Composable private fun Preview() { AppThemeSurface { PriceEditContent( onBack = {}, - priceModel = PriceDTO( - widgets = listOf( - PriceWidgetData( - pair = TradingPair.BTC_USD, - period = GraphPeriod.ONE_DAY, - change = Change(isPositive = true, formatted = "+2.5%"), - price = "$97,500", - pastValues = listOf(95000.0, 96000.0, 95500.0, 97000.0, 97500.0) - ), - PriceWidgetData( - pair = TradingPair.BTC_EUR, - period = GraphPeriod.ONE_DAY, - change = Change(isPositive = true, formatted = "+2.3%"), - price = "โ‚ฌ89,000", - pastValues = listOf(87000.0, 88000.0, 87500.0, 88500.0, 89000.0) - ) - ), - source = "Kraken" - ), - allPeriodsUsd = persistentListOf( - PriceWidgetData( - pair = TradingPair.BTC_USD, - period = GraphPeriod.ONE_DAY, - change = Change(isPositive = true, formatted = "+2.5%"), - price = "$97,500", - pastValues = listOf(95000.0, 96000.0, 95500.0, 97000.0, 97500.0) - ), - PriceWidgetData( - pair = TradingPair.BTC_USD, - period = GraphPeriod.ONE_WEEK, - change = Change(isPositive = true, formatted = "+5.0%"), - price = "$97,500", - pastValues = listOf(93000.0, 94000.0, 95000.0, 96000.0, 97500.0) - ) - ), onClickReset = {}, - onClickGraph = {}, - onClickTradingPair = {}, + onSelectPeriod = {}, + onSelectTradingPair = {}, onClickPreview = {}, - onClickSource = {}, preferences = PricePreferences(), - isLoading = false + isLoading = false, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt index ffa2d4d31..cc7045f1f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO -import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.WidgetType import to.bitkit.models.widget.PricePreferences @@ -68,8 +67,6 @@ class PriceViewModel @Inject constructor( private val _customPreferences = MutableStateFlow(PricePreferences()) val customPreferences: StateFlow = _customPreferences.asStateFlow() - private val _allPeriodsUsd = MutableStateFlow>(persistentListOf()) - val allPeriodsUsd: StateFlow> = _allPeriodsUsd.asStateFlow() private val _allPrices = MutableStateFlow>(persistentListOf()) private val _previewPrice: MutableStateFlow = MutableStateFlow(null) @@ -94,18 +91,8 @@ class PriceViewModel @Inject constructor( _previewPrice.update { _allPrices.value.firstOrNull { it.widgets.firstOrNull()?.period == period } } } - fun toggleTradingPair(pair: TradingPair) { - if (pair in _customPreferences.value.enabledPairs) { - _customPreferences.update { it.copy(enabledPairs = it.enabledPairs - pair) } - } else { - _customPreferences.update { it.copy(enabledPairs = it.enabledPairs + pair) } - } - } - - fun toggleShowSource() { - _customPreferences.update { preferences -> - preferences.copy(showSource = !preferences.showSource) - } + fun selectTradingPair(pair: TradingPair) { + _customPreferences.update { it.copy(enabledPairs = listOf(pair)) } } fun resetCustomPreferences() { @@ -152,7 +139,6 @@ class PriceViewModel @Inject constructor( _isLoading.update { true } widgetsRepo.fetchAllPeriods().onSuccess { data -> _allPrices.update { data.toImmutableList() } - _allPeriodsUsd.update { data.map { priceDTO -> priceDTO.widgets.first() }.toImmutableList() } _isLoading.update { false } }.onFailure { Logger.warn("collectAllPeriodPrices error. Trying again in 1 second", context = TAG) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7872f0e4d..1fc9803ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,13 @@ Loadingโ€ฆ + Currency + Day Bitcoin price tracker - Time period - Trading pairs + Month + Timeframe + Week + Year Store your bitcoin Back up Buy some bitcoin From 01fad4fcbf06197d0459d6f67302be79fbfa25a2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 10:38:25 -0300 Subject: [PATCH 059/100] feat: update the OS preview --- app/src/main/res/drawable/chart_preview.xml | 29 +++---- .../res/layout/appwidget_preview_price.xml | 86 +++---------------- 2 files changed, 25 insertions(+), 90 deletions(-) diff --git a/app/src/main/res/drawable/chart_preview.xml b/app/src/main/res/drawable/chart_preview.xml index ee3b28e75..5cbc84a10 100644 --- a/app/src/main/res/drawable/chart_preview.xml +++ b/app/src/main/res/drawable/chart_preview.xml @@ -1,22 +1,15 @@ - - - - - - - - + android:viewportHeight="48"> + + + + diff --git a/app/src/main/res/layout/appwidget_preview_price.xml b/app/src/main/res/layout/appwidget_preview_price.xml index 69637f743..91448622b 100644 --- a/app/src/main/res/layout/appwidget_preview_price.xml +++ b/app/src/main/res/layout/appwidget_preview_price.xml @@ -12,94 +12,36 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingVertical="2dp"> + android:orientation="horizontal"> - - - - - - - - - - - + android:text="+1.24%" + android:textColor="#FF75BF72" + android:textSize="22sp" + android:textStyle="bold" /> - - - - - - - - + android:layout_marginTop="4dp" + android:text="$ 75,326" + android:textColor="#FFFFFFFF" + android:textSize="34sp" + android:textStyle="bold" /> Date: Mon, 27 Apr 2026 10:51:00 -0300 Subject: [PATCH 060/100] fix: chart vertical padding --- .../appwidget/ui/price/PriceGlanceContent.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index ea3f9f290..5c8fe90b1 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -4,7 +4,6 @@ import android.appwidget.AppWidgetManager import android.content.Intent import android.graphics.Bitmap import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier import androidx.glance.Image @@ -16,11 +15,12 @@ import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.ContentScale +import androidx.glance.layout.HeightModifier import androidx.glance.layout.Row import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height +import androidx.glance.layout.padding import androidx.glance.text.Text import androidx.glance.unit.Dimension import to.bitkit.R @@ -95,7 +95,7 @@ private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { modifier = GlanceModifier.fillMaxWidth(), ) VerticalSpacer(8.dp) - ChartBox(chartBitmap = chartBitmap, height = 48.dp) + ChartBox(chartBitmap = chartBitmap) } @Suppress("RestrictedApi") @@ -127,18 +127,18 @@ private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { color = ColorProvider(day = changeColor, night = changeColor), ), ) - VerticalSpacer(16.dp) - ChartBox(chartBitmap = chartBitmap, height = 64.dp) + ChartBox(chartBitmap = chartBitmap) } @Suppress("RestrictedApi") @Composable -private fun ChartBox(chartBitmap: Bitmap?, height: Dp) { +private fun ChartBox(chartBitmap: Bitmap?) { if (chartBitmap == null) return Box( modifier = GlanceModifier .fillMaxWidth() - .height(height), + .then(HeightModifier(Dimension.Expand)) + .padding(vertical = 16.dp), ) { Image( provider = ImageProvider(chartBitmap), From 8b1ff754718c5cbb9d0eb0a4b75f7095f3e359c4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 11:23:37 -0300 Subject: [PATCH 061/100] feat: preview screen v61 --- .../ui/screens/widgets/price/PriceCard.kt | 71 +++++ .../widgets/price/PricePreviewScreen.kt | 272 +++++++++--------- app/src/main/res/drawable/chart_preview.xml | 29 +- app/src/main/res/values/strings.xml | 2 + 4 files changed, 233 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 8665dddd4..a71bccf59 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -168,6 +168,77 @@ fun PriceCard( } } +@Composable +fun PriceCardSmall( + modifier: Modifier = Modifier, + pricePreferences: PricePreferences, + priceDTO: PriceDTO, +) { + val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { + priceDTO.widgets.firstOrNull { it.pair in pricePreferences.enabledPairs } + ?: priceDTO.widgets.firstOrNull() + } ?: return + + Box( + modifier = modifier + .clip(shape = MaterialTheme.shapes.medium) + .background(Colors.White10), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_small_pair_row_${widgetData.pair.displayName}"), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Caption13Up( + text = widgetData.pair.displayName, + color = Colors.White64, + ) + Caption13Up( + text = widgetData.period.value, + color = Colors.White64, + ) + } + Text( + text = "${widgetData.pair.symbol} ${widgetData.price}", + color = Colors.White, + fontSize = 22.sp, + lineHeight = 26.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_small_pair_price_${widgetData.pair}"), + ) + Text( + text = widgetData.change.formatted, + color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, + fontSize = 15.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.testTag("price_card_small_pair_change_${widgetData.pair}"), + ) + } + + ChartComponent( + widgetData = widgetData, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag("price_card_small_chart"), + ) + } + } +} + @Composable fun ChartComponent( widgetData: PriceWidgetData, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index a4f6543e3..6e828f9d5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -1,27 +1,26 @@ package to.bitkit.ui.screens.widgets.price +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -31,21 +30,23 @@ import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair -import to.bitkit.ext.spaceToNewline import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.Headline +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Text13Up +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +private const val PAGE_SMALL = 0 +private const val PAGE_WIDE = 1 +private const val PAGE_COUNT = 2 + @Composable fun PricePreviewScreen( priceViewModel: PriceViewModel, @@ -53,7 +54,6 @@ fun PricePreviewScreen( onBack: () -> Unit, navigateEditWidget: () -> Unit, ) { - val showWidgetTitles by priceViewModel.showWidgetTitles.collectAsStateWithLifecycle() val customPricePreferences by priceViewModel.customPreferences.collectAsStateWithLifecycle() val price by priceViewModel.currentPrice.collectAsStateWithLifecycle() val previewPrice by priceViewModel.previewPrice.collectAsStateWithLifecycle() @@ -76,7 +76,6 @@ fun PricePreviewScreen( onBack = onBack, isPriceWidgetEnabled = isPriceWidgetEnabled, pricePreferences = customPricePreferences, - showWidgetTitles = showWidgetTitles, priceDTO = previewPrice ?: price, onClickEdit = navigateEditWidget, onClickDelete = { @@ -86,7 +85,7 @@ fun PricePreviewScreen( onClickSave = { priceViewModel.savePreferences() }, - isLoading = isLoading + isLoading = isLoading, ) } @@ -96,61 +95,36 @@ fun PricePreviewContent( onClickEdit: () -> Unit, onClickDelete: () -> Unit, onClickSave: () -> Unit, - showWidgetTitles: Boolean, isPriceWidgetEnabled: Boolean, pricePreferences: PricePreferences, priceDTO: PriceDTO?, isLoading: Boolean, ) { ScreenColumn( - modifier = Modifier.testTag("price_preview_screen") + modifier = Modifier.testTag("price_preview_screen"), ) { AppTopBar( - titleText = stringResource(R.string.widgets__widget__nav_title), + titleText = stringResource(R.string.widgets__price__name), onBackClick = onBack, - actions = { DrawerNavIcon() }, ) Column( modifier = Modifier .padding(horizontal = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()) - .testTag("WidgetEditScrollView") + .weight(1f), ) { - Spacer(modifier = Modifier.height(26.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("header_row"), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Headline( - text = AnnotatedString(stringResource(R.string.widgets__price__name).spaceToNewline()), - modifier = Modifier.testTag("widget_title"), - ) - Icon( - painter = painterResource(R.drawable.widget_chart_line), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier - .size(64.dp) - .testTag("widget_icon") - ) - } + VerticalSpacer(16.dp) BodyM( text = stringResource(R.string.widgets__price__description), color = Colors.White64, - modifier = Modifier - .padding(vertical = 16.dp) - .testTag("widget_description") + modifier = Modifier.testTag("widget_description"), ) + VerticalSpacer(16.dp) + HorizontalDivider( - modifier = Modifier.testTag("divider") + modifier = Modifier.testTag("divider"), ) SettingsButtonRow( @@ -160,149 +134,187 @@ fun PricePreviewContent( stringResource(R.string.widgets__widget__edit_default) } else { stringResource(R.string.widgets__widget__edit_custom) - } + }, ), onClick = onClickEdit, - modifier = Modifier.testTag("WidgetEdit") + modifier = Modifier.testTag("WidgetEdit"), ) - Spacer(modifier = Modifier.weight(1f)) - - Text13Up( - stringResource(R.string.common__preview), - color = Colors.White64, - modifier = Modifier - .padding(vertical = 16.dp) - .testTag("preview_label") - ) - - priceDTO?.let { dto -> - PriceCard( - modifier = Modifier - .fillMaxWidth() - .testTag("price_card"), - showWidgetTitle = showWidgetTitles, + if (priceDTO != null) { + WidgetCarousel( pricePreferences = pricePreferences, - priceDTO = dto + priceDTO = priceDTO, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), ) + } else { + Box(modifier = Modifier.weight(1f)) } } Row( modifier = Modifier - .padding(vertical = 21.dp, horizontal = 16.dp) + .padding(16.dp) .fillMaxWidth() .testTag("buttons_row"), - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { if (isPriceWidgetEnabled) { SecondaryButton( text = stringResource(R.string.common__delete), + fullWidth = false, + onClick = onClickDelete, modifier = Modifier .weight(1f) .testTag("WidgetDelete"), - fullWidth = false, - onClick = onClickDelete ) } PrimaryButton( text = stringResource(R.string.common__save), + fullWidth = false, + isLoading = isLoading, + onClick = onClickSave, modifier = Modifier .weight(1f) .testTag("WidgetSave"), - fullWidth = false, - isLoading = isLoading, - onClick = onClickSave ) } } } +@Composable +private fun WidgetCarousel( + pricePreferences: PricePreferences, + priceDTO: PriceDTO, + modifier: Modifier = Modifier, +) { + val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) + + Column( + modifier = modifier.testTag("price_preview_carousel"), + verticalArrangement = Arrangement.Center, + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag("price_preview_pager"), + ) { page -> + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + when (page) { + PAGE_SMALL -> PriceCardSmall( + modifier = Modifier + .width(163.dp) + .height(192.dp) + .testTag("price_card_small"), + pricePreferences = pricePreferences, + priceDTO = priceDTO, + ) + + PAGE_WIDE -> PriceCard( + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_wide"), + showWidgetTitle = false, + pricePreferences = pricePreferences, + priceDTO = priceDTO, + ) + } + } + } + + VerticalSpacer(16.dp) + + Caption13Up( + text = stringResource( + if (pagerState.currentPage == PAGE_SMALL) { + R.string.widgets__widget__size_small + } else { + R.string.widgets__widget__size_wide + }, + ), + color = Colors.White64, + modifier = Modifier + .fillMaxWidth() + .testTag("widget_size_label"), + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + ) + + VerticalSpacer(8.dp) + + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("page_indicator"), + horizontalArrangement = Arrangement.Center, + ) { + repeat(PAGE_COUNT) { index -> + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(8.dp) + .background( + color = if (pagerState.currentPage == index) Colors.White else Colors.White32, + shape = CircleShape, + ), + ) + } + } + } +} + @Preview(showBackground = true) @Composable private fun Preview() { AppThemeSurface { PricePreviewContent( onBack = {}, - showWidgetTitles = true, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, pricePreferences = PricePreferences(), - priceDTO = PriceDTO( - source = "Bitfinex.com", - widgets = listOf( - PriceWidgetData( - pair = TradingPair.BTC_USD, - change = Change( - isPositive = true, - formatted = "$ 20,326" - ), - price = "$20,326", - pastValues = listOf(1.0, 2.0, 3.0, 4.0), - period = GraphPeriod.ONE_DAY, - ), - PriceWidgetData( - pair = TradingPair.BTC_EUR, - change = Change( - isPositive = false, - formatted = "โ‚ฌ 20,326" - ), - price = "โ‚ฌ 20,326", - pastValues = listOf(1.0, 2.0, 3.0, 4.0), - period = GraphPeriod.ONE_DAY, - ) - ) - ), + priceDTO = SAMPLE_PRICE_DTO, isPriceWidgetEnabled = false, - isLoading = false + isLoading = false, ) } } @Preview(showBackground = true) @Composable -private fun Preview2() { +private fun PreviewWithDelete() { AppThemeSurface { PricePreviewContent( onBack = {}, - showWidgetTitles = false, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, pricePreferences = PricePreferences( - enabledPairs = listOf(TradingPair.BTC_USD, TradingPair.BTC_EUR), + enabledPairs = listOf(TradingPair.BTC_USD), period = GraphPeriod.ONE_WEEK, - showSource = true - ), - priceDTO = PriceDTO( - source = "Bitfinex.com", - widgets = listOf( - PriceWidgetData( - pair = TradingPair.BTC_USD, - change = Change( - isPositive = true, - formatted = "$ 20,326" - ), - price = "$20,326", - pastValues = listOf(1.0, 2.0, 3.0, 4.0), - period = GraphPeriod.ONE_DAY, - ), - PriceWidgetData( - pair = TradingPair.BTC_EUR, - change = Change( - isPositive = false, - formatted = "โ‚ฌ 20,326" - ), - price = "โ‚ฌ 20,326", - pastValues = listOf(1.0, 2.0, 3.0, 4.0), - period = GraphPeriod.ONE_DAY, - ) - ) ), + priceDTO = SAMPLE_PRICE_DTO, isPriceWidgetEnabled = true, - isLoading = false + isLoading = false, ) } } + +private val SAMPLE_PRICE_DTO = PriceDTO( + source = "Bitfinex.com", + widgets = listOf( + PriceWidgetData( + pair = TradingPair.BTC_USD, + change = Change(isPositive = true, formatted = "+1.24%"), + price = "75,326", + pastValues = listOf(1.0, 2.0, 1.5, 3.0, 2.5, 4.0), + period = GraphPeriod.ONE_DAY, + ), + ), +) diff --git a/app/src/main/res/drawable/chart_preview.xml b/app/src/main/res/drawable/chart_preview.xml index 5cbc84a10..ee3b28e75 100644 --- a/app/src/main/res/drawable/chart_preview.xml +++ b/app/src/main/res/drawable/chart_preview.xml @@ -1,15 +1,22 @@ - - - - + android:viewportHeight="96"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fc9803ac..5b17a9b4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1169,6 +1169,8 @@ Default Please select which fields you want to display in the {name} widget. Widget + Small + Wide Source Widgets From 3acc939e20eab50252c08459bd82f860599678d3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 11:26:17 -0300 Subject: [PATCH 062/100] feat: chart preview v61 --- app/src/main/res/drawable/chart_preview.xml | 29 ++++++++------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/drawable/chart_preview.xml b/app/src/main/res/drawable/chart_preview.xml index ee3b28e75..5cbc84a10 100644 --- a/app/src/main/res/drawable/chart_preview.xml +++ b/app/src/main/res/drawable/chart_preview.xml @@ -1,22 +1,15 @@ - - - - - - - - + android:viewportHeight="48"> + + + + From ff6993ce245471a55ac294811d3063f82dac05b5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:05:19 -0300 Subject: [PATCH 063/100] fix: padding --- .../ui/screens/widgets/price/PricePreviewScreen.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 6e828f9d5..c7a29c05b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -145,7 +145,6 @@ fun PricePreviewContent( pricePreferences = pricePreferences, priceDTO = priceDTO, modifier = Modifier - .weight(1f) .fillMaxWidth(), ) } else { @@ -155,7 +154,12 @@ fun PricePreviewContent( Row( modifier = Modifier - .padding(16.dp) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + top = 22.dp, + ) .fillMaxWidth() .testTag("buttons_row"), horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -246,7 +250,7 @@ private fun WidgetCarousel( textAlign = androidx.compose.ui.text.style.TextAlign.Center, ) - VerticalSpacer(8.dp) + VerticalSpacer(16.dp) Row( modifier = Modifier From 4a5061f9cf0ee865de292b68efe71da788350bc5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:10:20 -0300 Subject: [PATCH 064/100] fix: warp get data in runCatching --- app/src/main/java/to/bitkit/data/widgets/PriceService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt index 159bde54c..07c097fac 100644 --- a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt +++ b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt @@ -38,9 +38,9 @@ class PriceService @Inject constructor( override val refreshInterval = 1.minutes private val sourceLabel = "Bitfinex.com" - override suspend fun fetchData(): Result { + override suspend fun fetchData(): Result = runCatching { val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY - return fetchData(period) + fetchData(period).getOrThrow() } suspend fun fetchData(period: GraphPeriod): Result = runCatching { From d756a14a08cf10408987e1679d03599112b0d467 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:12:06 -0300 Subject: [PATCH 065/100] chore: lint --- .../to/bitkit/appwidget/ui/components/GlanceText.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt index 1d1c5bb17..b0b723a00 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceText.kt @@ -14,7 +14,7 @@ fun Subtitle( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.subtitle.withColor(color), maxLines = maxLines, modifier = modifier) } @Composable @@ -24,7 +24,7 @@ fun BodyMSB( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.bodyMSB.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.bodyMSB.withColor(color), maxLines = maxLines, modifier = modifier) } @Composable @@ -34,7 +34,7 @@ fun BodySSB( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.bodySSB.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.bodySSB.withColor(color), maxLines = maxLines, modifier = modifier) } @Composable @@ -44,7 +44,7 @@ fun BodySB( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.bodySB.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.bodySB.withColor(color), maxLines = maxLines, modifier = modifier) } @Composable @@ -54,7 +54,7 @@ fun CaptionB( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.captionB.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.captionB.withColor(color), maxLines = maxLines, modifier = modifier) } @Composable @@ -64,7 +64,7 @@ fun FootnoteM( color: ColorProvider? = null, maxLines: Int = Int.MAX_VALUE, ) { - Text(text = text, modifier = modifier, style = GlanceTextStyles.footnoteM.withColor(color), maxLines = maxLines) + Text(text = text, style = GlanceTextStyles.footnoteM.withColor(color), maxLines = maxLines, modifier = modifier) } private fun TextStyle.withColor(color: ColorProvider?): TextStyle = From c0821531ebc100056b729d1a799c9860f761aa78 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:17:35 -0300 Subject: [PATCH 066/100] chore: lint --- .../appwidget/config/AppWidgetConfigScreen.kt | 16 ++++++++-------- .../appwidget/ui/price/PriceGlanceContent.kt | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index b1af62ac2..3ff9247cb 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -73,7 +73,7 @@ private fun PriceConfigContent( modifier = Modifier .padding(horizontal = 16.dp) .weight(1f) - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) ) { VerticalSpacer(26.dp) @@ -118,17 +118,17 @@ private fun PriceConfigContent( } Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .padding(vertical = 21.dp, horizontal = 16.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), + .fillMaxWidth() ) { SecondaryButton( text = stringResource(R.string.common__reset), enabled = prefs != PricePreferences(), fullWidth = false, onClick = onReset, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) PrimaryButton( text = stringResource(R.string.common__save), @@ -136,7 +136,7 @@ private fun PriceConfigContent( enabled = !state.isSaving, fullWidth = false, onClick = onSave, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) } } @@ -154,19 +154,19 @@ private fun ConfigToggleRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(vertical = 12.dp) - .fillMaxWidth(), + .fillMaxWidth() ) { BodySSB( text = label, color = Colors.White64, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) IconButton(onClick = onClick) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, tint = if (isEnabled) Colors.Brand else Colors.White50, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(32.dp) ) } } diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 1b4c40b39..8ab8077d1 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -68,11 +68,11 @@ fun PriceGlanceContent( val chartWidget = displayWidgets.first() val chartColor = if (chartWidget.change.isPositive) Colors.Green else Colors.Red Box( + contentAlignment = Alignment.BottomStart, modifier = GlanceModifier .fillMaxWidth() .padding(top = 8.dp) - .then(HeightModifier(Dimension.Expand)), - contentAlignment = Alignment.BottomStart, + .then(HeightModifier(Dimension.Expand)) ) { Image( provider = ImageProvider(chartBitmap), @@ -81,12 +81,12 @@ fun PriceGlanceContent( modifier = GlanceModifier .fillMaxWidth() .fillMaxHeight() - .cornerRadius(8.dp), + .cornerRadius(8.dp) ) CaptionB( text = chartWidget.period.value, color = ColorProvider(day = chartColor, night = chartColor), - modifier = GlanceModifier.padding(7.dp), + modifier = GlanceModifier.padding(7.dp) ) } } @@ -97,13 +97,13 @@ fun PriceGlanceContent( @Composable private fun PriceRow(widget: PriceWidgetData) { Row( - modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp) ) { BodySB( text = widget.pair.displayName, color = GlanceColors.textSecondary, - modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) ) BodySB( text = widget.change.formatted, From 0a7085af282c41429a5e5a690e5fbfe3f3babf05 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:48:10 -0300 Subject: [PATCH 067/100] chore: lint --- .../to/bitkit/ui/screens/widgets/price/PriceCard.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index a71bccf59..43ac5db90 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -7,13 +7,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ShapeDefaults @@ -50,6 +48,8 @@ import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB +import to.bitkit.ui.components.HorizontalSpacer +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -91,7 +91,7 @@ fun PriceCard( .testTag("price_card_widget_title_icon"), tint = Color.Unspecified, ) - Spacer(modifier = Modifier.width(16.dp)) + HorizontalSpacer(16.dp) BodyMSB( text = stringResource(R.string.widgets__price__name), modifier = Modifier.testTag("price_card_widget_title_text"), @@ -112,7 +112,7 @@ fun PriceCard( .weight(1f) .testTag("PriceWidgetRow-${widgetData.pair.displayName}"), ) - Spacer(modifier = Modifier.width(16.dp)) + HorizontalSpacer(16.dp) Text( text = widgetData.change.formatted, color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, @@ -144,7 +144,7 @@ fun PriceCard( ) if (pricePreferences.showSource) { - Spacer(modifier = Modifier.height(8.dp)) + VerticalSpacer(8.dp) Row( horizontalArrangement = Arrangement.SpaceBetween, From 21f40536643243416edc1eae0898e9ec6f488b81 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:48:17 -0300 Subject: [PATCH 068/100] chore: lint --- .../to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 9e8f1f03e..9d36446b6 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -48,7 +49,7 @@ class AppWidgetConfigViewModel @Inject constructor( fun selectPricePair(pair: TradingPair) { _uiState.update { - it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = listOf(pair))) + it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = persistentListOf(pair))) } } From abd9cb8938784f583a4d48c0e62547c602ead729 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:57:58 -0300 Subject: [PATCH 069/100] chore: lint --- .../to/bitkit/ui/screens/widgets/price/PriceViewModel.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt index cc7045f1f..29b4c49ac 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt @@ -50,13 +50,6 @@ class PriceViewModel @Inject constructor( initialValue = false ) - val showWidgetTitles: StateFlow = widgetsRepo.showWidgetTitles - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT), - initialValue = true - ) - val currentPrice: StateFlow = widgetsRepo.priceFlow .stateIn( scope = viewModelScope, @@ -92,7 +85,7 @@ class PriceViewModel @Inject constructor( } fun selectTradingPair(pair: TradingPair) { - _customPreferences.update { it.copy(enabledPairs = listOf(pair)) } + _customPreferences.update { it.copy(enabledPairs = persistentListOf(pair)) } } fun resetCustomPreferences() { From f061ec5ac1235772bc3717fd4edf54d43ba66c72 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 13:58:24 -0300 Subject: [PATCH 070/100] chore: lint --- .../ui/screens/widgets/price/PricePreviewScreen.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index c7a29c05b..fee8e82ea 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -244,19 +245,19 @@ private fun WidgetCarousel( }, ), color = Colors.White64, + textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .testTag("widget_size_label"), - textAlign = androidx.compose.ui.text.style.TextAlign.Center, + .testTag("widget_size_label") ) VerticalSpacer(16.dp) Row( + horizontalArrangement = Arrangement.Center, modifier = Modifier .fillMaxWidth() - .testTag("page_indicator"), - horizontalArrangement = Arrangement.Center, + .testTag("page_indicator") ) { repeat(PAGE_COUNT) { index -> Box( From a95166a7843fa6c1e35c660e8a1d44ee3842c82a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 27 Apr 2026 14:01:49 -0300 Subject: [PATCH 071/100] chore: lint --- .../appwidget/ui/price/PriceGlanceContent.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 5c8fe90b1..c4349fca4 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -3,6 +3,7 @@ package to.bitkit.appwidget.ui.price import android.appwidget.AppWidgetManager import android.content.Intent import android.graphics.Bitmap +import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier @@ -32,6 +33,7 @@ import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles +import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors @@ -70,13 +72,14 @@ fun PriceGlanceContent( @Composable private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red + val periodLabel = LocalContext.current.getString(widget.period.labelRes()) Row( modifier = GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "${widget.pair.displayName} ${widget.period.value}".uppercase(), + text = "${widget.pair.displayName} $periodLabel".uppercase(), style = GlanceTextStyles.captionUp, modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), ) @@ -102,6 +105,7 @@ private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { @Composable private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red + val periodLabel = LocalContext.current.getString(widget.period.labelRes()) Row(modifier = GlanceModifier.fillMaxWidth()) { Text( @@ -110,7 +114,7 @@ private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), ) Text( - text = widget.period.value.uppercase(), + text = periodLabel.uppercase(), style = GlanceTextStyles.captionUp, ) } @@ -151,3 +155,11 @@ private fun ChartBox(chartBitmap: Bitmap?) { ) } } + +@StringRes +private fun GraphPeriod.labelRes(): Int = when (this) { + GraphPeriod.ONE_DAY -> R.string.appwidget__price__day + GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week + GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month + GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year +} From d78cc7f7f304cfcd63c1f21621809f2c7dccd1de Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 08:55:40 -0300 Subject: [PATCH 072/100] refactor: extract reusable dimen constant --- .../appwidget/ui/components/GlanceLayoutDimens.kt | 11 +++++++++++ .../bitkit/appwidget/ui/price/PriceGlanceContent.kt | 5 ++--- .../to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt | 7 ++----- 3 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/components/GlanceLayoutDimens.kt diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceLayoutDimens.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceLayoutDimens.kt new file mode 100644 index 000000000..d23ba8881 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceLayoutDimens.kt @@ -0,0 +1,11 @@ +package to.bitkit.appwidget.ui.components + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +object GlanceLayoutDimens { + val WIDE_LAYOUT_MIN_WIDTH = 280.dp + + val COMPACT_WIDGET_SIZE = DpSize(163.dp, 192.dp) + val WIDE_WIDGET_SIZE = DpSize(343.dp, 152.dp) +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index c4349fca4..25ccdaf5f 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -32,13 +32,12 @@ import to.bitkit.appwidget.ui.components.CaptionB import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.components.VerticalSpacer +import to.bitkit.appwidget.ui.components.GlanceLayoutDimens import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors -private val WIDE_LAYOUT_MIN_WIDTH = 280.dp - @Suppress("RestrictedApi") @Composable fun PriceGlanceContent( @@ -60,7 +59,7 @@ fun PriceGlanceContent( return@GlanceWidgetScaffold } - if (LocalSize.current.width >= WIDE_LAYOUT_MIN_WIDTH) { + if (LocalSize.current.width >= GlanceLayoutDimens.WIDE_LAYOUT_MIN_WIDTH) { WideContent(widget = widget, chartBitmap = chartBitmap) } else { CompactContent(widget = widget, chartBitmap = chartBitmap) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index 3236abf33..781ed6951 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -6,8 +6,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import androidx.glance.GlanceId import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager @@ -18,6 +16,7 @@ import to.bitkit.appwidget.AppWidgetEntryPoint 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.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors @@ -27,12 +26,10 @@ class PriceGlanceWidget : GlanceAppWidget() { companion object { private const val CHART_WIDTH = 600 private const val CHART_HEIGHT = 200 - val COMPACT = DpSize(163.dp, 192.dp) - val WIDE = DpSize(343.dp, 152.dp) } override val sizeMode = SizeMode.Responsive( - setOf(COMPACT, WIDE), + setOf(GlanceLayoutDimens.COMPACT_WIDGET_SIZE, GlanceLayoutDimens.WIDE_WIDGET_SIZE), ) override suspend fun provideGlance(context: Context, id: GlanceId) { From d0e7f4fdde8d73d5ddb6d881fcd68d54d12cee6b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 08:58:45 -0300 Subject: [PATCH 073/100] refactor: extract reusable dimens from xml --- app/src/main/res/values/dimens.xml | 5 +++++ app/src/main/res/values/integers.xml | 5 +++++ app/src/main/res/xml/appwidget_info_price.xml | 8 ++++---- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/integers.xml diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..dff979e1b --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 140dp + 110dp + diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 000000000..7f3f94908 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 4 + 2 + diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index e0c3ece22..d35e55edc 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -1,9 +1,9 @@ Date: Tue, 28 Apr 2026 09:01:05 -0300 Subject: [PATCH 074/100] chore: handle warnings --- app/src/main/res/xml/appwidget_info_price.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index d35e55edc..81e922f63 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -1,5 +1,6 @@ + android:updatePeriodMillis="0" + tools:targetApi="31" /> From f8a2c51eeee908c5d0302fdd0a83082eb7a6e23e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 09:16:09 -0300 Subject: [PATCH 075/100] fix: round OS widget corners for API level < 31 --- .../bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt | 7 +++---- .../main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt | 1 - app/src/main/res/drawable/appwidget_background.xml | 6 ++++++ 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/appwidget_background.xml diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt index 117f1fc9e..37aed3364 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt @@ -4,14 +4,14 @@ import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier +import androidx.glance.ImageProvider import androidx.glance.action.clickable import androidx.glance.appwidget.action.actionStartActivity -import androidx.glance.appwidget.cornerRadius import androidx.glance.background import androidx.glance.layout.Column import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding -import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.R @Composable fun GlanceWidgetScaffold( @@ -20,8 +20,7 @@ fun GlanceWidgetScaffold( ) { val modifier = GlanceModifier .fillMaxSize() - .cornerRadius(16.dp) - .background(GlanceColors.cardBackgroundProvider) + .background(ImageProvider(R.drawable.appwidget_background)) .padding(16.dp) .let { mod -> if (onClick != null) mod.clickable(actionStartActivity(onClick)) else mod diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt index 427bb161d..b8705ff13 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceColors.kt @@ -4,7 +4,6 @@ import androidx.glance.color.ColorProvider import to.bitkit.ui.theme.Colors object GlanceColors { - val cardBackgroundProvider = ColorProvider(day = Colors.Gray5, night = Colors.Gray5) val textPrimary = ColorProvider(day = Colors.White, night = Colors.White) val textSecondary = ColorProvider(day = Colors.White64, night = Colors.White64) val textTertiary = ColorProvider(day = Colors.White50, night = Colors.White50) diff --git a/app/src/main/res/drawable/appwidget_background.xml b/app/src/main/res/drawable/appwidget_background.xml new file mode 100644 index 000000000..6754ffd18 --- /dev/null +++ b/app/src/main/res/drawable/appwidget_background.xml @@ -0,0 +1,6 @@ + + + + + From 5920e7991df6d3248b5cf13f172a9545aa5948ce Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 09:27:43 -0300 Subject: [PATCH 076/100] fix: match min sizes for API < 31 --- app/src/main/res/values/dimens.xml | 4 +++- app/src/main/res/xml/appwidget_info_price.xml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index dff979e1b..96bba3e1e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,7 @@ - 140dp + 250dp 110dp + 110dp + 110dp diff --git a/app/src/main/res/xml/appwidget_info_price.xml b/app/src/main/res/xml/appwidget_info_price.xml index 81e922f63..94fe99672 100644 --- a/app/src/main/res/xml/appwidget_info_price.xml +++ b/app/src/main/res/xml/appwidget_info_price.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:minWidth="@dimen/appwidget_min_width" android:minHeight="@dimen/appwidget_min_height" + android:minResizeWidth="@dimen/appwidget_min_resize_width" + android:minResizeHeight="@dimen/appwidget_min_resize_height" android:targetCellWidth="@integer/appwidget_target_cell_width" android:targetCellHeight="@integer/appwidget_target_cell_height" android:resizeMode="horizontal|vertical" From ea94adf8f2bf1313ad551706939159dee943695b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 09:29:11 -0300 Subject: [PATCH 077/100] fix: glance_default_loading_layout.xml round corners for API < 31 --- app/src/main/res/layout/glance_default_loading_layout.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/glance_default_loading_layout.xml b/app/src/main/res/layout/glance_default_loading_layout.xml index b294f56e7..882ac72d0 100644 --- a/app/src/main/res/layout/glance_default_loading_layout.xml +++ b/app/src/main/res/layout/glance_default_loading_layout.xml @@ -2,7 +2,7 @@ Date: Tue, 28 Apr 2026 11:57:10 -0300 Subject: [PATCH 078/100] chore: lint --- .../appwidget/ui/price/PriceGlanceContent.kt | 14 ++--- .../ui/screens/widgets/price/PriceCard.kt | 58 +++++++++---------- .../widgets/price/PricePreviewScreen.kt | 38 ++++++------ 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index 25ccdaf5f..feb909f1d 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -74,13 +74,13 @@ private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { val periodLabel = LocalContext.current.getString(widget.period.labelRes()) Row( - modifier = GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.fillMaxWidth() ) { Text( text = "${widget.pair.displayName} $periodLabel".uppercase(), style = GlanceTextStyles.captionUp, - modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) ) HorizontalSpacer(16.dp) Text( @@ -94,7 +94,7 @@ private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { Text( text = "${widget.pair.symbol} ${widget.price}", style = GlanceTextStyles.headline34, - modifier = GlanceModifier.fillMaxWidth(), + modifier = GlanceModifier.fillMaxWidth() ) VerticalSpacer(8.dp) ChartBox(chartBitmap = chartBitmap) @@ -110,7 +110,7 @@ private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { Text( text = widget.pair.displayName.uppercase(), style = GlanceTextStyles.captionUp, - modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)), + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) ) Text( text = periodLabel.uppercase(), @@ -121,7 +121,7 @@ private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { Text( text = "${widget.pair.symbol} ${widget.price}", style = GlanceTextStyles.title22, - modifier = GlanceModifier.fillMaxWidth(), + modifier = GlanceModifier.fillMaxWidth() ) VerticalSpacer(8.dp) Text( @@ -141,7 +141,7 @@ private fun ChartBox(chartBitmap: Bitmap?) { modifier = GlanceModifier .fillMaxWidth() .then(HeightModifier(Dimension.Expand)) - .padding(vertical = 16.dp), + .padding(vertical = 16.dp) ) { Image( provider = ImageProvider(chartBitmap), @@ -150,7 +150,7 @@ private fun ChartBox(chartBitmap: Bitmap?) { modifier = GlanceModifier .fillMaxWidth() .fillMaxHeight() - .cornerRadius(8.dp), + .cornerRadius(8.dp) ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 43ac5db90..42dacd807 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -68,49 +68,49 @@ fun PriceCard( Box( modifier = modifier .clip(shape = MaterialTheme.shapes.medium) - .background(Colors.White10), + .background(Colors.White10) ) { Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(16.dp) ) { if (showWidgetTitle) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(bottom = 8.dp) - .testTag("price_card_widget_title_row"), + .testTag("price_card_widget_title_row") ) { Icon( painter = painterResource(R.drawable.widget_chart_line), contentDescription = null, + tint = Color.Unspecified, modifier = Modifier .size(32.dp) - .testTag("price_card_widget_title_icon"), - tint = Color.Unspecified, + .testTag("price_card_widget_title_icon") ) HorizontalSpacer(16.dp) BodyMSB( text = stringResource(R.string.widgets__price__name), - modifier = Modifier.testTag("price_card_widget_title_text"), + modifier = Modifier.testTag("price_card_widget_title_text") ) } } Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .testTag("price_card_pair_row_${widgetData.pair.displayName}"), - verticalAlignment = Alignment.CenterVertically, + .testTag("price_card_pair_row_${widgetData.pair.displayName}") ) { Caption13Up( text = "${widgetData.pair.displayName} ${widgetData.period.value}", color = Colors.White64, modifier = Modifier .weight(1f) - .testTag("PriceWidgetRow-${widgetData.pair.displayName}"), + .testTag("PriceWidgetRow-${widgetData.pair.displayName}") ) HorizontalSpacer(16.dp) Text( @@ -119,7 +119,7 @@ fun PriceCard( fontSize = 22.sp, lineHeight = 26.sp, fontWeight = FontWeight.Bold, - modifier = Modifier.testTag("price_card_pair_change_${widgetData.pair}"), + modifier = Modifier.testTag("price_card_pair_change_${widgetData.pair}") ) } @@ -131,7 +131,7 @@ fun PriceCard( fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxWidth() - .testTag("price_card_pair_price_${widgetData.pair}"), + .testTag("price_card_pair_price_${widgetData.pair}") ) ChartComponent( @@ -140,7 +140,7 @@ fun PriceCard( .fillMaxWidth() .height(48.dp) .padding(top = 8.dp) - .testTag("price_card_chart"), + .testTag("price_card_chart") ) if (pricePreferences.showSource) { @@ -150,17 +150,17 @@ fun PriceCard( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() - .testTag("PriceWidgetSource"), + .testTag("PriceWidgetSource") ) { CaptionB( text = stringResource(R.string.widgets__widget__source), color = Colors.White64, - modifier = Modifier.testTag("source_label"), + modifier = Modifier.testTag("source_label") ) CaptionB( text = priceDTO.source, color = Colors.White64, - modifier = Modifier.testTag("source_text"), + modifier = Modifier.testTag("source_text") ) } } @@ -182,22 +182,22 @@ fun PriceCardSmall( Box( modifier = modifier .clip(shape = MaterialTheme.shapes.medium) - .background(Colors.White10), + .background(Colors.White10) ) { Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + .padding(16.dp) ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row( + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() - .testTag("price_card_small_pair_row_${widgetData.pair.displayName}"), - horizontalArrangement = Arrangement.SpaceBetween, + .testTag("price_card_small_pair_row_${widgetData.pair.displayName}") ) { Caption13Up( text = widgetData.pair.displayName, @@ -216,7 +216,7 @@ fun PriceCardSmall( fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxWidth() - .testTag("price_card_small_pair_price_${widgetData.pair}"), + .testTag("price_card_small_pair_price_${widgetData.pair}") ) Text( text = widgetData.change.formatted, @@ -224,7 +224,7 @@ fun PriceCardSmall( fontSize = 15.sp, lineHeight = 20.sp, fontWeight = FontWeight.SemiBold, - modifier = Modifier.testTag("price_card_small_pair_change_${widgetData.pair}"), + modifier = Modifier.testTag("price_card_small_pair_change_${widgetData.pair}") ) } @@ -233,7 +233,7 @@ fun PriceCardSmall( modifier = Modifier .fillMaxWidth() .weight(1f) - .testTag("price_card_small_chart"), + .testTag("price_card_small_chart") ) } } @@ -254,10 +254,9 @@ fun ChartComponent( } Box( - modifier = modifier.clip(ShapeDefaults.Small), + modifier = modifier.clip(ShapeDefaults.Small) ) { LineChart( - modifier = Modifier.fillMaxSize(), data = remember(widgetData.pastValues, baseColor) { listOf( Line( @@ -288,6 +287,7 @@ fun ChartComponent( ), minValue = minValue, maxValue = maxValue, + modifier = Modifier.fillMaxSize() ) } } @@ -299,13 +299,12 @@ private val SAMPLE_PAST_VALUES = listOf(1.0, 2.0, 1.5, 3.0, 2.5, 4.0) private fun FullBlockCardPreview() { AppThemeSurface { Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(16.dp) ) { PriceCard( - modifier = Modifier.fillMaxWidth(), showWidgetTitle = true, pricePreferences = PricePreferences( showSource = true, @@ -325,6 +324,7 @@ private fun FullBlockCardPreview() { ), ), ), + modifier = Modifier.fillMaxWidth() ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index fee8e82ea..36d84b5e7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -102,7 +102,7 @@ fun PricePreviewContent( isLoading: Boolean, ) { ScreenColumn( - modifier = Modifier.testTag("price_preview_screen"), + modifier = Modifier.testTag("price_preview_screen") ) { AppTopBar( titleText = stringResource(R.string.widgets__price__name), @@ -112,20 +112,20 @@ fun PricePreviewContent( Column( modifier = Modifier .padding(horizontal = 16.dp) - .weight(1f), + .weight(1f) ) { VerticalSpacer(16.dp) BodyM( text = stringResource(R.string.widgets__price__description), color = Colors.White64, - modifier = Modifier.testTag("widget_description"), + modifier = Modifier.testTag("widget_description") ) VerticalSpacer(16.dp) HorizontalDivider( - modifier = Modifier.testTag("divider"), + modifier = Modifier.testTag("divider") ) SettingsButtonRow( @@ -138,7 +138,7 @@ fun PricePreviewContent( }, ), onClick = onClickEdit, - modifier = Modifier.testTag("WidgetEdit"), + modifier = Modifier.testTag("WidgetEdit") ) if (priceDTO != null) { @@ -146,7 +146,7 @@ fun PricePreviewContent( pricePreferences = pricePreferences, priceDTO = priceDTO, modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() ) } else { Box(modifier = Modifier.weight(1f)) @@ -154,6 +154,7 @@ fun PricePreviewContent( } Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .padding( start = 16.dp, @@ -162,8 +163,7 @@ fun PricePreviewContent( top = 22.dp, ) .fillMaxWidth() - .testTag("buttons_row"), - horizontalArrangement = Arrangement.spacedBy(16.dp), + .testTag("buttons_row") ) { if (isPriceWidgetEnabled) { SecondaryButton( @@ -172,7 +172,7 @@ fun PricePreviewContent( onClick = onClickDelete, modifier = Modifier .weight(1f) - .testTag("WidgetDelete"), + .testTag("WidgetDelete") ) } @@ -183,7 +183,7 @@ fun PricePreviewContent( onClick = onClickSave, modifier = Modifier .weight(1f) - .testTag("WidgetSave"), + .testTag("WidgetSave") ) } } @@ -198,37 +198,37 @@ private fun WidgetCarousel( val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) Column( - modifier = modifier.testTag("price_preview_carousel"), verticalArrangement = Arrangement.Center, + modifier = modifier.testTag("price_preview_carousel") ) { HorizontalPager( state = pagerState, modifier = Modifier .fillMaxWidth() .weight(1f) - .testTag("price_preview_pager"), + .testTag("price_preview_pager") ) { page -> Box( - modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() ) { when (page) { PAGE_SMALL -> PriceCardSmall( + pricePreferences = pricePreferences, + priceDTO = priceDTO, modifier = Modifier .width(163.dp) .height(192.dp) - .testTag("price_card_small"), - pricePreferences = pricePreferences, - priceDTO = priceDTO, + .testTag("price_card_small") ) PAGE_WIDE -> PriceCard( - modifier = Modifier - .fillMaxWidth() - .testTag("price_card_wide"), showWidgetTitle = false, pricePreferences = pricePreferences, priceDTO = priceDTO, + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_wide") ) } } From d50e73f5d82972ff93d734a05187e5e1df2d9a86 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 13:44:24 -0300 Subject: [PATCH 079/100] doc: changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7108ba3f5..bb766a921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Home screen widgets foundation with Glance, including price widget as the first implementation #895 +### Changed +- Redesign price widget with v61 wide and compact layouts, new preview and edit screens, and tap-to-edit behavior #914 + ## [2.2.0] - 2026-04-07 ### Fixed From 2db1167dde91067584e46c2aae78d41e5e5ae0a5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 13:59:25 -0300 Subject: [PATCH 080/100] refactor: reuse existent method --- .../bitkit/appwidget/config/AppWidgetConfigScreen.kt | 11 +---------- .../ui/screens/widgets/price/PriceEditScreen.kt | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index fa521ce11..6e7c6513a 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -31,6 +31,7 @@ import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.widgets.price.label import to.bitkit.ui.theme.Colors @Composable @@ -166,13 +167,3 @@ private fun SelectableRow( HorizontalDivider() } } - -@Composable -private fun GraphPeriod.label(): String = stringResource( - when (this) { - GraphPeriod.ONE_DAY -> R.string.appwidget__price__day - GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week - GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month - GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year - }, -) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index 4c74b7484..e39c306f3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -216,7 +216,7 @@ private fun SelectableRow( } @Composable -private fun GraphPeriod.label(): String = stringResource( +fun GraphPeriod.label(): String = stringResource( when (this) { GraphPeriod.ONE_DAY -> R.string.appwidget__price__day GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week From 5de9f16de1ce93db5e1af23b4bad97ed7835db08 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 14:09:46 -0300 Subject: [PATCH 081/100] chore: lint --- .../main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt | 1 - .../to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 42dacd807..2e0554bed 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -139,7 +139,6 @@ fun PriceCard( modifier = Modifier .fillMaxWidth() .height(48.dp) - .padding(top = 8.dp) .testTag("price_card_chart") ) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 36d84b5e7..3b70865ab 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -191,9 +191,9 @@ fun PricePreviewContent( @Composable private fun WidgetCarousel( + modifier: Modifier = Modifier, pricePreferences: PricePreferences, priceDTO: PriceDTO, - modifier: Modifier = Modifier, ) { val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) From af526a8e81e0136b055070f118254e285ddac9a3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 14:29:12 -0300 Subject: [PATCH 082/100] fix: stops the curve from overshot above/below the canvas at sharp angles, where it was being clipped and showing as gaps --- .../to/bitkit/appwidget/ui/price/LineChartBitmap.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt index cb6a17173..eba40c360 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt @@ -34,7 +34,7 @@ fun renderLineChartBitmap( x to y } - val linePath = buildSmoothPath(points) + val linePath = buildSmoothPath(points, yMin = padding, yMax = padding + drawHeight) val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = lineColor @@ -48,7 +48,11 @@ fun renderLineChartBitmap( return bitmap } -private fun buildSmoothPath(points: List>): Path = Path().apply { +private fun buildSmoothPath( + points: List>, + yMin: Float, + yMax: Float, +): Path = Path().apply { moveTo(points[0].first, points[0].second) for (i in 0 until points.size - 1) { val p0 = points[(i - 1).coerceAtLeast(0)] @@ -57,9 +61,9 @@ private fun buildSmoothPath(points: List>): Path = Path().app val p3 = points[(i + 2).coerceAtMost(points.lastIndex)] val cp1x = p1.first + (p2.first - p0.first) * SMOOTHING - val cp1y = p1.second + (p2.second - p0.second) * SMOOTHING + val cp1y = (p1.second + (p2.second - p0.second) * SMOOTHING).coerceIn(yMin, yMax) val cp2x = p2.first - (p3.first - p1.first) * SMOOTHING - val cp2y = p2.second - (p3.second - p1.second) * SMOOTHING + val cp2y = (p2.second - (p3.second - p1.second) * SMOOTHING).coerceIn(yMin, yMax) cubicTo(cp1x, cp1y, cp2x, cp2y, p2.first, p2.second) } From bfd94d794c10a36bc92e7bf11e3243db2a61900b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 14:49:26 -0300 Subject: [PATCH 083/100] feat: remove widget title --- .../bitkit/ui/screens/wallets/HomeScreen.kt | 1 - .../ui/screens/widgets/price/PriceCard.kt | 30 ------------------- .../widgets/price/PricePreviewScreen.kt | 1 - 3 files changed, 32 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 6b6dd21cb..a10e88ea7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -784,7 +784,6 @@ private fun Widgets( WidgetType.PRICE -> { homeUiState.currentPrice?.run { PriceCard( - showWidgetTitle = homeUiState.showWidgetTitles, pricePreferences = homeUiState.pricePreferences, priceDTO = homeUiState.currentPrice, modifier = Modifier diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 2e0554bed..fa0dbe93b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Text @@ -21,10 +19,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -45,7 +41,6 @@ import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences -import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.HorizontalSpacer @@ -56,7 +51,6 @@ import to.bitkit.ui.theme.Colors @Composable fun PriceCard( modifier: Modifier = Modifier, - showWidgetTitle: Boolean, pricePreferences: PricePreferences, priceDTO: PriceDTO, ) { @@ -76,29 +70,6 @@ fun PriceCard( .fillMaxWidth() .padding(16.dp) ) { - if (showWidgetTitle) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(bottom = 8.dp) - .testTag("price_card_widget_title_row") - ) { - Icon( - painter = painterResource(R.drawable.widget_chart_line), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier - .size(32.dp) - .testTag("price_card_widget_title_icon") - ) - HorizontalSpacer(16.dp) - BodyMSB( - text = stringResource(R.string.widgets__price__name), - modifier = Modifier.testTag("price_card_widget_title_text") - ) - } - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -304,7 +275,6 @@ private fun FullBlockCardPreview() { .padding(16.dp) ) { PriceCard( - showWidgetTitle = true, pricePreferences = PricePreferences( showSource = true, ), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 3b70865ab..79f4be1ab 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -223,7 +223,6 @@ private fun WidgetCarousel( ) PAGE_WIDE -> PriceCard( - showWidgetTitle = false, pricePreferences = pricePreferences, priceDTO = priceDTO, modifier = Modifier From ba45f42f1ea607fc9467c96ae47392857a2f67cf Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 14:53:39 -0300 Subject: [PATCH 084/100] feat: remove source field --- .../java/to/bitkit/data/dto/price/PriceDTO.kt | 1 - .../to/bitkit/data/widgets/PriceService.kt | 5 ++-- .../bitkit/ui/screens/wallets/HomeScreen.kt | 1 - .../ui/screens/widgets/price/PriceCard.kt | 27 ------------------- .../widgets/price/PricePreviewScreen.kt | 1 - 5 files changed, 2 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt b/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt index 22243cf7b..f878f7efe 100644 --- a/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt +++ b/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt @@ -7,5 +7,4 @@ import kotlinx.serialization.Serializable @Serializable data class PriceDTO( @Stable val widgets: List, - val source: String ) diff --git a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt index 07c097fac..5da7a4938 100644 --- a/app/src/main/java/to/bitkit/data/widgets/PriceService.kt +++ b/app/src/main/java/to/bitkit/data/widgets/PriceService.kt @@ -36,7 +36,6 @@ class PriceService @Inject constructor( override val widgetType = WidgetType.PRICE override val refreshInterval = 1.minutes - private val sourceLabel = "Bitfinex.com" override suspend fun fetchData(): Result = runCatching { val period = widgetsStore.data.first().pricePreferences.period ?: GraphPeriod.ONE_DAY @@ -50,7 +49,7 @@ class PriceService @Inject constructor( .getOrNull() } if (widgets.isEmpty()) throw PriceError.InvalidResponse("No price data available") - PriceDTO(widgets = widgets, source = sourceLabel) + PriceDTO(widgets = widgets) }.onFailure { Logger.warn(e = it, msg = "Failed to fetch price data", context = TAG) } @@ -62,7 +61,7 @@ class PriceService @Inject constructor( val widgets = TradingPair.entries.mapNotNull { pair -> runCatching { fetchPairData(pair = pair, period = period) }.getOrNull() } - PriceDTO(widgets = widgets, source = sourceLabel) + PriceDTO(widgets = widgets) } }.awaitAll().filter { it.widgets.isNotEmpty() } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index a10e88ea7..b15c91d27 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -981,7 +981,6 @@ private val previewArticle = ArticleModel( ) private val previewPrice = PriceDTO( - source = "Bitfinex.com", widgets = listOf( PriceWidgetData( pair = TradingPair.BTC_USD, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index fa0dbe93b..b02ba80f3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -34,7 +33,6 @@ import ir.ehsannarmani.compose_charts.models.HorizontalIndicatorProperties import ir.ehsannarmani.compose_charts.models.LabelHelperProperties import ir.ehsannarmani.compose_charts.models.LabelProperties import ir.ehsannarmani.compose_charts.models.Line -import to.bitkit.R import to.bitkit.data.dto.price.Change import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO @@ -42,9 +40,7 @@ import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.Caption13Up -import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.HorizontalSpacer -import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -112,28 +108,6 @@ fun PriceCard( .height(48.dp) .testTag("price_card_chart") ) - - if (pricePreferences.showSource) { - VerticalSpacer(8.dp) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .testTag("PriceWidgetSource") - ) { - CaptionB( - text = stringResource(R.string.widgets__widget__source), - color = Colors.White64, - modifier = Modifier.testTag("source_label") - ) - CaptionB( - text = priceDTO.source, - color = Colors.White64, - modifier = Modifier.testTag("source_text") - ) - } - } } } } @@ -279,7 +253,6 @@ private fun FullBlockCardPreview() { showSource = true, ), priceDTO = PriceDTO( - source = "Bitfinex.com", widgets = listOf( PriceWidgetData( pair = TradingPair.BTC_USD, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 79f4be1ab..ddc1c4b08 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -311,7 +311,6 @@ private fun PreviewWithDelete() { } private val SAMPLE_PRICE_DTO = PriceDTO( - source = "Bitfinex.com", widgets = listOf( PriceWidgetData( pair = TradingPair.BTC_USD, From 9757b2b5d9ef122bfae3130a45a8a71605fa3915 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 14:56:02 -0300 Subject: [PATCH 085/100] feat: display mock image on preview --- .../bitkit/ui/screens/widgets/price/PriceCard.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index b02ba80f3..f248c2e90 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -2,6 +2,7 @@ package to.bitkit.ui.screens.widgets.price import androidx.compose.animation.core.EaseInOutCubic import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -20,7 +21,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,6 +37,7 @@ import ir.ehsannarmani.compose_charts.models.HorizontalIndicatorProperties import ir.ehsannarmani.compose_charts.models.LabelHelperProperties import ir.ehsannarmani.compose_charts.models.LabelProperties import ir.ehsannarmani.compose_charts.models.Line +import to.bitkit.R import to.bitkit.data.dto.price.Change import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO @@ -200,6 +205,16 @@ fun ChartComponent( Box( modifier = modifier.clip(ShapeDefaults.Small) ) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(R.drawable.chart_preview), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = Modifier.fillMaxSize() + ) + return@Box + } + LineChart( data = remember(widgetData.pastValues, baseColor) { listOf( From fd42d62f4c962ede73885a5365a333d43c036fd4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 07:23:26 -0300 Subject: [PATCH 086/100] feat: update bg color --- .../to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 6 +++++- .../to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt | 5 ++++- .../bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 6e7c6513a..7629dbdf6 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -1,5 +1,6 @@ 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 @@ -66,7 +67,10 @@ private fun PriceConfigContent( val prefs = state.pricePreferences val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD - ScreenColumn { + ScreenColumn( + noBackground = true, + modifier = Modifier.background(Colors.Gray7) + ) { AppTopBar( titleText = stringResource(R.string.widgets__price__name), onBackClick = onCancel, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index e39c306f3..45009c6f4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -76,7 +76,10 @@ fun PriceEditContent( val selectedPair = preferences.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD ScreenColumn( - modifier = Modifier.testTag("price_edit_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("price_edit_screen") ) { Box( modifier = Modifier diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index ddc1c4b08..f334a524d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -102,7 +102,10 @@ fun PricePreviewContent( isLoading: Boolean, ) { ScreenColumn( - modifier = Modifier.testTag("price_preview_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("price_preview_screen") ) { AppTopBar( titleText = stringResource(R.string.widgets__price__name), From 7f730aebbd079978c7498d2eed0b0891279d351b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 09:00:22 -0300 Subject: [PATCH 087/100] chore: lint --- .../java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index feb909f1d..da85b3fb2 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -29,10 +29,10 @@ import to.bitkit.appwidget.config.AppWidgetConfigActivity import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType 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.components.HorizontalSpacer import to.bitkit.appwidget.ui.components.VerticalSpacer -import to.bitkit.appwidget.ui.components.GlanceLayoutDimens import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceWidgetData From 074d79c7c311d65b3ae232c8a76aaea9a0c0c7a0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 11:27:15 -0300 Subject: [PATCH 088/100] fix: remove drawer button --- .../java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index 45009c6f4..9d89e8506 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -38,7 +38,6 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -132,7 +131,6 @@ fun PriceEditContent( AppTopBar( titleText = stringResource(R.string.widgets__widget__edit), onBackClick = onBack, - actions = { DrawerNavIcon() }, modifier = Modifier.background( Brush.verticalGradient( colors = listOf( From 8f3f5a89f25fb473ea6a65dad857aaa775ed4457 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 30 Apr 2026 08:21:51 -0300 Subject: [PATCH 089/100] chore: lint --- .../to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index f334a524d..9b6e76a0d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -269,7 +269,7 @@ private fun WidgetCarousel( .background( color = if (pagerState.currentPage == index) Colors.White else Colors.White32, shape = CircleShape, - ), + ) ) } } From 57fd39ca3f37c0e13d35c84ac42029bc5ccb9b23 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 30 Apr 2026 13:34:42 -0300 Subject: [PATCH 090/100] doc: migrate changelog entry to fragment changelog pattern --- CHANGELOG.md | 3 --- changelog.d/next/895.added.md | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 changelog.d/next/895.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6207432bc..db4df41e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Align top bar back arrow and passphrase input cursor/placeholder with iOS #906 - Polish Terms of Use screen padding to match iOS #903 -### Added -- Home screen widgets foundation with Glance, including price widget as the first implementation #895 - ## [2.2.0] - 2026-04-07 ### Fixed diff --git a/changelog.d/next/895.added.md b/changelog.d/next/895.added.md new file mode 100644 index 000000000..e4b308242 --- /dev/null +++ b/changelog.d/next/895.added.md @@ -0,0 +1 @@ +Home screen widgets foundation with Glance, including price widget as the first implementation From ad3c3cd226b401f25abbb6264ced0835b6ae2f64 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 10:15:17 -0300 Subject: [PATCH 091/100] fix: guard widget config init on recreate --- .../java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index 049d383e2..111b38727 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -42,7 +42,7 @@ class AppWidgetConfigActivity : ComponentActivity() { val type = typeName?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } ?: AppWidgetType.PRICE - viewModel.init(appWidgetId, type) + if (savedInstanceState == null) viewModel.init(appWidgetId, type) setContent { AppThemeSurface { From 245021be7d20192a0608c493cd496a74062189d6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 10:15:26 -0300 Subject: [PATCH 092/100] fix: rename content composable --- .../java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 3ff9247cb..f8bf97dc3 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -42,7 +42,7 @@ fun AppWidgetConfigScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() when (state.type) { - AppWidgetType.PRICE -> PriceConfigContent( + AppWidgetType.PRICE -> Content( state = state, onTogglePair = { viewModel.togglePricePair(it) }, onSelectPeriod = { viewModel.selectPricePeriod(it) }, @@ -54,7 +54,7 @@ fun AppWidgetConfigScreen( } @Composable -private fun PriceConfigContent( +private fun Content( state: AppWidgetConfigUiState, onTogglePair: (TradingPair) -> Unit, onSelectPeriod: (GraphPeriod) -> Unit, From 4bdcde72417a659927237ed248686a743d3b8f62 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 11:47:06 -0300 Subject: [PATCH 093/100] refactor: extract graph period label --- app/src/main/java/to/bitkit/ext/GraphPeriod.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ext/GraphPeriod.kt diff --git a/app/src/main/java/to/bitkit/ext/GraphPeriod.kt b/app/src/main/java/to/bitkit/ext/GraphPeriod.kt new file mode 100644 index 000000000..37fafe50a --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/GraphPeriod.kt @@ -0,0 +1,13 @@ +package to.bitkit.ext + +import androidx.annotation.StringRes +import to.bitkit.R +import to.bitkit.data.dto.price.GraphPeriod + +@StringRes +fun GraphPeriod.labelRes(): Int = when (this) { + GraphPeriod.ONE_DAY -> R.string.appwidget__price__day + GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week + GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month + GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year +} From d7bd73c8d2e643b30515d7c087d09c404b76274f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 11:47:40 -0300 Subject: [PATCH 094/100] refactor: extract new text style --- .../main/java/to/bitkit/ui/components/Text.kt | 19 +++++++++++++++++++ app/src/main/java/to/bitkit/ui/theme/Type.kt | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index ef8d3e0cf..94aaa9340 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -60,6 +60,25 @@ fun Display( ) } +@Composable +fun Display34( + text: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = if (maxLines == 1) TextOverflow.Ellipsis else TextOverflow.Clip, +) { + Text( + text = text, + style = AppTextStyles.Display34.merge( + color = color, + ), + maxLines = maxLines, + overflow = overflow, + modifier = modifier + ) +} + @Composable fun Headline( text: AnnotatedString, diff --git a/app/src/main/java/to/bitkit/ui/theme/Type.kt b/app/src/main/java/to/bitkit/ui/theme/Type.kt index 73cde025f..0c3c7e346 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Type.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Type.kt @@ -50,6 +50,13 @@ object AppTextStyles { letterSpacing = (-1).sp, fontFamily = InterFontFamily, ) + val Display34 = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 34.sp, + lineHeight = 34.sp, + letterSpacing = (-1).sp, + fontFamily = InterFontFamily, + ) val Headline = TextStyle( fontWeight = FontWeight.Black, fontSize = 30.sp, From a52f238f89de055d7d60cd923a9c75c329e552e5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 13:21:00 -0300 Subject: [PATCH 095/100] refactor: label method --- .../to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt | 1 - .../bitkit/ui/screens/widgets/price/PriceEditScreen.kt | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt index cc7b9f026..e37b669a6 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/theme/GlanceTextStyles.kt @@ -10,7 +10,6 @@ object GlanceTextStyles { val bodySSB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textPrimary) val bodySB = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val captionB = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) - val captionUp = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium, color = GlanceColors.textSecondary) val title22 = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val headline34 = TextStyle(fontSize = 34.sp, fontWeight = FontWeight.Bold, color = GlanceColors.textPrimary) val headlineChange22 = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Bold) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index 9d89e8506..b9986fa36 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +import to.bitkit.ext.labelRes import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up @@ -217,14 +218,7 @@ private fun SelectableRow( } @Composable -fun GraphPeriod.label(): String = stringResource( - when (this) { - GraphPeriod.ONE_DAY -> R.string.appwidget__price__day - GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week - GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month - GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year - }, -) +fun GraphPeriod.label(): String = stringResource(labelRes()) @Preview(showSystemUi = true) @Composable From d03ff2e05ad2906ff6441eb1450c4d37e6e8a0f7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 13:21:46 -0300 Subject: [PATCH 096/100] fix: carousel weight --- .../to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 9b6e76a0d..4884a750c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -34,6 +34,7 @@ import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer @@ -150,9 +151,10 @@ fun PricePreviewContent( priceDTO = priceDTO, modifier = Modifier .fillMaxWidth() + .weight(1f) ) } else { - Box(modifier = Modifier.weight(1f)) + FillHeight() } } From f038788e79ce976c9a5d1c39f05b552629250fd4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 13:22:06 -0300 Subject: [PATCH 097/100] chore: remove unused property --- app/src/main/java/to/bitkit/models/widget/PricePreferences.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt b/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt index 49d5fd013..28a436c64 100644 --- a/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt @@ -12,5 +12,4 @@ data class PricePreferences( TradingPair.BTC_USD ), val period: GraphPeriod? = GraphPeriod.ONE_DAY, - val showSource: Boolean = false ) From e6758a7f1ff0385c4bd1df5af86d5665367492df Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 13:22:51 -0300 Subject: [PATCH 098/100] refactor: remove unnecessary logic priceAvailable --- .../appwidget/ui/price/PriceGlanceContent.kt | 28 ++++++------------- .../appwidget/ui/price/PriceGlanceWidget.kt | 1 - 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index da85b3fb2..f91ebad96 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -3,7 +3,6 @@ package to.bitkit.appwidget.ui.price import android.appwidget.AppWidgetManager import android.content.Intent import android.graphics.Bitmap -import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier @@ -34,15 +33,14 @@ import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold import to.bitkit.appwidget.ui.components.HorizontalSpacer import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceTextStyles -import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.ui.theme.Colors +import java.util.Locale @Suppress("RestrictedApi") @Composable fun PriceGlanceContent( widget: PriceWidgetData?, - priceAvailable: Boolean, entry: AppWidgetEntry, chartBitmap: Bitmap? = null, ) { @@ -54,7 +52,7 @@ fun PriceGlanceContent( } GlanceWidgetScaffold(onClick = configIntent) { - if (!priceAvailable || widget == null) { + if (widget == null) { CaptionB(text = context.getString(R.string.appwidget__loading)) return@GlanceWidgetScaffold } @@ -71,15 +69,14 @@ fun PriceGlanceContent( @Composable private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red - val periodLabel = LocalContext.current.getString(widget.period.labelRes()) Row( verticalAlignment = Alignment.CenterVertically, modifier = GlanceModifier.fillMaxWidth() ) { Text( - text = "${widget.pair.displayName} $periodLabel".uppercase(), - style = GlanceTextStyles.captionUp, + text = "${widget.pair.displayName} ${widget.period.value}".uppercase(Locale.ENGLISH), + style = GlanceTextStyles.captionB, modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) ) HorizontalSpacer(16.dp) @@ -104,17 +101,16 @@ private fun WideContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { @Composable private fun CompactContent(widget: PriceWidgetData, chartBitmap: Bitmap?) { val changeColor = if (widget.change.isPositive) Colors.Green else Colors.Red - val periodLabel = LocalContext.current.getString(widget.period.labelRes()) Row(modifier = GlanceModifier.fillMaxWidth()) { Text( - text = widget.pair.displayName.uppercase(), - style = GlanceTextStyles.captionUp, + text = widget.pair.displayName.uppercase(Locale.ENGLISH), + style = GlanceTextStyles.captionB, modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) ) Text( - text = periodLabel.uppercase(), - style = GlanceTextStyles.captionUp, + text = widget.period.value.uppercase(Locale.ENGLISH), + style = GlanceTextStyles.captionB, ) } VerticalSpacer(8.dp) @@ -154,11 +150,3 @@ private fun ChartBox(chartBitmap: Bitmap?) { ) } } - -@StringRes -private fun GraphPeriod.labelRes(): Int = when (this) { - GraphPeriod.ONE_DAY -> R.string.appwidget__price__day - GraphPeriod.ONE_WEEK -> R.string.appwidget__price__week - GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month - GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year -} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt index 781ed6951..1d1dbb58a 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceWidget.kt @@ -52,7 +52,6 @@ class PriceGlanceWidget : GlanceAppWidget() { PriceGlanceContent( widget = widget, - priceAvailable = price != null, entry = entry, chartBitmap = chartBitmap, ) From b66d98e6cc805af9c899cf75600a9c065b71c70b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 13:22:58 -0300 Subject: [PATCH 099/100] refactor: remove dead code --- .../to/bitkit/services/MigrationService.kt | 4 -- .../ui/screens/widgets/price/PriceCard.kt | 39 +++++++------------ 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 568856ae1..0745b713d 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1123,14 +1123,10 @@ class MigrationService @Inject constructor( else -> GraphPeriod.ONE_DAY } - val showSource = priceJson["showSource"]?.jsonPrimitive?.content - ?.toBooleanStrictOrNull() ?: false - widgetsStore.updatePricePreferences( PricePreferences( enabledPairs = selectedPairs, period = period, - showSource = showSource ) ) }.onFailure { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index f248c2e90..49fa81e81 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ShapeDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -25,10 +24,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import ir.ehsannarmani.compose_charts.LineChart import ir.ehsannarmani.compose_charts.models.DividerProperties import ir.ehsannarmani.compose_charts.models.DrawStyle @@ -44,8 +41,11 @@ import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.Display34 import to.bitkit.ui.components.HorizontalSpacer +import to.bitkit.ui.components.Title import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -56,8 +56,7 @@ fun PriceCard( priceDTO: PriceDTO, ) { val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { - priceDTO.widgets.firstOrNull { it.pair in pricePreferences.enabledPairs } - ?: priceDTO.widgets.firstOrNull() + priceDTO.resolveWidget(pricePreferences) } ?: return Box( @@ -85,22 +84,16 @@ fun PriceCard( .testTag("PriceWidgetRow-${widgetData.pair.displayName}") ) HorizontalSpacer(16.dp) - Text( + Title( text = widgetData.change.formatted, color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, - fontSize = 22.sp, - lineHeight = 26.sp, - fontWeight = FontWeight.Bold, modifier = Modifier.testTag("price_card_pair_change_${widgetData.pair}") ) } - Text( + Display34( text = "${widgetData.pair.symbol} ${widgetData.price}", color = Colors.White, - fontSize = 34.sp, - lineHeight = 34.sp, - fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxWidth() .testTag("price_card_pair_price_${widgetData.pair}") @@ -124,8 +117,7 @@ fun PriceCardSmall( priceDTO: PriceDTO, ) { val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { - priceDTO.widgets.firstOrNull { it.pair in pricePreferences.enabledPairs } - ?: priceDTO.widgets.firstOrNull() + priceDTO.resolveWidget(pricePreferences) } ?: return Box( @@ -157,22 +149,16 @@ fun PriceCardSmall( color = Colors.White64, ) } - Text( + Title( text = "${widgetData.pair.symbol} ${widgetData.price}", color = Colors.White, - fontSize = 22.sp, - lineHeight = 26.sp, - fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxWidth() .testTag("price_card_small_pair_price_${widgetData.pair}") ) - Text( + BodySSB( text = widgetData.change.formatted, color = if (widgetData.change.isPositive) Colors.Green else Colors.Red, - fontSize = 15.sp, - lineHeight = 20.sp, - fontWeight = FontWeight.SemiBold, modifier = Modifier.testTag("price_card_small_pair_change_${widgetData.pair}") ) } @@ -188,6 +174,9 @@ fun PriceCardSmall( } } +private fun PriceDTO.resolveWidget(prefs: PricePreferences): PriceWidgetData? = + widgets.firstOrNull { it.pair in prefs.enabledPairs } ?: widgets.firstOrNull() + @Composable fun ChartComponent( widgetData: PriceWidgetData, @@ -264,9 +253,7 @@ private fun FullBlockCardPreview() { .padding(16.dp) ) { PriceCard( - pricePreferences = PricePreferences( - showSource = true, - ), + pricePreferences = PricePreferences(), priceDTO = PriceDTO( widgets = listOf( PriceWidgetData( From cfc0e3fdb35f216c5e1ea65eff82dfd8f5a96dab Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 1 May 2026 14:08:02 -0300 Subject: [PATCH 100/100] refactor: lint --- .../java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 2 +- app/src/main/java/to/bitkit/ext/GraphPeriod.kt | 5 +++++ .../java/to/bitkit/ui/screens/widgets/price/PriceCard.kt | 4 ++-- .../to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt | 5 +---- .../to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 9d56d3be6..d871fad19 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -24,6 +24,7 @@ import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +import to.bitkit.ext.label import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up @@ -32,7 +33,6 @@ import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.widgets.price.label import to.bitkit.ui.theme.Colors @Composable diff --git a/app/src/main/java/to/bitkit/ext/GraphPeriod.kt b/app/src/main/java/to/bitkit/ext/GraphPeriod.kt index 37fafe50a..d8e6d3817 100644 --- a/app/src/main/java/to/bitkit/ext/GraphPeriod.kt +++ b/app/src/main/java/to/bitkit/ext/GraphPeriod.kt @@ -1,6 +1,8 @@ package to.bitkit.ext import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import to.bitkit.R import to.bitkit.data.dto.price.GraphPeriod @@ -11,3 +13,6 @@ fun GraphPeriod.labelRes(): Int = when (this) { GraphPeriod.ONE_MONTH -> R.string.appwidget__price__month GraphPeriod.ONE_YEAR -> R.string.appwidget__price__year } + +@Composable +fun GraphPeriod.label(): String = stringResource(labelRes()) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt index 49fa81e81..8aeaba439 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceCard.kt @@ -51,9 +51,9 @@ import to.bitkit.ui.theme.Colors @Composable fun PriceCard( - modifier: Modifier = Modifier, pricePreferences: PricePreferences, priceDTO: PriceDTO, + modifier: Modifier = Modifier, ) { val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { priceDTO.resolveWidget(pricePreferences) @@ -112,9 +112,9 @@ fun PriceCard( @Composable fun PriceCardSmall( - modifier: Modifier = Modifier, pricePreferences: PricePreferences, priceDTO: PriceDTO, + modifier: Modifier = Modifier, ) { val widgetData = remember(pricePreferences.enabledPairs, priceDTO.widgets) { priceDTO.resolveWidget(pricePreferences) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index b9986fa36..fa52e9e21 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -31,7 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair -import to.bitkit.ext.labelRes +import to.bitkit.ext.label import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up @@ -217,9 +217,6 @@ private fun SelectableRow( } } -@Composable -fun GraphPeriod.label(): String = stringResource(labelRes()) - @Preview(showSystemUi = true) @Composable private fun Preview() { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 4884a750c..1d458dfd7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -196,9 +196,9 @@ fun PricePreviewContent( @Composable private fun WidgetCarousel( - modifier: Modifier = Modifier, pricePreferences: PricePreferences, priceDTO: PriceDTO, + modifier: Modifier = Modifier, ) { val pagerState = rememberPagerState(pageCount = { PAGE_COUNT })