Add theme persistence and comprehensive Android design guidelines
This commit adds persistent theme storage and comprehensive documentation for Android development. Theme Persistence: - Created ThemeStorage with platform-specific implementations (SharedPreferences/UserDefaults) - Updated ThemeManager.initialize() to load saved theme on app start - Integrated ThemeStorage initialization in MainActivity and MainViewController - Theme selection now persists across app restarts Documentation (CLAUDE.md): - Added comprehensive Android Design System section - Documented all 11 themes and theme management - Provided color system guidelines (use MaterialTheme.colorScheme) - Documented spacing system (AppSpacing/AppRadius constants) - Added standard component usage examples (StandardCard, FormTextField, etc.) - Included screen patterns (Scaffold, pull-to-refresh, lists) - Provided button and dialog patterns - Listed key design principles for Android development Build Status: - ✅ Android builds successfully - ✅ iOS builds successfully - ✅ Theme persistence works on both platforms 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
340
CLAUDE.md
340
CLAUDE.md
@@ -513,6 +513,346 @@ if items.isEmpty {
|
|||||||
|
|
||||||
Android uses Compose UI directly from `composeApp` with shared ViewModels. Navigation via Jetpack Compose Navigation in `App.kt`.
|
Android uses Compose UI directly from `composeApp` with shared ViewModels. Navigation via Jetpack Compose Navigation in `App.kt`.
|
||||||
|
|
||||||
|
### Android Design System
|
||||||
|
|
||||||
|
**CRITICAL**: Always use the theme-aware design system components and colors. Never use hardcoded colors or spacing values.
|
||||||
|
|
||||||
|
#### Theme System
|
||||||
|
|
||||||
|
The app uses a comprehensive theming system with 11 themes matching iOS:
|
||||||
|
- **Default** (vibrant iOS system colors)
|
||||||
|
- **Teal**, **Ocean**, **Forest**, **Sunset**
|
||||||
|
- **Monochrome**, **Lavender**, **Crimson**, **Midnight**, **Desert**, **Mint**
|
||||||
|
|
||||||
|
**Theme Files:**
|
||||||
|
- `ui/theme/ThemeColors.kt` - All 11 themes with light/dark mode colors
|
||||||
|
- `ui/theme/ThemeManager.kt` - Singleton for dynamic theme switching with persistence
|
||||||
|
- `ui/theme/Spacing.kt` - Standardized spacing constants
|
||||||
|
- `ui/theme/Theme.kt` - Material3 theme integration
|
||||||
|
|
||||||
|
**Theme Usage:**
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
val currentTheme by remember { derivedStateOf { ThemeManager.currentTheme } }
|
||||||
|
|
||||||
|
MyCribTheme(themeColors = currentTheme) {
|
||||||
|
// App content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changing Themes:**
|
||||||
|
```kotlin
|
||||||
|
// In ProfileScreen or settings
|
||||||
|
ThemeManager.setTheme("ocean") // By ID
|
||||||
|
// or
|
||||||
|
ThemeManager.setTheme(AppThemes.Ocean) // By object
|
||||||
|
```
|
||||||
|
|
||||||
|
**Theme Persistence:**
|
||||||
|
Themes are automatically persisted using `ThemeStorage` (SharedPreferences on Android, UserDefaults on iOS). Initialize in MainActivity:
|
||||||
|
```kotlin
|
||||||
|
ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext))
|
||||||
|
ThemeManager.initialize() // Loads saved theme
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Color System
|
||||||
|
|
||||||
|
**ALWAYS use MaterialTheme.colorScheme instead of hardcoded colors:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ✅ CORRECT
|
||||||
|
Text(
|
||||||
|
text = "Hello",
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.backgroundSecondary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ❌ WRONG
|
||||||
|
Text(
|
||||||
|
text = "Hello",
|
||||||
|
color = Color(0xFF000000) // Never hardcode colors!
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Material3 ColorScheme Properties:**
|
||||||
|
- `primary`, `onPrimary` - Primary brand color and text on it
|
||||||
|
- `secondary`, `onSecondary` - Secondary brand color
|
||||||
|
- `error`, `onError` - Error states
|
||||||
|
- `background`, `onBackground` - Screen backgrounds
|
||||||
|
- `surface`, `onSurface` - Card/surface backgrounds
|
||||||
|
- `surfaceVariant`, `onSurfaceVariant` - Alternative surface colors
|
||||||
|
- **Custom extensions:**
|
||||||
|
- `backgroundSecondary` - For cards and elevated surfaces
|
||||||
|
- `textPrimary`, `textSecondary` - Semantic text colors
|
||||||
|
|
||||||
|
#### Spacing System
|
||||||
|
|
||||||
|
**ALWAYS use AppSpacing constants instead of hardcoded dp values:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ✅ CORRECT
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.padding(AppSpacing.lg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ WRONG
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp) // Never hardcode spacing!
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Spacing:**
|
||||||
|
```kotlin
|
||||||
|
AppSpacing.xs // 4.dp - Minimal spacing
|
||||||
|
AppSpacing.sm // 8.dp - Small spacing
|
||||||
|
AppSpacing.md // 12.dp - Medium spacing (default)
|
||||||
|
AppSpacing.lg // 16.dp - Large spacing
|
||||||
|
AppSpacing.xl // 24.dp - Extra large spacing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Radius:**
|
||||||
|
```kotlin
|
||||||
|
AppRadius.xs // 4.dp
|
||||||
|
AppRadius.sm // 8.dp
|
||||||
|
AppRadius.md // 12.dp - Standard card radius
|
||||||
|
AppRadius.lg // 16.dp
|
||||||
|
AppRadius.xl // 20.dp
|
||||||
|
AppRadius.xxl // 24.dp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Standard Components
|
||||||
|
|
||||||
|
**Use the provided standard components for consistency:**
|
||||||
|
|
||||||
|
**1. StandardCard - Primary card component:**
|
||||||
|
```kotlin
|
||||||
|
StandardCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = AppSpacing.lg // Default
|
||||||
|
) {
|
||||||
|
Text("Card content")
|
||||||
|
// More content...
|
||||||
|
}
|
||||||
|
|
||||||
|
// With custom background
|
||||||
|
StandardCard(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
) {
|
||||||
|
Text("Highlighted card")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. CompactCard - Smaller card variant:**
|
||||||
|
```kotlin
|
||||||
|
CompactCard {
|
||||||
|
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Text("Title")
|
||||||
|
Icon(Icons.Default.ChevronRight, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. FormTextField - Standardized input field:**
|
||||||
|
```kotlin
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
label = "Property Name",
|
||||||
|
placeholder = "Enter name",
|
||||||
|
leadingIcon = Icons.Default.Home,
|
||||||
|
error = error,
|
||||||
|
helperText = "This will be displayed on your dashboard",
|
||||||
|
keyboardType = KeyboardType.Text
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. FormSection - Group related form fields:**
|
||||||
|
```kotlin
|
||||||
|
FormSection(
|
||||||
|
header = "Property Details",
|
||||||
|
footer = "Enter the basic information about your property"
|
||||||
|
) {
|
||||||
|
FormTextField(value = name, onValueChange = { name = it }, label = "Name")
|
||||||
|
FormTextField(value = address, onValueChange = { address = it }, label = "Address")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. StandardEmptyState - Consistent empty states:**
|
||||||
|
```kotlin
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
StandardEmptyState(
|
||||||
|
icon = Icons.Default.Home,
|
||||||
|
title = "No Properties",
|
||||||
|
subtitle = "Add your first property to get started",
|
||||||
|
actionLabel = "Add Property",
|
||||||
|
onAction = { navigateToAddProperty() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Screen Patterns
|
||||||
|
|
||||||
|
**Standard Screen Structure:**
|
||||||
|
```kotlin
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MyScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
viewModel: MyViewModel = viewModel { MyViewModel() }
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Title", fontWeight = FontWeight.SemiBold) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
// Content with proper padding
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
// Content
|
||||||
|
}
|
||||||
|
is ApiResult.Loading -> {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
ErrorCard(message = state.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**List Screen with Pull-to-Refresh:**
|
||||||
|
```kotlin
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ListScreen() {
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
val items by viewModel.items.collectAsState()
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.loadItems(forceRefresh = true)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
items(items) { item ->
|
||||||
|
StandardCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick(item) }
|
||||||
|
) {
|
||||||
|
// Item content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Button Patterns
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Primary Action Button
|
||||||
|
Button(
|
||||||
|
onClick = { /* action */ },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(AppRadius.md)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Save, null)
|
||||||
|
Spacer(Modifier.width(AppSpacing.sm))
|
||||||
|
Text("Save Changes", fontWeight = FontWeight.SemiBold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructive Button
|
||||||
|
Button(
|
||||||
|
onClick = { /* action */ },
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Delete, null)
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text Button
|
||||||
|
TextButton(onClick = { /* action */ }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dialog Pattern
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun ThemePickerDialog(
|
||||||
|
currentTheme: ThemeColors,
|
||||||
|
onThemeSelected: (ThemeColors) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(AppRadius.lg),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(AppSpacing.xl)) {
|
||||||
|
Text(
|
||||||
|
"Choose Theme",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
// Content...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Key Design Principles
|
||||||
|
|
||||||
|
1. **Always use theme-aware colors** from MaterialTheme.colorScheme
|
||||||
|
2. **Always use spacing constants** from AppSpacing/AppRadius
|
||||||
|
3. **Use standard components** (StandardCard, FormTextField, etc.) for consistency
|
||||||
|
4. **Follow Material3 guidelines** for component usage
|
||||||
|
5. **Support dynamic theming** - never assume a specific theme
|
||||||
|
6. **Test in both light and dark mode** - all themes support both
|
||||||
|
|
||||||
## Environment Configuration
|
## Environment Configuration
|
||||||
|
|
||||||
**API Environment Toggle** (`composeApp/src/commonMain/kotlin/com/example/mycrib/network/ApiConfig.kt`):
|
**API Environment Toggle** (`composeApp/src/commonMain/kotlin/com/example/mycrib/network/ApiConfig.kt`):
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import com.example.mycrib.storage.TokenManager
|
|||||||
import com.example.mycrib.storage.TokenStorage
|
import com.example.mycrib.storage.TokenStorage
|
||||||
import com.example.mycrib.storage.TaskCacheManager
|
import com.example.mycrib.storage.TaskCacheManager
|
||||||
import com.example.mycrib.storage.TaskCacheStorage
|
import com.example.mycrib.storage.TaskCacheStorage
|
||||||
|
import com.example.mycrib.storage.ThemeStorage
|
||||||
|
import com.example.mycrib.storage.ThemeStorageManager
|
||||||
|
import com.example.mycrib.ui.theme.ThemeManager
|
||||||
import com.example.mycrib.fcm.FCMManager
|
import com.example.mycrib.fcm.FCMManager
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -42,6 +45,10 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
|
|||||||
// Initialize TaskCacheStorage for offline task caching
|
// Initialize TaskCacheStorage for offline task caching
|
||||||
TaskCacheStorage.initialize(TaskCacheManager.getInstance(applicationContext))
|
TaskCacheStorage.initialize(TaskCacheManager.getInstance(applicationContext))
|
||||||
|
|
||||||
|
// Initialize ThemeStorage and ThemeManager
|
||||||
|
ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext))
|
||||||
|
ThemeManager.initialize()
|
||||||
|
|
||||||
// Handle deep link from intent
|
// Handle deep link from intent
|
||||||
handleDeepLink(intent)
|
handleDeepLink(intent)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.mycrib.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android implementation of theme storage using SharedPreferences.
|
||||||
|
*/
|
||||||
|
actual class ThemeStorageManager(context: Context) {
|
||||||
|
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
actual fun saveThemeId(themeId: String) {
|
||||||
|
prefs.edit().putString(KEY_THEME_ID, themeId).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getThemeId(): String? {
|
||||||
|
return prefs.getString(KEY_THEME_ID, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun clearThemeId() {
|
||||||
|
prefs.edit().remove(KEY_THEME_ID).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "mycrib_theme_prefs"
|
||||||
|
private const val KEY_THEME_ID = "theme_id"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: ThemeStorageManager? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): ThemeStorageManager {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: ThemeStorageManager(context.applicationContext).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.example.mycrib.storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform theme storage for persisting theme selection.
|
||||||
|
* Uses platform-specific implementations (SharedPreferences on Android, UserDefaults on iOS).
|
||||||
|
*/
|
||||||
|
object ThemeStorage {
|
||||||
|
private var manager: ThemeStorageManager? = null
|
||||||
|
|
||||||
|
fun initialize(themeManager: ThemeStorageManager) {
|
||||||
|
manager = themeManager
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveThemeId(themeId: String) {
|
||||||
|
manager?.saveThemeId(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThemeId(): String? {
|
||||||
|
return manager?.getThemeId()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearThemeId() {
|
||||||
|
manager?.clearThemeId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific theme storage interface.
|
||||||
|
* Each platform implements this using their native storage mechanisms.
|
||||||
|
*/
|
||||||
|
expect class ThemeStorageManager {
|
||||||
|
fun saveThemeId(themeId: String)
|
||||||
|
fun getThemeId(): String?
|
||||||
|
fun clearThemeId()
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ package com.example.mycrib.ui.theme
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.example.mycrib.storage.ThemeStorage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeManager - Singleton for managing app themes
|
* ThemeManager - Singleton for managing app themes
|
||||||
* Matches iOS ThemeManager functionality
|
* Matches iOS ThemeManager functionality with persistent storage
|
||||||
* TODO: Add DataStore persistence
|
|
||||||
*/
|
*/
|
||||||
object ThemeManager {
|
object ThemeManager {
|
||||||
private const val DEFAULT_THEME_ID = "default"
|
private const val DEFAULT_THEME_ID = "default"
|
||||||
@@ -19,16 +19,28 @@ object ThemeManager {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set theme by ID
|
* Initialize theme manager and load saved theme
|
||||||
|
* Call this after ThemeStorage.initialize()
|
||||||
|
*/
|
||||||
|
fun initialize() {
|
||||||
|
val savedThemeId = ThemeStorage.getThemeId()
|
||||||
|
if (savedThemeId != null) {
|
||||||
|
val savedTheme = AppThemes.getThemeById(savedThemeId)
|
||||||
|
currentTheme = savedTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set theme by ID and persist the selection
|
||||||
*/
|
*/
|
||||||
fun setTheme(themeId: String) {
|
fun setTheme(themeId: String) {
|
||||||
val newTheme = AppThemes.getThemeById(themeId)
|
val newTheme = AppThemes.getThemeById(themeId)
|
||||||
currentTheme = newTheme
|
currentTheme = newTheme
|
||||||
// TODO: Persist theme selection
|
ThemeStorage.saveThemeId(themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set theme by ThemeColors object
|
* Set theme by ThemeColors object and persist the selection
|
||||||
*/
|
*/
|
||||||
fun setTheme(theme: ThemeColors) {
|
fun setTheme(theme: ThemeColors) {
|
||||||
setTheme(theme.id)
|
setTheme(theme.id)
|
||||||
@@ -42,7 +54,7 @@ object ThemeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset to default theme
|
* Reset to default theme and persist the selection
|
||||||
*/
|
*/
|
||||||
fun resetToDefault() {
|
fun resetToDefault() {
|
||||||
setTheme(DEFAULT_THEME_ID)
|
setTheme(DEFAULT_THEME_ID)
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import com.example.mycrib.storage.TokenManager
|
|||||||
import com.example.mycrib.storage.TokenStorage
|
import com.example.mycrib.storage.TokenStorage
|
||||||
import com.example.mycrib.storage.TaskCacheManager
|
import com.example.mycrib.storage.TaskCacheManager
|
||||||
import com.example.mycrib.storage.TaskCacheStorage
|
import com.example.mycrib.storage.TaskCacheStorage
|
||||||
|
import com.example.mycrib.storage.ThemeStorage
|
||||||
|
import com.example.mycrib.storage.ThemeStorageManager
|
||||||
|
import com.example.mycrib.ui.theme.ThemeManager
|
||||||
|
|
||||||
fun MainViewController() = ComposeUIViewController {
|
fun MainViewController() = ComposeUIViewController {
|
||||||
// Initialize TokenStorage with iOS TokenManager
|
// Initialize TokenStorage with iOS TokenManager
|
||||||
@@ -13,5 +16,9 @@ fun MainViewController() = ComposeUIViewController {
|
|||||||
// Initialize TaskCacheStorage for offline task caching
|
// Initialize TaskCacheStorage for offline task caching
|
||||||
TaskCacheStorage.initialize(TaskCacheManager.getInstance())
|
TaskCacheStorage.initialize(TaskCacheManager.getInstance())
|
||||||
|
|
||||||
|
// Initialize ThemeStorage and ThemeManager
|
||||||
|
ThemeStorage.initialize(ThemeStorageManager.getInstance())
|
||||||
|
ThemeManager.initialize()
|
||||||
|
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.example.mycrib.storage
|
||||||
|
|
||||||
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS implementation of theme storage using NSUserDefaults.
|
||||||
|
*/
|
||||||
|
actual class ThemeStorageManager {
|
||||||
|
private val defaults = NSUserDefaults.standardUserDefaults
|
||||||
|
|
||||||
|
actual fun saveThemeId(themeId: String) {
|
||||||
|
defaults.setObject(themeId, forKey = KEY_THEME_ID)
|
||||||
|
defaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getThemeId(): String? {
|
||||||
|
return defaults.stringForKey(KEY_THEME_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun clearThemeId() {
|
||||||
|
defaults.removeObjectForKey(KEY_THEME_ID)
|
||||||
|
defaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_THEME_ID = "theme_id"
|
||||||
|
|
||||||
|
private val instance by lazy { ThemeStorageManager() }
|
||||||
|
|
||||||
|
fun getInstance(): ThemeStorageManager = instance
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user