allow user to select a theme preference over light/dark/system (#318)

This commit is contained in:
lollerfirst
2025-08-28 08:14:46 +02:00
committed by GitHub
parent 846cd976c0
commit 28abd3c593
6 changed files with 94 additions and 8 deletions
@@ -2,6 +2,7 @@ package com.bitchat.android
import android.app.Application import android.app.Application
import com.bitchat.android.nostr.RelayDirectory import com.bitchat.android.nostr.RelayDirectory
import com.bitchat.android.ui.theme.ThemePreferenceManager
/** /**
* Main application class for bitchat Android * Main application class for bitchat Android
@@ -23,5 +24,8 @@ class BitchatApplication : Application() {
try { try {
com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(this) com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(this)
} catch (_: Exception) { } } catch (_: Exception) { }
// Initialize theme preference
ThemePreferenceManager.init(this)
} }
} }
@@ -10,13 +10,11 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -42,6 +40,8 @@ import com.bitchat.android.onboarding.PermissionManager
import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatScreen
import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.ui.theme.BitchatTheme
import com.bitchat.android.ui.theme.ThemePreference
import com.bitchat.android.ui.theme.ThemePreferenceManager
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -161,6 +161,40 @@ fun AboutSheet(
} }
} }
// Appearance section (theme toggle)
item {
val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "appearance",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.System,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) },
label = { Text("system", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.Light,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Light) },
label = { Text("light", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.Dark,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Dark) },
label = { Text("dark", fontFamily = FontFamily.Monospace) }
)
}
}
}
// Emergency warning // Emergency warning
item { item {
Surface( Surface(
@@ -35,6 +35,7 @@ import com.bitchat.android.R
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import com.bitchat.android.ui.theme.BASE_FONT_SIZE import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import androidx.compose.foundation.isSystemInDarkTheme
/** /**
* Input components for ChatScreen * Input components for ChatScreen
@@ -5,6 +5,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// Colors that match the iOS bitchat theme // Colors that match the iOS bitchat theme
@@ -36,14 +38,23 @@ private val LightColorScheme = lightColorScheme(
@Composable @Composable
fun BitchatTheme( fun BitchatTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean? = null,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { // App-level override from ThemePreferenceManager
darkTheme -> DarkColorScheme val themePref by ThemePreferenceManager.themeFlow.collectAsState(initial = ThemePreference.System)
else -> LightColorScheme val shouldUseDark = when (darkTheme) {
true -> true
false -> false
null -> when (themePref) {
ThemePreference.Dark -> true
ThemePreference.Light -> false
ThemePreference.System -> isSystemInDarkTheme()
}
} }
val colorScheme = if (shouldUseDark) DarkColorScheme else LightColorScheme
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
@@ -0,0 +1,36 @@
package com.bitchat.android.ui.theme
import android.content.Context
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* App theme preference: System default, Light, or Dark.
*/
enum class ThemePreference {
System, Light, Dark
}
/**
* Simple SharedPreferences-backed manager for theme preference with a StateFlow.
* Avoids adding DataStore dependency for now.
*/
object ThemePreferenceManager {
private const val PREFS_NAME = "bitchat_settings"
private const val KEY_THEME = "theme_preference"
private val _themeFlow = MutableStateFlow(ThemePreference.System)
val themeFlow: StateFlow<ThemePreference> = _themeFlow
fun init(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val saved = prefs.getString(KEY_THEME, ThemePreference.System.name)
_themeFlow.value = runCatching { ThemePreference.valueOf(saved!!) }.getOrDefault(ThemePreference.System)
}
fun set(context: Context, preference: ThemePreference) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_THEME, preference.name).apply()
_themeFlow.value = preference
}
}