From a7431d5358073ae1b5923b2bec5eee0eae6fdd8b Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 10:38:19 +0300 Subject: [PATCH 1/7] added icons --- .../fedo/modelpulse/ui/icons/IconAiFilled.kt | 51 ++++++++ .../modelpulse/ui/icons/IconAiOutlined.kt | 64 ++++++++++ .../modelpulse/ui/icons/IconSettingsFilled.kt | 73 ++++++++++++ .../ui/icons/IconSettingsOutlined.kt | 111 ++++++++++++++++++ .../modelpulse/ui/icons/IconTimelineFilled.kt | 78 ++++++++++++ 5 files changed, 377 insertions(+) create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiFilled.kt create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiOutlined.kt create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsFilled.kt create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsOutlined.kt create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/icons/IconTimelineFilled.kt diff --git a/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiFilled.kt b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiFilled.kt new file mode 100644 index 0000000..b087293 --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiFilled.kt @@ -0,0 +1,51 @@ +package com.fedo.modelpulse.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconAiFilled: ImageVector + get() { + if (_AiFilled != null) { + return _AiFilled!! + } + _AiFilled = ImageVector.Builder( + name = "AiFilled", + defaultWidth = 100.dp, + defaultHeight = 100.dp, + viewportWidth = 100f, + viewportHeight = 100f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(35f, 15f) + curveTo(35f, 30f, 40f, 45f, 70f, 50f) + curveTo(40f, 55f, 35f, 70f, 35f, 85f) + curveTo(35f, 70f, 30f, 55f, 0f, 50f) + curveTo(30f, 45f, 35f, 30f, 35f, 15f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveTo(75f, 10f) + curveTo(75f, 17f, 77.5f, 22.5f, 90f, 25f) + curveTo(77.5f, 27.5f, 75f, 33f, 75f, 40f) + curveTo(75f, 33f, 72.5f, 27.5f, 60f, 25f) + curveTo(72.5f, 22.5f, 75f, 17f, 75f, 10f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveTo(75f, 60f) + curveTo(75f, 67f, 77.5f, 72.5f, 90f, 75f) + curveTo(77.5f, 77.5f, 75f, 83f, 75f, 90f) + curveTo(75f, 83f, 72.5f, 77.5f, 60f, 75f) + curveTo(72.5f, 72.5f, 75f, 67f, 75f, 60f) + close() + } + }.build() + + return _AiFilled!! + } + +@Suppress("ObjectPropertyName") +private var _AiFilled: ImageVector? = null diff --git a/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiOutlined.kt b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiOutlined.kt new file mode 100644 index 0000000..8323182 --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconAiOutlined.kt @@ -0,0 +1,64 @@ +package com.fedo.modelpulse.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconAiOutlined: ImageVector + get() { + if (_AiOutlined != null) { + return _AiOutlined!! + } + _AiOutlined = ImageVector.Builder( + name = "AiOutlined", + defaultWidth = 100.dp, + defaultHeight = 100.dp, + viewportWidth = 100f, + viewportHeight = 100f + ).apply { + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(35f, 15f) + curveTo(35f, 30f, 40f, 45f, 70f, 50f) + curveTo(40f, 55f, 35f, 70f, 35f, 85f) + curveTo(35f, 70f, 30f, 55f, 0f, 50f) + curveTo(30f, 45f, 35f, 30f, 35f, 15f) + close() + } + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(75f, 10f) + curveTo(75f, 17f, 77.5f, 22.5f, 90f, 25f) + curveTo(77.5f, 27.5f, 75f, 33f, 75f, 40f) + curveTo(75f, 33f, 72.5f, 27.5f, 60f, 25f) + curveTo(72.5f, 22.5f, 75f, 17f, 75f, 10f) + close() + } + path( + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(75f, 60f) + curveTo(75f, 67f, 77.5f, 72.5f, 90f, 75f) + curveTo(77.5f, 77.5f, 75f, 83f, 75f, 90f) + curveTo(75f, 83f, 72.5f, 77.5f, 60f, 75f) + curveTo(72.5f, 72.5f, 75f, 67f, 75f, 60f) + close() + } + }.build() + + return _AiOutlined!! + } + +@Suppress("ObjectPropertyName") +private var _AiOutlined: ImageVector? = null diff --git a/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsFilled.kt b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsFilled.kt new file mode 100644 index 0000000..18340ac --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsFilled.kt @@ -0,0 +1,73 @@ +package com.fedo.modelpulse.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconSettingsFilled: ImageVector + get() { + if (_Settings != null) { + return _Settings!! + } + _Settings = ImageVector.Builder( + name = "Settings", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveToRelative(370f, 880f) + lineToRelative(-16f, -128f) + quadToRelative(-13f, -5f, -24.5f, -12f) + reflectiveQuadTo(307f, 725f) + lineToRelative(-119f, 50f) + lineTo(78f, 585f) + lineToRelative(103f, -78f) + quadToRelative(-1f, -7f, -1f, -13.5f) + verticalLineToRelative(-27f) + quadToRelative(0f, -6.5f, 1f, -13.5f) + lineTo(78f, 375f) + lineToRelative(110f, -190f) + lineToRelative(119f, 50f) + quadToRelative(11f, -8f, 23f, -15f) + reflectiveQuadToRelative(24f, -12f) + lineToRelative(16f, -128f) + horizontalLineToRelative(220f) + lineToRelative(16f, 128f) + quadToRelative(13f, 5f, 24.5f, 12f) + reflectiveQuadToRelative(22.5f, 15f) + lineToRelative(119f, -50f) + lineToRelative(110f, 190f) + lineToRelative(-103f, 78f) + quadToRelative(1f, 7f, 1f, 13.5f) + verticalLineToRelative(27f) + quadToRelative(0f, 6.5f, -2f, 13.5f) + lineToRelative(103f, 78f) + lineToRelative(-110f, 190f) + lineToRelative(-118f, -50f) + quadToRelative(-11f, 8f, -23f, 15f) + reflectiveQuadToRelative(-24f, 12f) + lineTo(590f, 880f) + lineTo(370f, 880f) + close() + moveTo(482f, 620f) + quadToRelative(58f, 0f, 99f, -41f) + reflectiveQuadToRelative(41f, -99f) + quadToRelative(0f, -58f, -41f, -99f) + reflectiveQuadToRelative(-99f, -41f) + quadToRelative(-59f, 0f, -99.5f, 41f) + reflectiveQuadTo(342f, 480f) + quadToRelative(0f, 58f, 40.5f, 99f) + reflectiveQuadToRelative(99.5f, 41f) + close() + } + }.build() + + return _Settings!! + } + +@Suppress("ObjectPropertyName") +private var _Settings: ImageVector? = null diff --git a/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsOutlined.kt b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsOutlined.kt new file mode 100644 index 0000000..707b57d --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconSettingsOutlined.kt @@ -0,0 +1,111 @@ +package com.fedo.modelpulse.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconSettingsOutlined: ImageVector + get() { + if (_Settings != null) { + return _Settings!! + } + _Settings = ImageVector.Builder( + name = "Settings", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveToRelative(370f, 880f) + lineToRelative(-16f, -128f) + quadToRelative(-13f, -5f, -24.5f, -12f) + reflectiveQuadTo(307f, 725f) + lineToRelative(-119f, 50f) + lineTo(78f, 585f) + lineToRelative(103f, -78f) + quadToRelative(-1f, -7f, -1f, -13.5f) + verticalLineToRelative(-27f) + quadToRelative(0f, -6.5f, 1f, -13.5f) + lineTo(78f, 375f) + lineToRelative(110f, -190f) + lineToRelative(119f, 50f) + quadToRelative(11f, -8f, 23f, -15f) + reflectiveQuadToRelative(24f, -12f) + lineToRelative(16f, -128f) + horizontalLineToRelative(220f) + lineToRelative(16f, 128f) + quadToRelative(13f, 5f, 24.5f, 12f) + reflectiveQuadToRelative(22.5f, 15f) + lineToRelative(119f, -50f) + lineToRelative(110f, 190f) + lineToRelative(-103f, 78f) + quadToRelative(1f, 7f, 1f, 13.5f) + verticalLineToRelative(27f) + quadToRelative(0f, 6.5f, -2f, 13.5f) + lineToRelative(103f, 78f) + lineToRelative(-110f, 190f) + lineToRelative(-118f, -50f) + quadToRelative(-11f, 8f, -23f, 15f) + reflectiveQuadToRelative(-24f, 12f) + lineTo(590f, 880f) + lineTo(370f, 880f) + close() + moveTo(440f, 800f) + horizontalLineToRelative(79f) + lineToRelative(14f, -106f) + quadToRelative(31f, -8f, 57.5f, -23.5f) + reflectiveQuadTo(639f, 633f) + lineToRelative(99f, 41f) + lineToRelative(39f, -68f) + lineToRelative(-86f, -65f) + quadToRelative(5f, -14f, 7f, -29.5f) + reflectiveQuadToRelative(2f, -31.5f) + quadToRelative(0f, -16f, -2f, -31.5f) + reflectiveQuadToRelative(-7f, -29.5f) + lineToRelative(86f, -65f) + lineToRelative(-39f, -68f) + lineToRelative(-99f, 42f) + quadToRelative(-22f, -23f, -48.5f, -38.5f) + reflectiveQuadTo(533f, 266f) + lineToRelative(-13f, -106f) + horizontalLineToRelative(-79f) + lineToRelative(-14f, 106f) + quadToRelative(-31f, 8f, -57.5f, 23.5f) + reflectiveQuadTo(321f, 327f) + lineToRelative(-99f, -41f) + lineToRelative(-39f, 68f) + lineToRelative(86f, 64f) + quadToRelative(-5f, 15f, -7f, 30f) + reflectiveQuadToRelative(-2f, 32f) + quadToRelative(0f, 16f, 2f, 31f) + reflectiveQuadToRelative(7f, 30f) + lineToRelative(-86f, 65f) + lineToRelative(39f, 68f) + lineToRelative(99f, -42f) + quadToRelative(22f, 23f, 48.5f, 38.5f) + reflectiveQuadTo(427f, 694f) + lineToRelative(13f, 106f) + close() + moveTo(482f, 620f) + quadToRelative(58f, 0f, 99f, -41f) + reflectiveQuadToRelative(41f, -99f) + quadToRelative(0f, -58f, -41f, -99f) + reflectiveQuadToRelative(-99f, -41f) + quadToRelative(-59f, 0f, -99.5f, 41f) + reflectiveQuadTo(342f, 480f) + quadToRelative(0f, 58f, 40.5f, 99f) + reflectiveQuadToRelative(99.5f, 41f) + close() + moveTo(480f, 480f) + close() + } + }.build() + + return _Settings!! + } + +@Suppress("ObjectPropertyName") +private var _Settings: ImageVector? = null diff --git a/app/src/main/java/com/fedo/modelpulse/ui/icons/IconTimelineFilled.kt b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconTimelineFilled.kt new file mode 100644 index 0000000..e6f8fa9 --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/icons/IconTimelineFilled.kt @@ -0,0 +1,78 @@ +package com.fedo.modelpulse.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconTimeline: ImageVector + get() { + if (_Timeline != null) { + return _Timeline!! + } + _Timeline = ImageVector.Builder( + name = "Timeline", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(120f, 720f) + quadToRelative(-33f, 0f, -56.5f, -23.5f) + reflectiveQuadTo(40f, 640f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(120f, 560f) + horizontalLineToRelative(10.5f) + quadToRelative(4.5f, 0f, 9.5f, 2f) + lineToRelative(182f, -182f) + quadToRelative(-2f, -5f, -2f, -9.5f) + lineTo(320f, 360f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(400f, 280f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(480f, 360f) + quadToRelative(0f, 2f, -2f, 20f) + lineToRelative(102f, 102f) + quadToRelative(5f, -2f, 9.5f, -2f) + horizontalLineToRelative(21f) + quadToRelative(4.5f, 0f, 9.5f, 2f) + lineToRelative(142f, -142f) + quadToRelative(-2f, -5f, -2f, -9.5f) + lineTo(760f, 320f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(840f, 240f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(920f, 320f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(840f, 400f) + horizontalLineToRelative(-10.5f) + quadToRelative(-4.5f, 0f, -9.5f, -2f) + lineTo(678f, 540f) + quadToRelative(2f, 5f, 2f, 9.5f) + verticalLineToRelative(10.5f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(600f, 640f) + quadToRelative(-33f, 0f, -56.5f, -23.5f) + reflectiveQuadTo(520f, 560f) + verticalLineToRelative(-10.5f) + quadToRelative(0f, -4.5f, 2f, -9.5f) + lineTo(420f, 438f) + quadToRelative(-5f, 2f, -9.5f, 2f) + lineTo(400f, 440f) + quadToRelative(-2f, 0f, -20f, -2f) + lineTo(198f, 620f) + quadToRelative(2f, 5f, 2f, 9.5f) + verticalLineToRelative(10.5f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(120f, 720f) + close() + } + }.build() + + return _Timeline!! + } + +@Suppress("ObjectPropertyName") +private var _Timeline: ImageVector? = null From 30915a3f8d2b4f0a79ee1826dda0200c98c9640a Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 10:38:33 +0300 Subject: [PATCH 2/7] added material adaptive dependencies --- app/build.gradle.kts | 4 ++++ gradle/libs.versions.toml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e63313d..19d7b74 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.cache.internal.ProducerGuard.adaptive + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) @@ -84,6 +86,8 @@ dependencies { implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.androidx.adaptive) + implementation(libs.androidx.compose.adaptive.navigation3) implementation(platform(libs.koin.bom)) implementation(libs.koin.android) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e5588c..929ae82 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ composeBom = "2026.02.01" # Held at alpha01: it is the newest alpha that still carries the # ModalBottomSheet overload the Fedo SDK calls — decisions/0006. material3 = "1.5.0-alpha01" +adaptive = "1.3.0" coreKtx = "1.19.0" lifecycle = "2.11.0" activityCompose = "1.13.0" @@ -30,6 +31,8 @@ androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" } androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" } +androidx-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" } +androidx-compose-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } From e49e9aed44bd14bdcf0427b23f350168c4babe8a Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 10:38:48 +0300 Subject: [PATCH 3/7] replace bottom bar with HorizontalFloatingToolbar --- .../ui/navigation/ModelPulseNavDisplay.kt | 178 ++++++++++++++++-- .../fedo/modelpulse/ui/navigation/NavKeys.kt | 37 +++- 2 files changed, 196 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt b/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt index d7758a5..3f6fbfd 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt @@ -1,29 +1,65 @@ package com.fedo.modelpulse.ui.navigation +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingToolbarDefaults +import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset +import androidx.compose.material3.HorizontalFloatingToolbar +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay -import com.fedo.modelpulse.R +import androidx.window.core.layout.WindowSizeClass import com.fedo.modelpulse.FedoIntegration import com.fedo.modelpulse.ui.detail.ModelDetailRoute import com.fedo.modelpulse.ui.models.ModelsRoute import com.fedo.modelpulse.ui.roadmap.RoadmapScreen import com.fedo.modelpulse.ui.settings.SettingsRoute +import com.fedo.modelpulse.ui.theme.ModelPulseTheme import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ModelPulseNavDisplay(modifier: Modifier = Modifier) { val backStack = rememberNavBackStack(ModelsKey) @@ -31,19 +67,13 @@ fun ModelPulseNavDisplay(modifier: Modifier = Modifier) { Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { - NavigationBar { - TopLevelDestination.entries.forEach { destination -> - NavigationBarItem( - selected = backStack.lastOrNull() == destination.key, - onClick = { - backStack.clear() - backStack.addAll(backStackFor(destination)) - }, - icon = {}, - label = { Text(stringResource(destination.labelRes)) }, - ) + BottomBar( + currentNavKey = backStack.lastOrNull(), + onSelect = { destination -> + backStack.clear() + backStack.addAll(backStackFor(destination)) } - } + ) }, ) { innerPadding -> NavDisplay( @@ -81,3 +111,121 @@ fun ModelPulseNavDisplay(modifier: Modifier = Modifier) { /** Kept so the keys file is the only place that knows the destination list. */ internal val topLevelKeys: List = TopLevelDestination.entries.map(TopLevelDestination::key) + + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun BottomBar( + currentNavKey: NavKey?, + onSelect: (TopLevelDestination) -> Unit, + modifier: Modifier = Modifier +) { + val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass + val wide = remember { + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + ) + } + val systemBarsInsets = WindowInsets.systemBars.asPaddingValues() + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxWidth() + ) { + HorizontalFloatingToolbar( + expanded = true, + colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors( + toolbarContainerColor = colorScheme.primaryContainer, + toolbarContentColor = colorScheme.onPrimaryContainer + ), + modifier = Modifier + .padding( + top = ScreenOffset, + bottom = systemBarsInsets.calculateBottomPadding() + + ScreenOffset + ) + .zIndex(1f) + ) { + TopLevelDestination.entries.forEachIndexed { index, destination -> + val selected by remember(currentNavKey) { derivedStateOf { currentNavKey == destination.key } } + ToggleButton( + checked = selected, + onCheckedChange = { + if (it) onSelect(destination) + }, + colors = ToggleButtonDefaults.toggleButtonColors( + containerColor = colorScheme.primaryContainer, + contentColor = colorScheme.onPrimaryContainer, + checkedContainerColor = colorScheme.primary, + checkedContentColor = colorScheme.onPrimary + ), + shapes = ToggleButtonDefaults.shapes( + CircleShape, + CircleShape, + CircleShape + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Crossfade(selected) { + if (it) Icon( + destination.selectedIcon, + stringResource(destination.labelRes), + modifier = Modifier.size(24.dp) + ) + else Icon( + destination.unselectedIcon, + stringResource(destination.labelRes), + modifier = Modifier.size(24.dp) + ) + } + AnimatedVisibility( + visible = selected || wide, + enter = expandHorizontally(motionScheme.defaultSpatialSpec()), + exit = shrinkHorizontally(motionScheme.defaultSpatialSpec()) + ) { + Text( + text = stringResource(destination.labelRes), + fontSize = 16.sp, + lineHeight = 24.sp, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Clip, + modifier = Modifier.padding(start = ButtonDefaults.IconSpacing) + ) + } + } + } + } + } + } +} + +@Preview(name = "Light") +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(device = "spec:width=1280dp,height=800dp,dpi=240", name = "Tablet") +private annotation class ThemePreviews + +class BottomSheetStateProvider : PreviewParameterProvider { + override val values = sequenceOf( + null, + TopLevelDestination.MODELS.key, + TopLevelDestination.ROADMAP.key, + TopLevelDestination.SETTINGS.key, + ) +} + +@ThemePreviews +@Composable +private fun BottomBarPreview( + @PreviewParameter(BottomSheetStateProvider::class) navKey: NavKey? +) { + ModelPulseTheme { + Surface { + BottomBar( + currentNavKey = navKey, + onSelect = {} + ) + } + } +} diff --git a/app/src/main/java/com/fedo/modelpulse/ui/navigation/NavKeys.kt b/app/src/main/java/com/fedo/modelpulse/ui/navigation/NavKeys.kt index e907a76..aed751c 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/navigation/NavKeys.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/navigation/NavKeys.kt @@ -1,6 +1,15 @@ package com.fedo.modelpulse.ui.navigation +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.navigation3.runtime.NavKey +import com.fedo.modelpulse.R +import com.fedo.modelpulse.ui.icons.IconAiFilled +import com.fedo.modelpulse.ui.icons.IconAiOutlined +import com.fedo.modelpulse.ui.icons.IconSettingsFilled +import com.fedo.modelpulse.ui.icons.IconSettingsOutlined +import com.fedo.modelpulse.ui.icons.IconTimeline import kotlinx.serialization.Serializable @Serializable @@ -17,10 +26,30 @@ data object RoadmapKey : NavKey data object SettingsKey : NavKey /** A bottom bar destination: the key it opens and the label it shows. */ -enum class TopLevelDestination(val key: NavKey, val labelRes: Int) { - MODELS(ModelsKey, com.fedo.modelpulse.R.string.nav_models), - ROADMAP(RoadmapKey, com.fedo.modelpulse.R.string.nav_roadmap), - SETTINGS(SettingsKey, com.fedo.modelpulse.R.string.nav_settings), +enum class TopLevelDestination( + val key: NavKey, + val labelRes: Int, + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, +) { + MODELS( + ModelsKey, + R.string.nav_models, + IconAiFilled, + IconAiOutlined + ), + ROADMAP( + RoadmapKey, + R.string.nav_roadmap, + IconTimeline, + IconTimeline + ), + SETTINGS( + SettingsKey, + R.string.nav_settings, + IconSettingsFilled, + IconSettingsOutlined + ), } /** From 9ad8ce0fe67845f57a957dbacec5ede536e7069f Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 11:18:25 +0300 Subject: [PATCH 4/7] update screens content padding based on main Scaffol innerPadding --- .../main/java/com/fedo/modelpulse/ui/Utils.kt | 22 ++++++++ .../modelpulse/ui/detail/ModelDetailScreen.kt | 18 +++++-- .../fedo/modelpulse/ui/models/ModelsScreen.kt | 14 +++-- .../ui/navigation/ModelPulseNavDisplay.kt | 15 ++++-- .../modelpulse/ui/settings/SettingsScreen.kt | 53 ++++++++++++------- 5 files changed, 91 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/fedo/modelpulse/ui/Utils.kt diff --git a/app/src/main/java/com/fedo/modelpulse/ui/Utils.kt b/app/src/main/java/com/fedo/modelpulse/ui/Utils.kt new file mode 100644 index 0000000..b2df625 --- /dev/null +++ b/app/src/main/java/com/fedo/modelpulse/ui/Utils.kt @@ -0,0 +1,22 @@ +package com.fedo.modelpulse.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +fun mergePaddingValues( + topSource: PaddingValues, + restSource: PaddingValues +): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + + return PaddingValues( + top = topSource.calculateTopPadding(), + bottom = restSource.calculateBottomPadding(), + start = restSource.calculateStartPadding(layoutDirection), + end = restSource.calculateEndPadding(layoutDirection) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/fedo/modelpulse/ui/detail/ModelDetailScreen.kt b/app/src/main/java/com/fedo/modelpulse/ui/detail/ModelDetailScreen.kt index 7646208..7bb3183 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/detail/ModelDetailScreen.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/detail/ModelDetailScreen.kt @@ -4,6 +4,7 @@ import android.content.ClipData import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -44,6 +45,7 @@ import com.fedo.modelpulse.data.Price import com.fedo.modelpulse.data.contextLabel import com.fedo.modelpulse.data.perMillionLabel import com.fedo.modelpulse.data.relativeLabel +import com.fedo.modelpulse.ui.mergePaddingValues import com.fedo.modelpulse.ui.theme.ModelPulseTheme import java.math.BigDecimal import java.time.Instant @@ -56,6 +58,7 @@ internal fun ModelDetailRoute( onBackClick: () -> Unit, modifier: Modifier = Modifier, viewModel: ModelDetailViewModel = koinViewModel(), + contentPadding: PaddingValues = PaddingValues() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val clipboard = LocalClipboard.current @@ -72,6 +75,7 @@ internal fun ModelDetailRoute( } }, modifier = modifier, + contentPadding = contentPadding ) } @@ -82,6 +86,7 @@ internal fun ModelDetailScreen( onBackClick: () -> Unit, onCopyId: (String) -> Unit, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues() ) { Scaffold( modifier = modifier.fillMaxSize(), @@ -106,23 +111,25 @@ internal fun ModelDetailScreen( ) }, ) { innerPadding -> + val mergedContentPadding = mergePaddingValues(innerPadding, contentPadding) + when (uiState) { - ModelDetailUiState.Loading -> LoadingState(Modifier.padding(innerPadding)) + ModelDetailUiState.Loading -> LoadingState(Modifier.padding(mergedContentPadding)) ModelDetailUiState.NotFound -> Text( text = stringResource(R.string.detail_not_found), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier - .padding(innerPadding) .fillMaxWidth() + .padding(mergedContentPadding) .padding(32.dp), ) is ModelDetailUiState.Success -> ModelDetailContent( model = uiState.model, onCopyId = onCopyId, - modifier = Modifier.padding(innerPadding), + contentPadding = mergedContentPadding, ) } } @@ -133,13 +140,16 @@ private fun ModelDetailContent( model: AiModel, onCopyId: (String) -> Unit, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues() ) { Column( modifier = modifier .fillMaxSize() // A long description scrolls rather than clipping. .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(contentPadding) + .padding(horizontal = 16.dp, vertical = 8.dp) + , verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text(model.providerName, style = MaterialTheme.typography.titleMedium) diff --git a/app/src/main/java/com/fedo/modelpulse/ui/models/ModelsScreen.kt b/app/src/main/java/com/fedo/modelpulse/ui/models/ModelsScreen.kt index 324677e..36ba09f 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/models/ModelsScreen.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/models/ModelsScreen.kt @@ -62,6 +62,7 @@ import com.fedo.modelpulse.data.contextLabel import com.fedo.modelpulse.data.perMillionLabel import com.fedo.modelpulse.data.providerFilters import com.fedo.modelpulse.data.relativeLabel +import com.fedo.modelpulse.ui.mergePaddingValues import com.fedo.modelpulse.ui.theme.ModelPulseTheme import com.fedo.sdk.ui.FedoCreateFeedbackSheet import java.math.BigDecimal @@ -74,6 +75,7 @@ internal fun ModelsRoute( onModelClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: ModelsViewModel = koinViewModel(), + contentPadding: PaddingValues = PaddingValues(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -85,6 +87,7 @@ internal fun ModelsRoute( onQueryChange = viewModel::onQueryChange, onProviderChange = viewModel::onProviderChange, modifier = modifier, + contentPadding = contentPadding, ) } @@ -98,6 +101,7 @@ internal fun ModelsScreen( onQueryChange: (String) -> Unit, onProviderChange: (String?) -> Unit, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val snackbarHostState = remember { SnackbarHostState() } @@ -136,14 +140,16 @@ internal fun ModelsScreen( ) }, ) { innerPadding -> + val mergedContentPadding = mergePaddingValues(innerPadding, contentPadding) + when (uiState) { - ModelsUiState.Loading -> LoadingState(Modifier.padding(innerPadding)) + ModelsUiState.Loading -> LoadingState(Modifier.padding(mergedContentPadding)) is ModelsUiState.Error -> MessageState( message = stringResource(uiState.messageRes), actionLabel = stringResource(R.string.models_retry), onAction = onRefresh, - modifier = Modifier.padding(innerPadding), + modifier = Modifier.padding(mergedContentPadding), secondaryLabel = stringResource(R.string.models_report_problem), onSecondaryAction = openFeedback, ) @@ -156,7 +162,7 @@ internal fun ModelsScreen( actionLabel = stringResource(R.string.models_refresh), onAction = onRefresh, title = stringResource(R.string.models_empty_title), - modifier = Modifier.padding(innerPadding), + modifier = Modifier.padding(mergedContentPadding), ) } else { ModelsContent( @@ -166,7 +172,7 @@ internal fun ModelsScreen( onRefresh = onRefresh, onQueryChange = onQueryChange, onProviderChange = onProviderChange, - contentPadding = innerPadding, + contentPadding = mergedContentPadding, ) } } diff --git a/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt b/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt index 3f6fbfd..4f54a8d 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/navigation/ModelPulseNavDisplay.kt @@ -86,24 +86,31 @@ fun ModelPulseNavDisplay(modifier: Modifier = Modifier) { ), entryProvider = entryProvider { entry { - ModelsRoute(onModelClick = { id -> backStack.add(ModelDetailKey(id)) }) + ModelsRoute( + onModelClick = { id -> backStack.add(ModelDetailKey(id)) }, + contentPadding = innerPadding + ) } entry { key -> ModelDetailRoute( onBackClick = { backStack.removeLastOrNull() }, viewModel = koinViewModel { parametersOf(key) }, + contentPadding = innerPadding ) } - // ponytail: placeholders until uyb.1 and uyb.3 fill them in. entry { RoadmapScreen( isConfigured = FedoIntegration.isConfigured, onBack = { backStack.removeLastOrNull() }, + modifier = Modifier.padding(innerPadding) + ) + } + entry { + SettingsRoute( + contentPadding = innerPadding ) } - entry { SettingsRoute() } }, - modifier = Modifier.padding(innerPadding), ) } } diff --git a/app/src/main/java/com/fedo/modelpulse/ui/settings/SettingsScreen.kt b/app/src/main/java/com/fedo/modelpulse/ui/settings/SettingsScreen.kt index bd72378..77f1972 100644 --- a/app/src/main/java/com/fedo/modelpulse/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/fedo/modelpulse/ui/settings/SettingsScreen.kt @@ -3,6 +3,7 @@ package com.fedo.modelpulse.ui.settings import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -14,6 +15,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -26,6 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.fedo.modelpulse.R +import com.fedo.modelpulse.ui.mergePaddingValues import com.fedo.modelpulse.ui.theme.ModelPulseTheme import org.koin.androidx.compose.koinViewModel @@ -33,6 +36,7 @@ import org.koin.androidx.compose.koinViewModel internal fun SettingsRoute( modifier: Modifier = Modifier, viewModel: SettingsViewModel = koinViewModel(), + contentPadding: PaddingValues = PaddingValues() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -43,6 +47,7 @@ internal fun SettingsRoute( onSignIn = viewModel::signIn, onSignOut = viewModel::signOut, modifier = modifier, + contentPadding = contentPadding, ) } @@ -54,29 +59,39 @@ internal fun SettingsScreen( onSignIn: () -> Unit, onSignOut: () -> Unit, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues() ) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text( - text = stringResource(R.string.settings_title), - style = MaterialTheme.typography.headlineSmall, - ) + Scaffold( + modifier = modifier, + ) { innerPadding -> + val mergedContentPadding = mergePaddingValues(innerPadding, contentPadding) - SdkStatusCard(isConfigured = uiState.isConfigured) + Column( - DemoAccountCard( - uiState = uiState, - onNameChange = onNameChange, - onEmailChange = onEmailChange, - onSignIn = onSignIn, - onSignOut = onSignOut, - ) + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(mergedContentPadding) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(R.string.settings_title), + style = MaterialTheme.typography.headlineSmall, + ) + + SdkStatusCard(isConfigured = uiState.isConfigured) + + DemoAccountCard( + uiState = uiState, + onNameChange = onNameChange, + onEmailChange = onEmailChange, + onSignIn = onSignIn, + onSignOut = onSignOut, + ) + } } + } @Composable From 4ed613a7434c022885747b98387cec1517e054bc Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 11:21:33 +0300 Subject: [PATCH 5/7] docs: updated changelog --- CHANGELOG.md | 2 +- specs/architecture.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2acca..b83726e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,7 +110,7 @@ contextual feedback sheet on the other. full scrolling description, plus the model id in monospace with a copy action. An id missing from the catalogue shows a not-found state by @mabd-agent -- App navigation scaffold: a bottom navigation bar with Models, Roadmap and +- App navigation scaffold: a floating bottom navigation bar with Models, Roadmap and Settings, built on Navigation 3 (`NavDisplay` + a `@Serializable` `NavKey` back stack). Models is the back-stack root, so system back from any other destination returns to it. Roadmap and Settings are placeholders until the diff --git a/specs/architecture.md b/specs/architecture.md index 394f6c7..dd87cf0 100644 --- a/specs/architecture.md +++ b/specs/architecture.md @@ -109,9 +109,9 @@ refresh survives the ViewModel that triggered it. ### Data Sources -| Type | Implementation | Purpose | -|------|----------------|---------| -| Remote | OkHttp + kotlinx.serialization | Fetch the OpenRouter catalogue | +| Type | Implementation | Purpose | +|--------|--------------------------------------|------------------------------------------------------------| +| Remote | OkHttp + kotlinx.serialization | Fetch the OpenRouter catalogue | | Memory | `MutableStateFlow` in the repository | Survive navigation and failed refreshes within one session | There is no local database and no preferences store. Add one only when a From deb8ca653234cb32727fb459035b6ef1c45a12b1 Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 11:21:41 +0300 Subject: [PATCH 6/7] beads change --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 4256708..1f20648 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -19,6 +19,7 @@ {"_type":"issue","id":"fedo-android-example-its.3","title":"Models list screen","description":"Newest models first, with loading, error and empty states and pull-to-refresh. A failed refresh must keep already-loaded data and say so inline (do not wipe the list, do not fail silently). Only one load may run at a time: a new load cancels/supersedes the in-flight one so the initial load, refresh and retry cannot race. The error state offers Retry; the empty state offers Refresh. Row shows short name, provider display name, relative release date, context label and input price per 1M.","acceptance_criteria":"AC-1: the list shows models newest first. AC-2: a first load that fails shows an error state with Retry. AC-3: a refresh that fails while models are on screen keeps the list and shows an inline notice. AC-4: an empty catalogue shows an empty state with Refresh. AC-5: only one load runs at a time; a refresh started while a load is in flight joins it and shares its result instead of starting a second fetch.","status":"closed","priority":1,"issue_type":"feature","assignee":"mabd","owner":"mabd.agent@gmail.com","created_at":"2026-09-21T08:20:49Z","created_by":"mabd","updated_at":"2026-09-21T11:33:37Z","started_at":"2026-09-21T10:50:32Z","closed_at":"2026-09-21T11:12:24Z","close_reason":"List screen with loading, error, empty, refreshing and inline-refresh-error states; AC-1..AC-5 each have a named test; verified on device against the live catalogue. Needed a material3 1.5.0-alpha28 pin for expressive (decisions/0003).","labels":["app"],"dependencies":[{"issue_id":"fedo-android-example-its.3","depends_on_id":"fedo-android-example-its","type":"parent-child","created_at":"2026-09-21T11:20:49Z","created_by":"mabd","metadata":"{}"},{"issue_id":"fedo-android-example-its.3","depends_on_id":"fedo-android-example-its.1","type":"blocks","created_at":"2026-09-21T11:20:49Z","created_by":"mabd","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fedo-android-example-its.1","title":"OpenRouter client, model type and formatters","description":"Model type decoding the subset above, tolerant of null/missing optional fields. Derived values: release date, provider display name (name prefix before \": \" else id prefix with \"~\" stripped), a stable lowercase providerID grouping key, short name, price per 1M tokens as a display string, context label.\n\nFormatting rules (from the iOS example): \"0.00000096\" -\u003e \"$0.96\", \"0.000000075\" -\u003e \"$0.075\", \"0.000015\" -\u003e \"$15\", \"0\" -\u003e \"Free\", \"-1\"/unparsable -\u003e \"Variable\", and anything above 0 but below $0.01 per 1M -\u003e \"\u003c$0.01\" (never \"$0\"). Context: 1_048_576 -\u003e \"1M\", 200_000 -\u003e \"200K\", 131_072 -\u003e \"131K\", 32_768 -\u003e \"32K\", 512 -\u003e \"512\".\n\nFetch function returns models sorted newest first and throws on non-2xx. Keep decoding/formatting as pure functions so they can be unit tested without the network.","acceptance_criteria":"Fetching decodes all live models; formatter and grouping functions are pure and callable from tests.","status":"closed","priority":1,"issue_type":"task","owner":"mabd.agent@gmail.com","created_at":"2026-09-21T08:20:46Z","created_by":"mabd","updated_at":"2026-09-21T10:31:54Z","closed_at":"2026-09-21T10:31:54Z","close_reason":"Data layer landed: AiModel + Price + pure formatters + OpenRouterDataSource.getModels(). Verified against the live endpoint (446 models decode, newest first). Broader test coverage stays with its.2, the payload fixture with its.8.","labels":["app"],"dependencies":[{"issue_id":"fedo-android-example-its.1","depends_on_id":"fedo-android-example-8nq.2","type":"blocks","created_at":"2026-09-21T11:20:46Z","created_by":"mabd","metadata":"{}"},{"issue_id":"fedo-android-example-its.1","depends_on_id":"fedo-android-example-its","type":"parent-child","created_at":"2026-09-21T11:20:45Z","created_by":"mabd","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"fedo-android-example-its","title":"ModelPulse core app","description":"Browse the newest AI models from the public OpenRouter API: GET https://openrouter.ai/api/v1/models (no key, ~445 models).\n\nEach data[] entry has: id (\"anthropic/claude-opus-5\", sometimes \"~\" prefixed), name (often \"Provider: Model\"), created (unix seconds), description, context_length (nullable), pricing.prompt / pricing.completion (USD per token, as STRINGS; \"0\" = free, \"-1\" = variable), architecture.input_modalities ([String]).\n\nKeep the app small and readable - it is documentation for SDK integrators. Put logic (decoding, formatting, filtering) in plain testable functions, not inside composables.","status":"closed","priority":1,"issue_type":"epic","owner":"mabd.agent@gmail.com","created_at":"2026-09-21T08:20:45Z","created_by":"mabd","updated_at":"2026-09-23T05:43:07Z","closed_at":"2026-09-23T05:43:07Z","close_reason":"All children closed: catalogue client, formatters, list, search and provider filter, detail, navigation, fixture, snackbar, string resources, theme pass. Full gate green, three destinations smoke-tested on the tablet","labels":["app"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fedo-android-example-f9g.23","title":"Pre-release review sweep of the app source","description":"Two incidental findings (f9g.21 stale wording, f9g.22 unknown vs variable price) came out of re-reading merged code. Do it deliberately once, over the whole of app/src, before the first release: correctness, state handling, lifecycle, error paths, and places where the code and the specs disagree. File what is real; fix what is small.","acceptance_criteria":"AC-1: every file under app/src/main is reviewed.\nAC-2: each finding is either fixed with a test or filed as its own bead.\nAC-3: the gate stays green and the app still runs on the tablet.","status":"in_progress","priority":2,"issue_type":"task","assignee":"mabd","owner":"mabd.agent@gmail.com","created_at":"2026-09-23T06:04:19Z","created_by":"mabd","updated_at":"2026-09-23T06:04:20Z","started_at":"2026-09-23T06:04:20Z","labels":["docs","github"],"dependencies":[{"issue_id":"fedo-android-example-f9g.23","depends_on_id":"fedo-android-example-f9g","type":"parent-child","created_at":"2026-09-23T09:04:19Z","created_by":"mabd","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fedo-android-example-f9g.22","title":"A missing price renders as 'Variable', which is a different claim from 'unknown'","description":"Price.parse returns Variable for anything that will not parse, including the empty string that a missing pricing block maps to. OpenRouter ships entries with no pricing object (the test fixture has one), and the list then reads 'Variable / 1M in' for a model whose price we simply do not know. '-1' genuinely means variable pricing; absent data does not.\n\nspecs/testing.md already assumes the distinction exists — it asserts Price.Unknown for an unparseable price — but the sealed interface has no Unknown case, so the spec cannot compile.\n\nAdd Price.Unknown, map unparseable and absent prices to it, and render it the way an unknown context window already renders: an em dash.","acceptance_criteria":"AC-1: an absent or unparseable price is Price.Unknown, not Price.Variable.\nAC-2: '-1' is still Price.Variable.\nAC-3: Unknown renders as the same em dash an unknown context window uses.\nAC-4: specs/testing.md's Price example compiles against the real type.","status":"closed","priority":2,"issue_type":"bug","assignee":"mabd","owner":"mabd.agent@gmail.com","created_at":"2026-09-23T06:01:38Z","created_by":"mabd","updated_at":"2026-09-23T06:03:47Z","started_at":"2026-09-23T06:01:40Z","closed_at":"2026-09-23T06:03:47Z","close_reason":"AC-1..AC-4: Unknown added and rendered as an em dash, -1 still Variable, spec example now compiles; verified on device","labels":["docs","github"],"dependencies":[{"issue_id":"fedo-android-example-f9g.22","depends_on_id":"fedo-android-example-f9g","type":"parent-child","created_at":"2026-09-23T09:01:37Z","created_by":"mabd","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fedo-android-example-f9g.20","title":"Docs, badges and CI assume GitHub, but the only remote is Gitea","description":"git remote -v shows one remote: gitea ssh://git@git.mabd.dev:3101/fedo/fedo-android-example.git. There is no GitHub remote, and gh auth is not working here (invalid GITHUB_TOKEN).\n\nEverything written so far assumes github.com/getfedo/fedo-android-example: the README CI badge and clone URL, CONTRIBUTING's issue links, SECURITY.md's private advisory URL, .github/ISSUE_TEMPLATE and PULL_REQUEST_TEMPLATE, and .github/workflows/ci.yml with actions pinned by GitHub commit SHA.\n\nDecide which is true:\n- the repo is published to GitHub as well, and Gitea is a mirror - then nothing changes, but CI only runs once GitHub has the code;\n- the repo lives on Gitea only - then the badge, the clone URL, the advisory link and the issue links are wrong, and CI should be Gitea Actions (compatible syntax, but actions resolve through a configured mirror, not github.com).\n\nBlocks f9g.8 and f9g.9 from ever going green, and makes f9g.11 (repo metadata) ambiguous.","status":"closed","priority":2,"issue_type":"task","assignee":"mabd","owner":"mabd.agent@gmail.com","created_at":"2026-09-23T05:55:02Z","created_by":"mabd","updated_at":"2026-09-23T05:58:16Z","started_at":"2026-09-23T05:57:59Z","closed_at":"2026-09-23T05:58:16Z","close_reason":"Answered by the sibling repo: getfedo/fedo-ios-example is public on GitHub with CI, topics and homepage set, while its local clone also has only the Gitea remote. GitHub is the publish target; getfedo/fedo-android-example just does not exist yet (404). Docs stay as written; CONTRIBUTING now says so","labels":["docs","github"],"dependencies":[{"issue_id":"fedo-android-example-f9g.20","depends_on_id":"fedo-android-example-f9g","type":"parent-child","created_at":"2026-09-23T08:55:01Z","created_by":"mabd","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fedo-android-example-its.10","title":"Move the ViewModel's user-facing error strings into resources","description":"ModelsViewModel.userMessage() returns English sentences from Kotlin, so the two error messages the user reads are the only user-facing copy outside strings.xml. The constitution now says a ViewModel carries a @StringRes Int and the UI layer resolves it.","acceptance_criteria":"AC-1: no user-facing sentence remains in Kotlin outside strings.xml.\nAC-2: the error and refresh-error paths show the same text on screen as before.","status":"closed","priority":2,"issue_type":"task","assignee":"mabd","owner":"mabd.agent@gmail.com","created_at":"2026-09-23T05:04:59Z","created_by":"mabd","updated_at":"2026-09-23T05:08:49Z","started_at":"2026-09-23T05:05:27Z","closed_at":"2026-09-23T05:08:49Z","close_reason":"Rules added to the constitution and the one violation fixed; verified on device","labels":["app"],"dependencies":[{"issue_id":"fedo-android-example-its.10","depends_on_id":"fedo-android-example-its","type":"parent-child","created_at":"2026-09-23T08:04:59Z","created_by":"mabd","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} From 0eae721510704991486138c044e094f1e252bda4 Mon Sep 17 00:00:00 2001 From: MABD Date: Wed, 23 Sep 2026 11:32:58 +0300 Subject: [PATCH 7/7] remove unused import --- app/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19d7b74..43a3cd8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,3 @@ -import org.gradle.cache.internal.ProducerGuard.adaptive - plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose)