Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
47c2ca8
feat: implement v61 wide and compact layouts for price OS and interna…
jvsena42 Apr 27, 2026
606be8d
chore: update comma rule
jvsena42 Apr 27, 2026
ebac006
feat: implement edit screen v61 and remove multi pairs support
jvsena42 Apr 27, 2026
01fad4f
feat: update the OS preview
jvsena42 Apr 27, 2026
b7b4a05
fix: chart vertical padding
jvsena42 Apr 27, 2026
8b1ff75
feat: preview screen v61
jvsena42 Apr 27, 2026
3acc939
feat: chart preview v61
jvsena42 Apr 27, 2026
ff6993c
fix: padding
jvsena42 Apr 27, 2026
7b784cf
Merge remote-tracking branch 'origin/feat/system-widgets-foundation' …
jvsena42 Apr 27, 2026
0a7085a
chore: lint
jvsena42 Apr 27, 2026
21f4053
chore: lint
jvsena42 Apr 27, 2026
abd9cb8
chore: lint
jvsena42 Apr 27, 2026
f061ec5
chore: lint
jvsena42 Apr 27, 2026
a95166a
chore: lint
jvsena42 Apr 27, 2026
d78cc7f
refactor: extract reusable dimen constant
jvsena42 Apr 28, 2026
d0e7f4f
refactor: extract reusable dimens from xml
jvsena42 Apr 28, 2026
947ff4b
chore: handle warnings
jvsena42 Apr 28, 2026
f8a2c51
fix: round OS widget corners for API level < 31
jvsena42 Apr 28, 2026
5920e79
fix: match min sizes for API < 31
jvsena42 Apr 28, 2026
ea94adf
fix: glance_default_loading_layout.xml round corners for API < 31
jvsena42 Apr 28, 2026
d193dbf
chore: lint
jvsena42 Apr 28, 2026
d50e73f
doc: changelog entry
jvsena42 Apr 28, 2026
d99859e
Merge branch 'feat/system-widgets-foundation' into feat/price-widget-v61
jvsena42 Apr 28, 2026
2db1167
refactor: reuse existent method
jvsena42 Apr 28, 2026
5de9f16
chore: lint
jvsena42 Apr 28, 2026
af526a8
fix: stops the curve from overshot above/below the canvas at sharp an…
jvsena42 Apr 28, 2026
bfd94d7
feat: remove widget title
jvsena42 Apr 28, 2026
ba45f42
feat: remove source field
jvsena42 Apr 28, 2026
9757b2b
feat: display mock image on preview
jvsena42 Apr 28, 2026
fd42d62
feat: update bg color
jvsena42 Apr 29, 2026
7f730ae
chore: lint
jvsena42 Apr 29, 2026
074d79c
fix: remove drawer button
jvsena42 Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS use `remember` for expensive Compose computations
- ALWAYS declare `modifier: Modifier = Modifier,` as the FIRST optional parameter in composable declarations
- ALWAYS pass `modifier = ...` as the LAST argument in composable calls
- ALWAYS add trailing commas in multi-line declarations; NEVER add a trailing comma to `modifier = ...` at call sites
- ALWAYS add trailing commas in multi-line declarations, EXCEPT after a `modifier = ...` last argument — never add a trailing comma there, whether the modifier is a single call (`modifier = Modifier.weight(1f)`) or a chain (`modifier = Modifier.fillMaxWidth().testTag("foo")`)
- ALWAYS use `navController.navigateTo(route)` for simple navigation; NEVER use raw `navController.navigate(route)` — `navigateTo` prevents duplicate destinations
- ALWAYS prefer `VerticalSpacer`, `HorizontalSpacer`, `FillHeight` and `FillWidth` over `Spacer` when applicable
- PREFER declaring small dependant classes, constants, interfaces or top-level functions in the same file with the core class where these are used
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Home screen widgets foundation with Glance, including price widget as the first implementation #895

### Changed
- Redesign price widget with v61 wide and compact layouts, new preview and edit screens, and tap-to-edit behavior #914

