Skip to content

Commit fb689db

Browse files
Keril3claude
andcommitted
Old Tenshi-chan back, typed target range, calmer screens, no flash on language change
- Tenshi-chan is the angel again (sky-blue hair, halo, feathered wings) with the red ribbon kept from the fairy; the icons follow. A tap tilts her head, closes her eyes in ^ ^ and makes her smile, with two little hearts. - The target range is typed into two fields in the chosen unit and saved with a button that only works for a range that makes sense (40–400 mg/dL, the lower bound first, at least 0.5 mmol/L apart). The saved range sits on its own line under "Target range:". - Removed: the address bar at the top, the window buttons and rolling up, and the moving bubbles, stars and hearts in the background. Dialogs got a plain "Close" button instead of the window's "×". - The Now tab shows a house. - Changing the language restarted the screen, which flashed black. The main screen now handles the locale change itself and redraws in place; a device test checks that the same screen stays and shows the new language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 571a025 commit fb689db

53 files changed

Lines changed: 777 additions & 920 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Newest version first. A version's section becomes the release notes on GitHub.
44

5+
## 1.5.0 — 2026-10-11
6+
7+
- Tenshi-chan is the angel again: sky-blue hair, a halo and feathered wings, now with a red ribbon. Tap her and
8+
she tilts her head, closes her eyes and smiles. The app icon is back too.
9+
- The target range is typed in: two fields in the chosen unit and a "Save" button that only works for a range
10+
that makes sense. The saved range is shown on its own line.
11+
- Calmer screens: no more address bar at the top (the theme and the language are in Settings), no window
12+
buttons, and the background no longer moves: no bubbles, stars or hearts, only the still field of dots.
13+
- The "Now" tab has a house icon.
14+
- Fixed: changing the language made the whole screen flash black. The screen now stays and its texts switch
15+
to the new language in place.
16+
517
## 1.4.0 — 2026-10-11
618

719
- A new look after keril.art: a blue sky with dots, bubbles, stars and hearts, Windows XP style windows

