From d12ccb8cb4fe4e7c24d0059219cf54cb008b69c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:18:21 +0000 Subject: [PATCH 1/7] Initial plan From cb0e500bf0ab2aa6a6e68524c1a9e01068cdbbab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:32:16 +0000 Subject: [PATCH 2/7] feat(android): Add theme selector and improve status bar visibility - Created light and dark theme XML with transparent status bars - Added ThemePreferenceManager for theme persistence - Implemented theme selector in navigation drawer - Fixed Java version compatibility (VERSION_17) - Added edge-to-edge display with proper window insets - Integrated Material3 dynamic colors for Android 12+ Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- app-android/build.gradle.kts | 6 +- .../codeoba/android/MainActivity.kt | 47 ++++++++++++++- .../codeoba/android/ThemePreferenceManager.kt | 60 +++++++++++++++++++ .../src/main/res/values-night/themes.xml | 14 +++++ app-android/src/main/res/values/themes.xml | 11 +++- .../codeoba/core/ui/CodeobaUI.kt | 56 ++++++++++++++++- 6 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/ThemePreferenceManager.kt create mode 100644 app-android/src/main/res/values-night/themes.xml diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index a1fa858..2f0394c 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -9,7 +9,7 @@ plugins { kotlin { compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_25) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } @@ -38,8 +38,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_25 - targetCompatibility = JavaVersion.VERSION_25 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt index d5204bd..6a0df3d 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt @@ -10,13 +10,19 @@ import android.util.Base64 import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat import kotlinx.coroutines.CoroutineScope @@ -41,6 +47,7 @@ class MainActivity : ComponentActivity() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private lateinit var codeobaApp: CodeobaApp + private lateinit var themePreferenceManager: ThemePreferenceManager companion object { private const val TAG = "MainActivity" @@ -85,6 +92,12 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Enable edge-to-edge display + enableEdgeToEdge() + + // Initialize theme preference manager + themePreferenceManager = ThemePreferenceManager(this) + // Request required permissions requestRequiredPermissions() @@ -105,7 +118,29 @@ class MainActivity : ComponentActivity() { ) setContent { - MaterialTheme { + val themeMode = themePreferenceManager.currentThemeMode() + val isDarkTheme = when (themeMode) { + ThemeMode.LIGHT -> false + ThemeMode.DARK -> true + ThemeMode.SYSTEM -> isSystemInDarkTheme() + } + + // Use dynamic colors on Android 12+ (API 31+), fallback to default color scheme + val colorScheme = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + if (isDarkTheme) { + dynamicDarkColorScheme(this) + } else { + dynamicLightColorScheme(this) + } + } else { + if (isDarkTheme) { + darkColorScheme() + } else { + lightColorScheme() + } + } + + MaterialTheme(colorScheme = colorScheme) { Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) } ) { padding -> @@ -123,6 +158,16 @@ class MainActivity : ComponentActivity() { CodeobaUI( app = codeobaApp, config = config, + themePreferenceManager = themePreferenceManager, + currentThemeMode = themeMode.name, + onThemeChange = { modeName -> + try { + val newMode = ThemeMode.valueOf(modeName) + themePreferenceManager.setThemeMode(newMode) + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Invalid theme mode: $modeName", e) + } + }, onTestWebViewClick = { // Launch test WebView activity startActivity( diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/ThemePreferenceManager.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/ThemePreferenceManager.kt new file mode 100644 index 0000000..f0c9e37 --- /dev/null +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/ThemePreferenceManager.kt @@ -0,0 +1,60 @@ +package llc.lookatwhataicando.codeoba.android + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Theme mode options for the app. + */ +enum class ThemeMode { + LIGHT, + DARK, + SYSTEM +} + +/** + * Manages theme preferences for the application. + * Stores user's theme selection and provides reactive state updates. + */ +class ThemePreferenceManager(context: Context) { + private val prefs: SharedPreferences = context.getSharedPreferences( + "theme_prefs", + Context.MODE_PRIVATE + ) + + private val _themeMode = MutableStateFlow(loadThemeMode()) + val themeMode: StateFlow = _themeMode.asStateFlow() + + companion object { + private const val KEY_THEME_MODE = "theme_mode" + } + + private fun loadThemeMode(): ThemeMode { + val savedMode = prefs.getString(KEY_THEME_MODE, ThemeMode.SYSTEM.name) + return try { + ThemeMode.valueOf(savedMode ?: ThemeMode.SYSTEM.name) + } catch (e: IllegalArgumentException) { + ThemeMode.SYSTEM + } + } + + fun setThemeMode(mode: ThemeMode) { + prefs.edit().putString(KEY_THEME_MODE, mode.name).apply() + _themeMode.value = mode + } +} + +/** + * Composable helper to get the current theme mode. + */ +@Composable +fun ThemePreferenceManager.currentThemeMode(): ThemeMode { + val mode by themeMode.collectAsState() + return mode +} diff --git a/app-android/src/main/res/values-night/themes.xml b/app-android/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..2829e5b --- /dev/null +++ b/app-android/src/main/res/values-night/themes.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app-android/src/main/res/values/themes.xml b/app-android/src/main/res/values/themes.xml index 55593a7..ffb5e92 100644 --- a/app-android/src/main/res/values/themes.xml +++ b/app-android/src/main/res/values/themes.xml @@ -1,7 +1,14 @@ - + diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index 17a6258..ed6b15e 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -7,10 +7,12 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -71,6 +73,9 @@ import llc.lookatwhataicando.codeoba.core.mirror fun CodeobaUI( app: CodeobaApp, config: RealtimeConfig, + themePreferenceManager: Any? = null, + currentThemeMode: String = "SYSTEM", + onThemeChange: ((String) -> Unit)? = null, onTestWebViewClick: (() -> Unit)? = null ) { val connectionState by app.connectionState.collectAsState() @@ -90,7 +95,6 @@ fun CodeobaUI( drawerState = drawerState, drawerContent = { ModalDrawerSheet { - // Placeholder drawer content Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -102,6 +106,20 @@ fun CodeobaUI( ) HorizontalDivider() + // Theme Selector + if (onThemeChange != null) { + Text( + "Theme", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + ThemeSelector( + currentMode = currentThemeMode, + onModeSelected = onThemeChange + ) + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + } + // Test WebView menu item if (onTestWebViewClick != null) { TextButton( @@ -125,6 +143,7 @@ fun CodeobaUI( } ) { Scaffold( + contentWindowInsets = WindowInsets.systemBars, topBar = { Column { TopAppBar( @@ -617,3 +636,38 @@ fun AudioRoutePanel( } } +@Composable +fun ThemeSelector( + currentMode: String, + onModeSelected: (String) -> Unit +) { + val themeModes = listOf("LIGHT", "DARK", "SYSTEM") + val themeLabels = mapOf( + "LIGHT" to "☀️ Light", + "DARK" to "🌙 Dark", + "SYSTEM" to "⚙️ System" + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + themeModes.forEach { mode -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = mode == currentMode, + onClick = { onModeSelected(mode) } + ) + Text( + text = themeLabels[mode] ?: mode, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} + From 9357049a751cc4ca2f9a0219489e6dcb8ea3b006 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:33:54 +0000 Subject: [PATCH 3/7] refactor: Remove unnecessary themePreferenceManager parameter from CodeobaUI Address code review feedback by removing the unused Any? parameter Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt | 1 - .../kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt index 6a0df3d..841aed9 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt @@ -158,7 +158,6 @@ class MainActivity : ComponentActivity() { CodeobaUI( app = codeobaApp, config = config, - themePreferenceManager = themePreferenceManager, currentThemeMode = themeMode.name, onThemeChange = { modeName -> try { diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index ed6b15e..eb5d86c 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -73,7 +73,6 @@ import llc.lookatwhataicando.codeoba.core.mirror fun CodeobaUI( app: CodeobaApp, config: RealtimeConfig, - themePreferenceManager: Any? = null, currentThemeMode: String = "SYSTEM", onThemeChange: ((String) -> Unit)? = null, onTestWebViewClick: (() -> Unit)? = null From f930525b97a03dab641046e258663cfc00c64587 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:35:00 +0000 Subject: [PATCH 4/7] improvement: Add user feedback for invalid theme mode selection Show snackbar when an invalid theme mode is encountered Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../llc/lookatwhataicando/codeoba/android/MainActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt index 841aed9..ee4d520 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt @@ -165,6 +165,9 @@ class MainActivity : ComponentActivity() { themePreferenceManager.setThemeMode(newMode) } catch (e: IllegalArgumentException) { Log.e(TAG, "Invalid theme mode: $modeName", e) + scope.launch { + snackbarHostState.showSnackbar("Invalid theme mode: $modeName") + } } }, onTestWebViewClick = { From 58516f1ab609c7cec25f82af916b5d2764751e8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:49:40 +0000 Subject: [PATCH 5/7] fix: Replace radio buttons with SingleChoiceSegmentedButtonRow and improve status bar contrast - Replaced column of radio buttons with Material3 SingleChoiceSegmentedButtonRow as requested - Added enforceStatusBarContrast for better visibility in both light and dark modes - Status bar icons now have proper contrast in dark mode Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../src/main/res/values-night/themes.xml | 3 ++ app-android/src/main/res/values/themes.xml | 2 ++ .../codeoba/core/ui/CodeobaUI.kt | 31 +++++++++++-------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app-android/src/main/res/values-night/themes.xml b/app-android/src/main/res/values-night/themes.xml index 2829e5b..f8fd005 100644 --- a/app-android/src/main/res/values-night/themes.xml +++ b/app-android/src/main/res/values-night/themes.xml @@ -6,9 +6,12 @@ shortEdges @android:color/transparent + false @android:color/transparent false + + true diff --git a/app-android/src/main/res/values/themes.xml b/app-android/src/main/res/values/themes.xml index ffb5e92..572414b 100644 --- a/app-android/src/main/res/values/themes.xml +++ b/app-android/src/main/res/values/themes.xml @@ -10,5 +10,7 @@ @android:color/transparent true + + true diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index eb5d86c..4008705 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -36,6 +37,9 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Tab @@ -647,23 +651,24 @@ fun ThemeSelector( "SYSTEM" to "⚙️ System" ) - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp) + val selectedIndex = themeModes.indexOf(currentMode).takeIf { it >= 0 } ?: 2 + + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() ) { - themeModes.forEach { mode -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + themeModes.forEachIndexed { index, mode -> + SegmentedButton( + selected = index == selectedIndex, + onClick = { onModeSelected(mode) }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = themeModes.size + ), + icon = {} ) { - RadioButton( - selected = mode == currentMode, - onClick = { onModeSelected(mode) } - ) Text( text = themeLabels[mode] ?: mode, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.labelLarge ) } } From 013e474b3075c0458a3500d580c14288fd3c177d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:59:40 +0000 Subject: [PATCH 6/7] fix: Remove excessive top margin by removing contentWindowInsets - Removed contentWindowInsets = WindowInsets.systemBars from Scaffold - With edge-to-edge enabled, content now draws behind transparent system bars - Eliminates the large empty space at top of screen and navigation drawer - System bars remain visible with enforceStatusBarContrast Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index 4008705..feb6176 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -7,13 +7,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -146,7 +144,6 @@ fun CodeobaUI( } ) { Scaffold( - contentWindowInsets = WindowInsets.systemBars, topBar = { Column { TopAppBar( From c21ac778724fd2609582ceb898780d11769acb9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:28:13 +0000 Subject: [PATCH 7/7] fix: Remove TopAppBar window insets to eliminate remaining top padding - Set TopAppBar windowInsets to WindowInsets(0, 0, 0, 0) - TopAppBar was adding automatic padding for status bar - With edge-to-edge, content now properly aligns at top - Eliminates the empty space between status bar and app bar Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index feb6176..e528a27 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -45,6 +46,7 @@ import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -169,7 +171,8 @@ fun CodeobaUI( enabled = connectionState !is ConnectionState.Connecting ) } - } + }, + windowInsets = WindowInsets(0, 0, 0, 0) ) // Tabs below the top bar