From 9f86a9acb0ed6854da6e432029be139744b5d419 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 3 Apr 2026 14:41:49 -0300 Subject: [PATCH 01/55] 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 02/55] 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 03/55] 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 04/55] 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 05/55] 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 06/55] 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 07/55] 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 08/55] 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 09/55] 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 10/55] 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 11/55] 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 12/55] 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 13/55] 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 14/55] 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 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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 29/55] 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 30/55] 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 31/55] 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 32/55] 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 33/55] 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 34/55] 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 35/55] 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 36/55] 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 37/55] 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 38/55] 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 39/55] 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 40/55] 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 41/55] 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 42/55] 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 43/55] 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 44/55] 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 45/55] 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 46/55] 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 47/55] 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 48/55] 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 49/55] 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 50/55] 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 51/55] 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 52/55] 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 53/55] 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 54/55] 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 55/55] 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 {