‎README.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ from the LibreLinkUp cloud and writes them to Health Connect, where Samsung Heal
2828
- **Estimated HbA1c (GMI)** for 14 and 90 days, from all the history SugarBridge has written to Health Connect.
2929
- **Its own chart and statistics** for 12 hours, mmol/L or mg/dL, your own target range. Tap the chart to open it
3030
full screen and move a finger along it to read every value and its time.
31-
- **Light and dark theme** in the style of [keril.art](https://keril.art/): a blue sky with bubbles and stars,
32-
Windows XP style windows and a pixel font. The phone's theme by default; the sun/moon button switches it.
31+
- **Light and dark theme** in the style of [keril.art](https://keril.art/): a blue sky with a field of dots,
32+
Windows XP style windows and a pixel font. The phone's theme by default, or light or dark in Settings.
3333
- **12 languages**: English, Русский, Українська, Deutsch, Français, Español, Português, Italiano, Türkçe,
3434
简体中文, 日本語, 한국어. Chosen on the first start, changeable in Settings or in Android's per-app language settings.
3535
- **The password is protected by the Android Keystore** and stays on the phone.
3636
- **Updates in the app**: SugarBridge finds a new version on GitHub, downloads it and hands it to Android to install.
37-
- **Tenshi-chan**, an ice-fairy chibi mascot who is happy, worried, flustered or sleepy depending on your glucose.
37+
- **Tenshi-chan**, a chibi angel mascot who is happy, worried, flustered or sleepy depending on your glucose.
38+
Tap her and she tilts her head, closes her eyes and smiles.
3839

3940
Data path: sensor → LibreLink on the phone → LibreLinkUp cloud → SugarBridge → Health Connect → Samsung Health.
4041

@@ -198,14 +199,14 @@ GitHub Actions builds and signs the APK in Releases when a `v*` tag arrives in t
198199
```
199200
3. **Every release:** add the version's section to [CHANGELOG.md](CHANGELOG.md), then
200201
```sh
201-
git tag v1.4.0
202-
git push origin v1.4.0
202+
git tag v1.5.0
203+
git push origin v1.5.0
203204
```
204205
A few minutes later `SugarBridge.apk` appears in Releases with the notes from the CHANGELOG.
205206

206207
A signed APK can be built locally too: put `sugarbridge.keystore`, `sugarbridge.keystorePassword`,
207208
`sugarbridge.keyAlias` and `sugarbridge.keyPassword` into `~/.gradle/gradle.properties` (outside the repository)
208-
and run `./gradlew assembleRelease -Psugarbridge.version=1.4.0`.
209+
and run `./gradlew assembleRelease -Psugarbridge.version=1.5.0`.
209210

210211
## Thanks
211212

‎README.ru.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@
2828
- **Расчётный HbA1c (GMI)** за 14 и 90 дней — по всей истории, которую SugarBridge записал в Health Connect.
2929
- **Свой график и статистика** за 12 часов, ммоль/л или мг/дл, свой целевой диапазон. Нажмите на график —
3030
он откроется на весь экран, а пальцем по нему видно каждое значение и его время.
31-
- **Светлая и тёмная тема** в стиле [keril.art](https://keril.art/): голубое небо с пузырьками и звёздами,
32-
окна в духе Windows XP и пиксельный шрифт. По умолчанию — как на телефоне, кнопка с солнцем/луной переключает.
31+
- **Светлая и тёмная тема** в стиле [keril.art](https://keril.art/): голубое небо в точку, окна в духе
32+
Windows XP и пиксельный шрифт. По умолчанию — как на телефоне, в настройках можно выбрать светлую или тёмную.
3333
- **12 языков**: English, Русский, Українська, Deutsch, Français, Español, Português, Italiano, Türkçe,
3434
简体中文, 日本語, 한국어. Язык выбирается при первом запуске, поменять его можно в настройках приложения
3535
или в системных настройках языка приложений.
3636
- **Пароль под защитой Android Keystore**, хранится только на телефоне.
3737
- **Обновления прямо в приложении**: SugarBridge находит новую версию на GitHub, скачивает её и передаёт Android на установку.
38-
- **Тэнси-тян** — ледяная фея в стиле чиби: радуется, волнуется, смущается или спит в зависимости от сахара.
38+
- **Тэнси-тян** — ангелочек в стиле чиби: радуется, волнуется, смущается или спит в зависимости от сахара.
39+
Если на неё нажать, она наклонит голову, зажмурится и улыбнётся.
3940

4041
Путь данных: датчик → LibreLink на телефоне → облако LibreLinkUp → SugarBridge → Health Connect →
4142
Samsung Health.
@@ -204,14 +205,14 @@ APK в Releases собирает и подписывает GitHub Actions, ко
204205
```
205206
3. **Каждый выпуск:** допишите раздел версии в [CHANGELOG.md](CHANGELOG.md) (на английском), затем
206207
```sh
207-
git tag v1.4.0
208-
git push origin v1.4.0
208+
git tag v1.5.0
209+
git push origin v1.5.0
209210
```
210211
Через несколько минут в Releases появится `SugarBridge.apk` с описанием из CHANGELOG.
211212

212213
Подписанный APK можно собрать и у себя: пропишите в `~/.gradle/gradle.properties` (вне репозитория)
213214
`sugarbridge.keystore`, `sugarbridge.keystorePassword`, `sugarbridge.keyAlias`, `sugarbridge.keyPassword`
214-
и запустите `./gradlew assembleRelease -Psugarbridge.version=1.4.0`.
215+
и запустите `./gradlew assembleRelease -Psugarbridge.version=1.5.0`.
215216

216217
## Благодарности
217218

‎app/build.gradle.kts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ plugins {
33
alias(libs.plugins.kotlin.compose)
44
}
55

6-
// The version: a release takes it from the git tag (./gradlew -Psugarbridge.version=1.4.0), otherwise this one.
6+
// The version: a release takes it from the git tag (./gradlew -Psugarbridge.version=1.5.0), otherwise this one.
77
// versionCode grows with it: 1.2.3 → 10203.
8-
val appVersion = providers.gradleProperty("sugarbridge.version").getOrElse("1.4.0")
8+
val appVersion = providers.gradleProperty("sugarbridge.version").getOrElse("1.5.0")
99
val appVersionCode = (appVersion.substringBefore('-').split('.').map { it.toIntOrNull() ?: 0 } + listOf(0, 0, 0))
1010
.take(3)
1111
.let { (major, minor, patch) -> major * 10_000 + minor * 100 + patch }
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.sugarbridge.app.ui
2+
3+
import androidx.compose.ui.test.junit4.createEmptyComposeRule
4+
import androidx.compose.ui.test.onNodeWithText
5+
import androidx.test.core.app.ActivityScenario
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
7+
import androidx.test.platform.app.InstrumentationRegistry
8+
import com.sugarbridge.app.MainActivity
9+
import com.sugarbridge.app.R
10+
import com.sugarbridge.app.libre.Libre
11+
import com.sugarbridge.app.resourcesIn
12+
import com.sugarbridge.app.settings.SettingsStore
13+
import com.sugarbridge.app.useAppLanguage
14+
import org.junit.After
15+
import org.junit.Assert.assertSame
16+
import org.junit.Rule
17+
import org.junit.Test
18+
import org.junit.runner.RunWith
19+
20+
/**
21+
* Switching the language must not restart the screen: a restart is what made the whole screen flash black.
22+
* The same Activity stays and only redraws its texts in the new language.
23+
*/
24+
@RunWith(AndroidJUnit4::class)
25+
class LanguageSwitchTest {
26+
27+
@get:Rule
28+
val compose = createEmptyComposeRule()
29+
30+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
31+
32+
@After
33+
fun cleanUp() = useAppLanguage(context, null)
34+
35+
@Test
36+
fun languageChangesWithoutRestartingTheScreen() {
37+
Libre.signOut(context)
38+
SettingsStore(context).languageChosen = true // the sign-in screen
39+
useAppLanguage(context, "en")
40+
val scenario = ActivityScenario.launch(MainActivity::class.java)
41+
compose.onNodeWithText(resourcesIn(context, "en").getString(R.string.login_title)).assertExists()
42+
lateinit var before: MainActivity
43+
scenario.onActivity { before = it }
44+
45+
useAppLanguage(context, "de")
46+
compose.waitUntil(5_000) {
47+
runCatching { compose.onNodeWithText(resourcesIn(context, "de").getString(R.string.login_title)).assertExists() }.isSuccess
48+
}
49+
scenario.onActivity { assertSame("the screen was restarted", before, it) }
50+
scenario.close()
51+
}
52+
}

‎app/src/androidTest/java/com/sugarbridge/app/ui/ScreenshotTest.kt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import android.graphics.Bitmap
44
import androidx.compose.ui.test.junit4.createEmptyComposeRule
55
import androidx.compose.ui.test.onNodeWithText
66
import androidx.compose.ui.geometry.Offset
7+
import androidx.compose.ui.test.hasSetTextAction
8+
import androidx.compose.ui.test.hasText
79
import androidx.compose.ui.test.onNodeWithTag
10+
import androidx.compose.ui.test.performTextReplacement
811
import androidx.compose.ui.test.performClick
912
import androidx.compose.ui.test.performTouchInput
1013
import androidx.test.espresso.Espresso
@@ -16,7 +19,10 @@ import androidx.test.platform.app.InstrumentationRegistry
1619
import com.sugarbridge.app.MainActivity
1720
import com.sugarbridge.app.MainViewModel
1821
import com.sugarbridge.app.R
22+
import com.sugarbridge.app.glucose.GlucoseUnit
23+
import com.sugarbridge.app.glucose.MGDL_PER_MMOL
1924
import com.sugarbridge.app.glucose.Reading
25+
import com.sugarbridge.app.glucose.formatGlucose
2026
import com.sugarbridge.app.health.HealthConnectState
2127
import com.sugarbridge.app.health.WRITE_GLUCOSE
2228
import com.sugarbridge.app.health.deleteOwnGlucose
@@ -44,6 +50,7 @@ import java.time.LocalDate
4450
import java.time.ZoneId
4551
import java.time.format.DateTimeFormatter
4652
import java.util.Locale
53+
import kotlin.math.abs
4754
import kotlin.math.exp
4855
import kotlin.math.sin
4956
import kotlinx.coroutines.runBlocking
@@ -149,6 +156,8 @@ class ScreenshotTest {
149156
compose.waitUntil(10_000) { vm.ui.signedIn && vm.ui.canWrite != null }
150157
compose.onNodeWithText(text(R.string.tab_now)).assertExists() // the screen really is in this language
151158
shot(language, "home")
159+
compose.onNodeWithTag("mascot").performClick() // she tilts her head, closes her eyes and smiles
160+
shot(language, "home_tap")
152161
compose.waitUntil(20_000) { vm.ui.gmi14 != null }
153162
compose.onNodeWithText(res.getString(R.string.gmi_hint, 70)).performScrollTo()
154163
shot(language, "home_gmi")
@@ -176,6 +185,13 @@ class ScreenshotTest {
176185
compose.onNodeWithText(text(R.string.settings_about_title)).performScrollTo()
177186
shot(language, "settings_bottom")
178187

188+
// the target range is typed in: a new lower bound with the decimal separator of the language, then Save
189+
val locale = Locale.forLanguageTag(language)
190+
compose.onNode(hasSetTextAction() and hasText(formatGlucose(70.0, GlucoseUnit.MMOL, locale))).performTextReplacement(String.format(locale, "%.1f", 4.2))
191+
compose.onNodeWithText(text(R.string.target_save)).performClick()
192+
compose.waitUntil(5_000) { abs(vm.ui.settings.lowMgDl - 4.2 * MGDL_PER_MMOL) < 0.1 }
193+
scenario.onActivity { vm.updateSettings { it.copy(lowMgDl = 70.0) } }
194+
179195
compose.onNodeWithText(text(R.string.tab_now)).performClick()
180196
scenario.onActivity { vm.updateSettings { it.copy(theme = ThemeMode.DARK) } } // the sun/moon switch
181197
shot(language, "home_night")

‎app/src/main/AndroidManifest.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
android:roundIcon="@mipmap/ic_launcher_round"
2828
android:supportsRtl="true"
2929
android:theme="@style/Theme.SugarBridge">
30+
<!-- a language change is handled in place: no restart, so the screen doesn't flash black -->
3031
<activity
3132
android:name=".MainActivity"
33+
android:configChanges="locale|layoutDirection"
3234
android:exported="true"
3335
android:launchMode="singleTop"
3436
android:theme="@style/Theme.SugarBridge"

‎app/src/main/java/com/sugarbridge/app/MainActivity.kt‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.content.ActivityNotFoundException
66
import android.content.Intent
77
import android.content.pm.PackageInstaller
88
import android.content.pm.PackageManager
9+
import android.content.res.Configuration
910
import android.graphics.Color
1011
import android.net.Uri
1112
import android.os.Bundle
@@ -64,7 +65,7 @@ class MainActivity : ComponentActivity(), AppActions {
6465
override fun onCreate(savedInstanceState: Bundle?) {
6566
super.onCreate(savedInstanceState)
6667
enableEdgeToEdge()
67-
createNotificationChannels(this) // also renames the channels after a language change
68+
createNotificationChannels(this)
6869
scheduleSync(this) // safe to call on every start
6970
if (savedInstanceState == null) handleIntent(intent)
7071
setContent {
@@ -80,6 +81,15 @@ class MainActivity : ComponentActivity(), AppActions {
8081
}
8182
}
8283

84+
/**
85+
* The app language changed. The screen stays (see configChanges in the manifest) and Compose redraws
86+
* every text in the new language; only the notification channels need their new names.
87+
*/
88+
override fun onConfigurationChanged(newConfig: Configuration) {
89+
super.onConfigurationChanged(newConfig)
90+
createNotificationChannels(this)
91+
}
92+
8393
override fun onNewIntent(intent: Intent) {
8494
super.onNewIntent(intent)
8595
handleIntent(intent)
@@ -121,7 +131,7 @@ class MainActivity : ComponentActivity(), AppActions {
121131
}
122132
}
123133

124-
/** null follows the phone. If the language really changes, Android recreates this screen in it. */
134+
/** null follows the phone. The screen redraws in the new language in place (see onConfigurationChanged). */
125135
override fun setLanguage(tag: String?) {
126136
vm.languageChosen()
127137
setAppLanguage(this, tag)

‎app/src/main/java/com/sugarbridge/app/MainViewModel.kt‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import com.sugarbridge.app.libreview.readingsOnDays
3535
import com.sugarbridge.app.libreview.summarizeDays
3636
import com.sugarbridge.app.settings.AppSettings
3737
import com.sugarbridge.app.settings.SettingsStore
38-
import com.sugarbridge.app.settings.ThemeMode
3938
import com.sugarbridge.app.sync.LiveSyncService
4039
import com.sugarbridge.app.sync.Problem
4140
import com.sugarbridge.app.sync.SyncBlocked
@@ -254,9 +253,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
254253
/** What Android's installer answered, for the Activity. */
255254
fun updateFailed(message: String) = say(R.string.update_failed, message)
256255

257-
/** The sun/moon button: switch to the opposite of what is on screen now. */
258-
fun toggleTheme(nightNow: Boolean) = updateSettings { it.copy(theme = if (nightNow) ThemeMode.LIGHT else ThemeMode.DARK) }
259-
260256
/**
261257
* Estimated HbA1c (GMI) for 14 and 90 days from all SugarBridge readings in Health Connect, in one pass
262258
* over 90 days of records. Recalculated at most every 10 minutes: it barely changes in that time.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.sugarbridge.app.glucose
2+
3+
// What a target range may be: Libre measures 40–500 mg/dL, and a range narrower than 0.5 mmol/L makes no sense
4+
const val TARGET_MIN_MGDL = 40.0
5+
const val TARGET_MAX_MGDL = 400.0
6+
const val TARGET_MIN_WIDTH_MGDL = 9.0
7+
8+
/** A typed glucose value ("3,9", "3.9", " 70 ") in the given unit, as mg/dL; null if it isn't a number. */
9+
fun parseGlucose(text: String, unit: GlucoseUnit): Double? {
10+
val number = text.trim().replace(',', '.').toDoubleOrNull() ?: return null
11+
if (number.isNaN() || number.isInfinite()) return null
12+
return when (unit) {
13+
GlucoseUnit.MMOL -> number * MGDL_PER_MMOL
14+
GlucoseUnit.MGDL -> number
15+
}
16+
}
17+
18+
/** true when both bounds are set, the lower one comes first and both lie within what makes sense. */
19+
fun isValidTarget(lowMgDl: Double?, highMgDl: Double?): Boolean =
20+
lowMgDl != null && highMgDl != null &&
21+
lowMgDl >= TARGET_MIN_MGDL - 0.5 && highMgDl <= TARGET_MAX_MGDL + 0.5 &&
22+
highMgDl - lowMgDl >= TARGET_MIN_WIDTH_MGDL - 0.5

0 commit comments

Comments
 (0)