## [2.2.0] - 2026-04-07

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

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -10,7 +12,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
Expand All @@ -24,13 +25,14 @@ import to.bitkit.appwidget.model.AppWidgetType
import to.bitkit.data.dto.price.GraphPeriod
import to.bitkit.data.dto.price.TradingPair
import to.bitkit.models.widget.PricePreferences
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.BodySSB
import to.bitkit.ui.components.Caption13Up
import to.bitkit.ui.components.PrimaryButton
import to.bitkit.ui.components.SecondaryButton
import to.bitkit.ui.components.VerticalSpacer
import to.bitkit.ui.scaffold.AppTopBar
import to.bitkit.ui.scaffold.ScreenColumn
import to.bitkit.ui.screens.widgets.price.label
import to.bitkit.ui.theme.Colors

@Composable
Expand All @@ -44,7 +46,7 @@ fun AppWidgetConfigScreen(
when (state.type) {
AppWidgetType.PRICE -> PriceConfigContent(
state = state,
onTogglePair = { viewModel.togglePricePair(it) },
onSelectPair = { viewModel.selectPricePair(it) },
onSelectPeriod = { viewModel.selectPricePeriod(it) },
onReset = { viewModel.resetPreferences() },
onSave = { viewModel.saveAndFinish(onConfirm) },
Expand All @@ -56,16 +58,21 @@ fun AppWidgetConfigScreen(
@Composable
private fun PriceConfigContent(
state: AppWidgetConfigUiState,
onTogglePair: (TradingPair) -> Unit,
onSelectPair: (TradingPair) -> Unit,
onSelectPeriod: (GraphPeriod) -> Unit,
onReset: () -> Unit,
onSave: () -> Unit,
onCancel: () -> Unit,
) {
val prefs = state.pricePreferences
ScreenColumn {
val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD

ScreenColumn(
noBackground = true,
modifier = Modifier.background(Colors.Gray7)
) {
AppTopBar(
titleText = stringResource(R.string.widgets__widget__edit),
titleText = stringResource(R.string.widgets__price__name),
onBackClick = onCancel,
)

Expand All @@ -75,43 +82,33 @@ private fun PriceConfigContent(
.weight(1f)
.verticalScroll(rememberScrollState())
) {
VerticalSpacer(26.dp)

BodyM(
text = stringResource(R.string.widgets__widget__edit_description).replace(
"{name}",
stringResource(R.string.widgets__price__name),
),
color = Colors.White64,
)

VerticalSpacer(32.dp)
VerticalSpacer(16.dp)

BodySSB(
text = stringResource(R.string.appwidget__price__trading_pairs),
Caption13Up(
text = stringResource(R.string.appwidget__price__currency),
color = Colors.White64,
modifier = Modifier.padding(bottom = 16.dp)
)
VerticalSpacer(8.dp)

for (pair in TradingPair.entries) {
ConfigToggleRow(
SelectableRow(
label = pair.displayName,
isEnabled = pair in prefs.enabledPairs,
onClick = { onTogglePair(pair) },
isSelected = pair == selectedPair,
onClick = { onSelectPair(pair) },
)
}

VerticalSpacer(16.dp)
BodySSB(
text = stringResource(R.string.appwidget__price__period),
Caption13Up(
text = stringResource(R.string.appwidget__price__timeframe),
color = Colors.White64,
modifier = Modifier.padding(vertical = 16.dp)
)
VerticalSpacer(8.dp)

for (period in GraphPeriod.entries) {
ConfigToggleRow(
label = period.value,
isEnabled = period == prefs.period,
SelectableRow(
label = period.label(),
isSelected = period == prefs.period,
onClick = { onSelectPeriod(period) },
)
}
Expand All @@ -120,7 +117,7 @@ private fun PriceConfigContent(
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(vertical = 21.dp, horizontal = 16.dp)
.padding(16.dp)
.fillMaxWidth()
) {
SecondaryButton(
Expand All @@ -143,29 +140,30 @@ private fun PriceConfigContent(
}

@Composable
private fun ConfigToggleRow(
private fun SelectableRow(
label: String,
isEnabled: Boolean,
isSelected: Boolean,
onClick: () -> Unit,
) {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = 12.dp)
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 14.dp)
) {
BodySSB(
text = label,
color = Colors.White64,
color = if (isSelected) Colors.White else Colors.White64,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onClick) {
if (isSelected) {
Icon(
painter = painterResource(R.drawable.ic_checkmark),
contentDescription = null,
tint = if (isEnabled) Colors.Brand else Colors.White50,
tint = Colors.Brand,
modifier = Modifier.size(32.dp)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -46,15 +47,9 @@ class AppWidgetConfigViewModel @Inject constructor(
}
}

fun togglePricePair(pair: TradingPair) {
fun selectPricePair(pair: TradingPair) {
_uiState.update {
val current = it.pricePreferences.enabledPairs.toMutableList()
if (pair in current) {
if (current.size > 1) current.remove(pair)
} else {
current.add(pair)
}
it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = current.sortedBy { p -> p.position }))
it.copy(pricePreferences = it.pricePreferences.copy(enabledPairs = persistentListOf(pair)))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package to.bitkit.appwidget.ui.components

import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp

object GlanceLayoutDimens {
val WIDE_LAYOUT_MIN_WIDTH = 280.dp

val COMPACT_WIDGET_SIZE = DpSize(163.dp, 192.dp)
val WIDE_WIDGET_SIZE = DpSize(343.dp, 152.dp)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.ImageProvider
import androidx.glance.action.clickable
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import to.bitkit.appwidget.ui.theme.GlanceColors
import to.bitkit.R

@Composable
fun GlanceWidgetScaffold(
Expand All @@ -20,8 +20,7 @@ fun GlanceWidgetScaffold(
) {
val modifier = GlanceModifier
.fillMaxSize()
.cornerRadius(16.dp)
.background(GlanceColors.cardBackgroundProvider)
.background(ImageProvider(R.drawable.appwidget_background))
.padding(16.dp)
.let { mod ->
if (onClick != null) mod.clickable(actionStartActivity(onClick)) else mod
Expand Down
32 changes: 8 additions & 24 deletions app/src/main/java/to/bitkit/appwidget/ui/price/LineChartBitmap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package to.bitkit.appwidget.ui.price

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Shader
import androidx.annotation.ColorInt
import androidx.core.graphics.createBitmap

Expand Down Expand Up @@ -36,7 +34,7 @@ fun renderLineChartBitmap(
x to y
}

val linePath = buildSmoothPath(points)
val linePath = buildSmoothPath(points, yMin = padding, yMax = padding + drawHeight)

val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = lineColor
Expand All @@ -47,28 +45,14 @@ fun renderLineChartBitmap(
}
canvas.drawPath(linePath, linePaint)

val fillPath = Path(linePath).apply {
lineTo(points.last().first, height.toFloat())
lineTo(points.first().first, height.toFloat())
close()
}

val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
shader = LinearGradient(
0f, padding,
0f, height.toFloat(),
(lineColor and 0x00FFFFFF) or 0xCC000000.toInt(),
(lineColor and 0x00FFFFFF) or 0x4D000000,
Shader.TileMode.CLAMP,
)
style = Paint.Style.FILL
}
canvas.drawPath(fillPath, fillPaint)

return bitmap
}

private fun buildSmoothPath(points: List<Pair<Float, Float>>): Path = Path().apply {
private fun buildSmoothPath(
points: List<Pair<Float, Float>>,
yMin: Float,
yMax: Float,
): Path = Path().apply {
moveTo(points[0].first, points[0].second)
for (i in 0 until points.size - 1) {
val p0 = points[(i - 1).coerceAtLeast(0)]
Expand All @@ -77,9 +61,9 @@ private fun buildSmoothPath(points: List<Pair<Float, Float>>): Path = Path().app
val p3 = points[(i + 2).coerceAtMost(points.lastIndex)]

val cp1x = p1.first + (p2.first - p0.first) * SMOOTHING
val cp1y = p1.second + (p2.second - p0.second) * SMOOTHING
val cp1y = (p1.second + (p2.second - p0.second) * SMOOTHING).coerceIn(yMin, yMax)
val cp2x = p2.first - (p3.first - p1.first) * SMOOTHING
val cp2y = p2.second - (p3.second - p1.second) * SMOOTHING
val cp2y = (p2.second - (p3.second - p1.second) * SMOOTHING).coerceIn(yMin, yMax)

cubicTo(cp1x, cp1y, cp2x, cp2y, p2.first, p2.second)
}
Expand Down
Loading
Loading