From 83c2c3ccdc26db88ec8523a4984a229a25b4f286 Mon Sep 17 00:00:00 2001 From: ralina Date: Mon, 1 Jun 2026 21:09:20 +0300 Subject: [PATCH] fixed database and strings --- build.gradle.kts | 1 + .../mobile/archive/ArchiveEventsView.kt | 36 ++--- .../kotlin/friends/mobile/auth/LoginScreen.kt | 11 +- .../mobile/auth/RegisterBottomSheet.kt | 16 ++- .../kotlin/friends/mobile/auth/RootScreen.kt | 16 +++ .../friends/mobile/calendar/BusyDaysView.kt | 25 ++-- .../designsystem/components/ButtonFactory.kt | 6 +- .../designsystem/components/ErrorBanner.kt | 56 +++++--- .../designsystem/components/LoadingView.kt | 2 +- .../designsystem/components/SearchBar.kt | 4 +- .../friends/mobile/events/CreateEventView.kt | 46 +++--- .../friends/mobile/events/EventDetailView.kt | 42 +++--- .../events/PendingEventDetailContent.kt | 22 +-- .../mobile/events/PendingEventListView.kt | 29 ++-- .../mobile/friends/FriendProfileScreen.kt | 17 ++- .../mobile/friends/FriendsComponents.kt | 26 ++-- .../friends/mobile/friends/FriendsScreen.kt | 7 +- .../kotlin/friends/mobile/main/MainView.kt | 61 ++++---- .../mobile/profile/EditProfileScreen.kt | 16 ++- .../friends/mobile/profile/ProfileScreen.kt | 16 +-- .../mobile/wishplaces/WishPlacesComponents.kt | 22 +-- .../mobile/wishplaces/WishPlacesSection.kt | 17 ++- .../src/androidMain/res/values/strings.xml | 136 ++++++++++++++++++ gradle/libs.versions.toml | 5 + shared/build.gradle.kts | 12 ++ .../mobile/core/di/PlatformModule.android.kt | 10 ++ .../mobile/core/db/ProfileCacheStorage.kt | 42 +++--- .../mobile/core/db/di/DatabaseModule.kt | 9 +- .../feature/auth/presentation/RootEvent.kt | 1 + .../auth/presentation/RootViewModel.kt | 3 + .../main/presentation/MainViewModel.kt | 11 +- .../feature/profile/di/ProfileModule.kt | 1 - .../friends/mobile/core/db/Profile.sq | 16 +++ .../mobile/core/di/PlatformModule.ios.kt | 9 ++ 34 files changed, 487 insertions(+), 262 deletions(-) create mode 100644 shared/src/commonMain/sqldelight/friends/mobile/core/db/Profile.sq diff --git a/build.gradle.kts b/build.gradle.kts index 3cf6cd3..1aa9782 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.detekt) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.firebase.crashlytics) apply false + alias(libs.plugins.sqldelight) apply false } val detektVersion: String? = libs.versions.detekt.get() diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/archive/ArchiveEventsView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/archive/ArchiveEventsView.kt index f2b9a99..2bf18ee 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/archive/ArchiveEventsView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/archive/ArchiveEventsView.kt @@ -21,7 +21,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.CalendarToday import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Schedule import androidx.compose.material3.CircularProgressIndicator @@ -46,8 +45,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.designsystem.components.ButtonFactory +import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.feature.archive.presentation.ArchiveEventsViewModel import friends.mobile.feature.archive.presentation.ArchiveViewAction import friends.mobile.feature.archive.presentation.ArchiveViewState @@ -69,7 +71,7 @@ fun ArchiveEventsView( Scaffold( topBar = { TopAppBar( - title = { Text("Archive", style = DesignTheme.Typography.heading) } + title = { Text(stringResource(R.string.archive_title), style = DesignTheme.Typography.heading) } ) } ) { innerPadding -> @@ -83,31 +85,15 @@ fun ArchiveEventsView( } errorMessage != null -> { - Column( + Box( modifier = Modifier .fillMaxSize() .padding(DesignTheme.Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + contentAlignment = Alignment.Center, ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.height(DesignTheme.Spacing.md)) - Text( - text = errorMessage, - style = DesignTheme.Typography.body, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(DesignTheme.Spacing.md)) - ButtonFactory.Primary( - text = "Retry", - onClick = { viewModel.obtainEvent(ArchiveViewAction.OnRefresh) }, - modifier = Modifier.fillMaxWidth() + ErrorBanner( + message = errorMessage, + onRetry = { viewModel.obtainEvent(ArchiveViewAction.OnRefresh) } ) } } @@ -126,7 +112,7 @@ fun ArchiveEventsView( ) Spacer(modifier = Modifier.height(DesignTheme.Spacing.sm)) Text( - text = "No archived events", + text = stringResource(R.string.archive_empty), style = DesignTheme.Typography.body, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -216,7 +202,7 @@ private fun CompletedBadge() { tint = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = "Completed", + text = stringResource(R.string.archive_badge_completed), style = DesignTheme.Typography.bodySmallest.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/auth/LoginScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/auth/LoginScreen.kt index 656bd0f..86c90c7 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/auth/LoginScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/auth/LoginScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType @@ -139,7 +140,7 @@ private fun LoginForm( FormTextField( value = username, onValueChange = onUsernameChanged, - placeholder = "Username", + placeholder = stringResource(R.string.label_username), modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, @@ -150,7 +151,7 @@ private fun LoginForm( FormSecureField( value = password, onValueChange = onPasswordChanged, - placeholder = "Password", + placeholder = stringResource(R.string.label_password), modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) @@ -163,7 +164,7 @@ private fun LoginForm( item { ButtonFactory.Primary( - text = "Login", + text = stringResource(R.string.auth_login), onClick = onLoginClick, modifier = Modifier .fillMaxWidth() @@ -180,12 +181,12 @@ private fun LoginForm( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Don't have an account?", + text = stringResource(R.string.auth_no_account), style = DesignTheme.Typography.caption, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = "Sign up", + text = stringResource(R.string.auth_sign_up), modifier = Modifier.clickable { onRegisterClick() }, style = DesignTheme.Typography.captionSemibold, color = MaterialTheme.colorScheme.primary diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/auth/RegisterBottomSheet.kt b/composeApp/src/androidMain/kotlin/friends/mobile/auth/RegisterBottomSheet.kt index ee843aa..8daae80 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/auth/RegisterBottomSheet.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/auth/RegisterBottomSheet.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.FormErrorMessage import friends.mobile.designsystem.components.FormSecureField @@ -89,14 +91,14 @@ fun RegisterBottomSheet( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Create Account", + text = stringResource(R.string.auth_create_account), style = DesignTheme.Typography.heading, color = MaterialTheme.colorScheme.onSurface ) IconButton(onClick = onDismiss) { Icon( imageVector = Icons.Default.Cancel, - contentDescription = "Close", + contentDescription = stringResource(R.string.close), tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } @@ -116,7 +118,7 @@ fun RegisterBottomSheet( username = it viewModel.obtainEvent(RegisterEvent.OnUsernameChanged(it)) }, - placeholder = "Username", + placeholder = stringResource(R.string.label_username), modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, @@ -130,7 +132,7 @@ fun RegisterBottomSheet( password = it viewModel.obtainEvent(RegisterEvent.OnPasswordChanged(it)) }, - placeholder = "Password", + placeholder = stringResource(R.string.label_password), modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) @@ -143,7 +145,7 @@ fun RegisterBottomSheet( item { ButtonFactory.Primary( - text = "Create Account", + text = stringResource(R.string.auth_create_account), onClick = { viewModel.obtainEvent(RegisterEvent.OnRegisterClick) }, modifier = Modifier .fillMaxWidth() @@ -162,13 +164,13 @@ fun RegisterBottomSheet( .padding(horizontal = DesignTheme.Spacing.xl) ) { Text( - text = "Already have an account?", + text = stringResource(R.string.auth_already_have_account), style = DesignTheme.Typography.caption, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(DesignTheme.Spacing.xs)) Text( - text = "Login", + text = stringResource(R.string.auth_login), modifier = Modifier.clickable { onDismiss() }, style = DesignTheme.Typography.captionSemibold, color = MaterialTheme.colorScheme.primary diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/auth/RootScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/auth/RootScreen.kt index 9407155..bee1573 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/auth/RootScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/auth/RootScreen.kt @@ -2,12 +2,16 @@ package friends.mobile.auth import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import friends.mobile.designsystem.components.ErrorBanner +import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.feature.auth.presentation.RootEvent import friends.mobile.feature.auth.presentation.RootViewModel import friends.mobile.feature.auth.presentation.RootViewState @@ -27,6 +31,18 @@ fun RootScreen( } } is RootViewState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(DesignTheme.Spacing.lg), + contentAlignment = Alignment.Center, + ) { + ErrorBanner( + message = currentState.message, + modifier = Modifier.fillMaxWidth(), + onRetry = { viewModel.obtainEvent(RootEvent.OnRetry) } + ) + } } is RootViewState.Content -> { val session = currentState.session diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/calendar/BusyDaysView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/calendar/BusyDaysView.kt index 2e81725..2a87b8f 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/calendar/BusyDaysView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/calendar/BusyDaysView.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.theme.DesignTheme @@ -99,7 +101,15 @@ private fun WeekdayLabels() { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - val weekdays = listOf("S", "M", "T", "W", "T", "F", "S") + val weekdays = listOf( + stringResource(R.string.calendar_weekday_sun), + stringResource(R.string.calendar_weekday_mon), + stringResource(R.string.calendar_weekday_tue), + stringResource(R.string.calendar_weekday_wed), + stringResource(R.string.calendar_weekday_thu), + stringResource(R.string.calendar_weekday_fri), + stringResource(R.string.calendar_weekday_sat), + ) weekdays.forEach { day -> Text( text = day, @@ -181,7 +191,7 @@ private fun EmptyContent() { tint = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = "No busy days recorded", + text = stringResource(R.string.calendar_no_busy_days), style = DesignTheme.Typography.body, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -190,12 +200,9 @@ private fun EmptyContent() { @Composable private fun ErrorContent(message: String, onRetry: () -> Unit) { - Column( + ErrorBanner( + message = message, modifier = Modifier.fillMaxWidth().padding(vertical = DesignTheme.Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) - ) { - ErrorBanner(message = message, modifier = Modifier.fillMaxWidth()) - ButtonFactory.Primary(text = "Retry", onClick = onRetry, modifier = Modifier.fillMaxWidth()) - } + onRetry = onRetry + ) } diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ButtonFactory.kt b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ButtonFactory.kt index 841dcae..4a81889 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ButtonFactory.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ButtonFactory.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme object ButtonFactory { @@ -62,7 +64,7 @@ object ButtonFactory { color = Color.White, strokeWidth = 2.dp ) - Text("Loading...", style = DesignTheme.Typography.button) + Text(stringResource(R.string.loading), style = DesignTheme.Typography.button) } } else { Row( @@ -205,7 +207,7 @@ object ButtonFactory { color = Color.White, strokeWidth = 2.dp ) - Text("Removing...", style = DesignTheme.Typography.button) + Text(stringResource(R.string.loading_removing), style = DesignTheme.Typography.button) } } else { Text(text, style = DesignTheme.Typography.button) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ErrorBanner.kt b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ErrorBanner.kt index dffdc3c..b529c40 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ErrorBanner.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/ErrorBanner.kt @@ -2,6 +2,7 @@ package friends.mobile.designsystem.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -16,34 +17,49 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme @Composable fun ErrorBanner( message: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null ) { val errorColor = MaterialTheme.colorScheme.error - Row( - modifier = modifier - .fillMaxWidth() - .background(errorColor.copy(alpha = 0.1f), RoundedCornerShape(10.dp)) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = errorColor, - modifier = Modifier.size(16.dp) - ) - Text( - text = message, - style = DesignTheme.Typography.bodySmall, - color = errorColor - ) - Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier + .fillMaxWidth() + .background(errorColor.copy(alpha = 0.1f), RoundedCornerShape(10.dp)) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = errorColor, + modifier = Modifier.size(16.dp) + ) + Text( + text = message, + style = DesignTheme.Typography.bodySmall, + color = errorColor + ) + Spacer(modifier = Modifier.weight(1f)) + } + if (onRetry != null) { + ButtonFactory.Primary( + text = stringResource(R.string.retry), + onClick = onRetry, + modifier = Modifier.fillMaxWidth() + ) + } } } diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/LoadingView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/LoadingView.kt index 9beae49..cb093f9 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/LoadingView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/LoadingView.kt @@ -21,7 +21,7 @@ import friends.mobile.designsystem.theme.DesignTheme @Composable fun LoadingView( modifier: Modifier = Modifier, - message: String = "Loading friends..." + message: String = "Loading..." ) { Column( modifier = modifier diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/SearchBar.kt b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/SearchBar.kt index f49d398..45029bc 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/SearchBar.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/designsystem/components/SearchBar.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.material3.MaterialTheme import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme /** @@ -88,7 +90,7 @@ fun SearchBar( IconButton(onClick = onClear) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear", + contentDescription = stringResource(R.string.search_clear), tint = Color.Gray, modifier = Modifier.size(18.dp) ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/events/CreateEventView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/events/CreateEventView.kt index f2318a4..ece78e8 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/events/CreateEventView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/events/CreateEventView.kt @@ -50,6 +50,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.components.FormErrorMessage @@ -93,12 +95,12 @@ fun CreateEventView( Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text("Create Event") }, + title = { Text(stringResource(R.string.event_create_title)) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.primary ) } @@ -110,7 +112,7 @@ fun CreateEventView( is CreateEventViewState.Loading -> { LoadingView( modifier = Modifier.fillMaxSize().padding(innerPadding), - message = "Loading available friends..." + message = stringResource(R.string.event_loading_friends) ) } @@ -125,7 +127,7 @@ fun CreateEventView( ) { ErrorBanner(message = (state as CreateEventViewState.Error).message) ButtonFactory.Primary( - text = "Back", + text = stringResource(R.string.back), onClick = onBackClick, modifier = Modifier.padding(top = DesignTheme.Spacing.lg), ) @@ -155,7 +157,7 @@ fun CreateEventView( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.sm) ) { Text( - text = "Event Date", + text = stringResource(R.string.event_section_date), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -178,7 +180,7 @@ fun CreateEventView( ) { CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) Text( - text = "Loading available friends...", + text = stringResource(R.string.event_loading_friends), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -201,31 +203,31 @@ fun CreateEventView( item { Column(verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.lg)) { LabeledFormField( - label = "Title", + label = stringResource(R.string.label_title), icon = Icons.Default.Edit, value = contentState.title, onValueChange = { viewModel.obtainEvent(CreateEventEvent.OnTitleChanged(it)) }, - placeholder = "Event title" + placeholder = stringResource(R.string.event_hint_title) ) LabeledFormField( - label = "Description", + label = stringResource(R.string.label_description), icon = Icons.Default.Description, value = contentState.description, onValueChange = { viewModel.obtainEvent(CreateEventEvent.OnDescriptionChanged(it)) }, - placeholder = "Event description" + placeholder = stringResource(R.string.event_hint_description) ) LabeledFormField( - label = "Location", + label = stringResource(R.string.label_location), icon = Icons.Default.LocationOn, value = contentState.location, onValueChange = { viewModel.obtainEvent(CreateEventEvent.OnLocationChanged(it)) }, - placeholder = "Event location" + placeholder = stringResource(R.string.event_hint_location) ) } } @@ -247,7 +249,7 @@ fun CreateEventView( // Create button fixed at bottom ButtonFactory.Primary( - text = "Create Event", + text = stringResource(R.string.event_create_title), onClick = { viewModel.obtainEvent(CreateEventEvent.OnCreateEvent) }, isLoading = contentState.isCreatingEvent, isEnabled = contentState.isCreateButtonEnabled && !contentState.isCreatingEvent, @@ -347,14 +349,14 @@ private fun FriendsSelectionSection( tint = MaterialTheme.colorScheme.onSurface, ) Text( - text = "Select Friends", + text = stringResource(R.string.event_select_friends), style = DesignTheme.Typography.button, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f) ) if (selectedCount > 0) { Text( - text = "$selectedCount selected", + text = stringResource(R.string.event_selected_count, selectedCount), style = DesignTheme.Typography.bodySmallest, color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier @@ -382,7 +384,7 @@ private fun FriendsSelectionSection( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { Text( - text = "Selected Friends", + text = stringResource(R.string.event_selected_friends), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -411,7 +413,7 @@ private fun FriendsSelectionSection( ) Icon( imageVector = Icons.Default.Cancel, - contentDescription = "Remove", + contentDescription = stringResource(R.string.remove), modifier = Modifier .size(12.dp) .clickable { onRemoveFriend(friend.id) }, @@ -445,8 +447,8 @@ private fun FriendsSelectionSheet( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text(text = "Select Friends", style = DesignTheme.Typography.heading) - ButtonFactory.Compact(text = "Done", onClick = onDone) + Text(text = stringResource(R.string.event_select_friends), style = DesignTheme.Typography.heading) + ButtonFactory.Compact(text = stringResource(R.string.done), onClick = onDone) } when { @@ -463,7 +465,7 @@ private fun FriendsSelectionSheet( ) { CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) Text( - text = "Loading friends...", + text = stringResource(R.string.loading_friends), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -490,11 +492,11 @@ private fun FriendsSelectionSheet( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = "No Friends Available", + text = stringResource(R.string.event_no_friends_title), style = DesignTheme.Typography.captionSemibold, ) Text( - text = "No friends available on selected date", + text = stringResource(R.string.event_no_friends_subtitle), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/events/EventDetailView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/events/EventDetailView.kt index a02aad8..0d97157 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/events/EventDetailView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/events/EventDetailView.kt @@ -22,7 +22,6 @@ import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -37,6 +36,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -45,10 +45,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import friends.mobile.R import androidx.compose.material3.MaterialTheme import friends.mobile.designsystem.theme.DesignTheme +import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.components.LoadingView import friends.mobile.feature.events.domain.model.Event import friends.mobile.feature.events.domain.model.EventParticipant import friends.mobile.feature.events.domain.model.ParticipationStatus +import friends.mobile.feature.events.presentation.eventdetail.EventDetailEvent import friends.mobile.feature.events.presentation.eventdetail.EventDetailViewModel import friends.mobile.feature.events.presentation.eventdetail.EventDetailViewState @@ -71,7 +73,7 @@ fun EventDetailView( IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.primary ) } @@ -83,30 +85,22 @@ fun EventDetailView( is EventDetailViewState.Loading -> { LoadingView( modifier = Modifier.fillMaxSize().padding(innerPadding), - message = "Loading event details..." + message = stringResource(R.string.event_detail_loading) ) } is EventDetailViewState.Error -> { - Column( + Box( modifier = Modifier .fillMaxSize() .padding(innerPadding) .padding(DesignTheme.Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + contentAlignment = Alignment.Center, ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - Text( - text = currentState.message, - color = MaterialTheme.colorScheme.error, - style = DesignTheme.Typography.body, - modifier = Modifier.padding(top = DesignTheme.Spacing.md) + ErrorBanner( + message = currentState.message, + modifier = Modifier.fillMaxWidth(), + onRetry = { viewModel.obtainEvent(EventDetailEvent.OnRefresh) } ) } } @@ -159,15 +153,15 @@ private fun EventDetailContent( Column(verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md)) { DetailRowWithIcon( icon = Icons.Default.CalendarToday, - label = "Date", + label = stringResource(R.string.label_date), value = event.date ) event.time?.let { - DetailRowWithIcon(icon = Icons.Default.Schedule, label = "Time", value = it) + DetailRowWithIcon(icon = Icons.Default.Schedule, label = stringResource(R.string.label_time), value = it) } event.location?.let { if (it.isNotEmpty()) { - DetailRowWithIcon(icon = Icons.Default.LocationOn, label = "Location", value = it) + DetailRowWithIcon(icon = Icons.Default.LocationOn, label = stringResource(R.string.label_location), value = it) } } } @@ -187,7 +181,7 @@ private fun EventDetailContent( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.sm) ) { Text( - text = "Description", + text = stringResource(R.string.label_description), style = DesignTheme.Typography.button, color = MaterialTheme.colorScheme.onSurface ) @@ -204,13 +198,13 @@ private fun EventDetailContent( item { Column(verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md)) { Text( - text = "Participants", + text = stringResource(R.string.event_detail_participants), style = DesignTheme.Typography.button, color = MaterialTheme.colorScheme.onSurface ) if (event.participants.isEmpty()) { Text( - text = "No participants yet", + text = stringResource(R.string.event_detail_no_participants), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(DesignTheme.Spacing.md) @@ -368,7 +362,7 @@ private fun ParticipantAvatarRow( ) Text( text = when (participant.status) { - ParticipationStatus.INVITED -> "Pending" + ParticipationStatus.INVITED -> stringResource(R.string.pending_status) else -> participant.status.name.lowercase().replaceFirstChar { it.uppercase() } }, style = DesignTheme.Typography.bodySmallest, diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventDetailContent.kt b/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventDetailContent.kt index 9bd9449..60bdfab 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventDetailContent.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventDetailContent.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.LoadingView import friends.mobile.designsystem.theme.DesignTheme @@ -50,7 +52,7 @@ fun PendingEventDetailContent( isLoading -> { LoadingView( modifier = Modifier.fillMaxWidth(), - message = "Loading event details..." + message = stringResource(R.string.event_detail_loading) ) } errorMessage != null -> { @@ -83,7 +85,7 @@ private fun ErrorContent(message: String) { tint = MaterialTheme.colorScheme.error, ) Text( - text = "Something went wrong", + text = stringResource(R.string.pending_error), style = DesignTheme.Typography.heading, ) Text( @@ -174,19 +176,19 @@ private fun EventDetailsSection(event: Event) { verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md), ) { Text( - text = "EVENT DETAILS", + text = stringResource(R.string.pending_section_event_details), style = DesignTheme.Typography.bodySmallest, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - DetailRow(icon = Icons.Default.CalendarToday, label = "Date", value = event.date) + DetailRow(icon = Icons.Default.CalendarToday, label = stringResource(R.string.label_date), value = event.date) if (!event.time.isNullOrEmpty()) { - DetailRow(icon = Icons.Default.Schedule, label = "Time", value = event.time!!) + DetailRow(icon = Icons.Default.Schedule, label = stringResource(R.string.label_time), value = event.time!!) } if (!event.location.isNullOrEmpty()) { - DetailRow(icon = Icons.Default.LocationOn, label = "Location", value = event.location!!) + DetailRow(icon = Icons.Default.LocationOn, label = stringResource(R.string.label_location), value = event.location!!) } } } @@ -229,7 +231,7 @@ private fun ParticipantsSection(participants: List) { verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md), ) { Text( - text = "PARTICIPANTS", + text = stringResource(R.string.pending_section_participants), style = DesignTheme.Typography.bodySmallest, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -285,7 +287,7 @@ private fun ParticipantCard(participant: EventParticipant) { ) Text( text = when (participant.status) { - ParticipationStatus.INVITED -> "Pending" + ParticipationStatus.INVITED -> stringResource(R.string.pending_status) else -> participant.status.name.lowercase().replaceFirstChar { it.uppercase() } }, style = DesignTheme.Typography.bodySmallest, @@ -308,11 +310,11 @@ private fun ActionsSection( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md), ) { ButtonFactory.Primary( - text = "Accept Invitation", + text = stringResource(R.string.pending_accept), onClick = { onAccept(eventId) }, ) ButtonFactory.Secondary( - text = "Decline Invitation", + text = stringResource(R.string.pending_decline), onClick = { onDecline(eventId) }, ) } diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventListView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventListView.kt index 32e4343..43086f3 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventListView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/events/PendingEventListView.kt @@ -43,6 +43,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.components.LoadingView @@ -91,12 +93,12 @@ fun PendingEventListView( Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text("Pending Invitations") }, + title = { Text(stringResource(R.string.pending_title)) }, navigationIcon = { IconButton(onClick = { viewModel.obtainEvent(PendingEvent.OnBackClick) }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.primary ) } @@ -109,24 +111,21 @@ fun PendingEventListView( isLoading && pendingEvents.isEmpty() -> { LoadingView( modifier = Modifier.fillMaxSize().padding(innerPadding), - message = "Loading invitations..." + message = stringResource(R.string.pending_loading) ) } errorMessage != null && pendingEvents.isEmpty() -> { - Column( + Box( modifier = Modifier .fillMaxSize() .padding(innerPadding) .padding(DesignTheme.Spacing.xl), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.lg), + contentAlignment = Alignment.TopCenter, ) { - ErrorBanner(message = errorMessage) - ButtonFactory.Primary( - text = "Retry", - onClick = { viewModel.obtainEvent(PendingEvent.OnRefresh) }, - modifier = Modifier.fillMaxWidth() + ErrorBanner( + message = errorMessage, + onRetry = { viewModel.obtainEvent(PendingEvent.OnRefresh) } ) } } @@ -155,12 +154,12 @@ fun PendingEventListView( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.xs) ) { Text( - text = "No Pending Invitations", + text = stringResource(R.string.pending_no_invitations), style = DesignTheme.Typography.captionSemibold, color = MaterialTheme.colorScheme.onSurface ) Text( - text = "You're all caught up!", + text = stringResource(R.string.pending_all_caught_up), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -189,7 +188,7 @@ fun PendingEventListView( modifier = Modifier.fillMaxWidth().padding(DesignTheme.Spacing.lg), contentAlignment = Alignment.Center, ) { - LoadingView(modifier = Modifier.fillMaxWidth(), message = "Refreshing...") + LoadingView(modifier = Modifier.fillMaxWidth(), message = stringResource(R.string.loading_refreshing)) } } } @@ -296,7 +295,7 @@ private fun PendingEventCard( tint = MaterialTheme.colorScheme.tertiary, ) Text( - text = "Awaiting your response", + text = stringResource(R.string.pending_awaiting), style = DesignTheme.Typography.bodySmallest, color = MaterialTheme.colorScheme.tertiary, ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendProfileScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendProfileScreen.kt index d4d1a61..22d92c7 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendProfileScreen.kt @@ -31,6 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.Dimension @@ -120,7 +122,8 @@ fun FriendProfileScreenContent( message = currentState.message, modifier = Modifier .fillMaxWidth() - .padding(DesignTheme.Spacing.lg) + .padding(DesignTheme.Spacing.lg), + onRetry = { viewModel.obtainEvent(FriendProfileEvent.ScreenOpened(userId = userId)) } ) is FriendProfileViewState.Content -> { FriendProfileContent( @@ -189,7 +192,7 @@ private fun FriendProfileContent( if (content.status == FriendshipStatus.FRIENDS) { Text( - text = "Wish Places", + text = stringResource(R.string.wish_places_section), style = DesignTheme.Typography.heading, modifier = Modifier.align(Alignment.Start) ) @@ -213,7 +216,7 @@ private fun ActionButtons( ) { when (status) { FriendshipStatus.NONE -> ButtonFactory.Primary( - text = "Add Friend", + text = stringResource(R.string.friends_add), onClick = { callbacks.onSendRequest(user.id) }, modifier = Modifier.fillMaxWidth(), isLoading = isLoading, @@ -221,7 +224,7 @@ private fun ActionButtons( ) FriendshipStatus.REQUESTING -> ButtonFactory.Disabled( - text = "Request Sent", + text = stringResource(R.string.friends_request_sent), modifier = Modifier.fillMaxWidth(), icon = { Icon( @@ -238,14 +241,14 @@ private fun ActionButtons( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { ButtonFactory.Primary( - text = "Accept", + text = stringResource(R.string.friends_accept), onClick = { callbacks.onAcceptRequest(user.id) }, modifier = Modifier.fillMaxWidth(), isLoading = isLoading, isEnabled = !isLoading ) ButtonFactory.Secondary( - text = "Decline", + text = stringResource(R.string.friends_decline), onClick = { callbacks.onRejectRequest(user.id) }, modifier = Modifier.fillMaxWidth(), isEnabled = !isLoading @@ -253,7 +256,7 @@ private fun ActionButtons( } FriendshipStatus.FRIENDS -> ButtonFactory.Destructive( - text = "Remove Friend", + text = stringResource(R.string.friends_remove), onClick = { callbacks.onRemoveFriend(user.id) }, modifier = Modifier.fillMaxWidth(), isLoading = isLoading diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsComponents.kt b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsComponents.kt index 42c1aa0..1f5be44 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsComponents.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsComponents.kt @@ -35,6 +35,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.designsystem.components.IndicatorFactory import friends.mobile.feature.friends.domain.model.User @@ -162,12 +164,12 @@ fun EmptyStateView( ) { if (isSearchEmpty && searchText.isNotEmpty()) { Text( - text = "No users found", + text = stringResource(R.string.friends_no_users_found), style = DesignTheme.Typography.captionSemibold, color = MaterialTheme.colorScheme.onSurface, ) Text( - text = "Try searching with a different name", + text = stringResource(R.string.friends_try_different_name), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -187,16 +189,18 @@ fun EmptyStateView( } } +@Composable private fun emptyStateTitle(tab: RequestTab): String = when (tab) { - RequestTab.FRIENDS -> "No friends yet" - RequestTab.INCOMING -> "No incoming requests" - RequestTab.OUTGOING -> "No outgoing requests" + RequestTab.FRIENDS -> stringResource(R.string.friends_no_friends) + RequestTab.INCOMING -> stringResource(R.string.friends_no_incoming) + RequestTab.OUTGOING -> stringResource(R.string.friends_no_outgoing) } +@Composable private fun emptyStateSubtitle(tab: RequestTab): String = when (tab) { - RequestTab.FRIENDS -> "Search and send friend requests to get started" - RequestTab.INCOMING -> "You will see incoming requests here" - RequestTab.OUTGOING -> "Requests you have sent will appear here" + RequestTab.FRIENDS -> stringResource(R.string.friends_hint_search_start) + RequestTab.INCOMING -> stringResource(R.string.friends_hint_incoming) + RequestTab.OUTGOING -> stringResource(R.string.friends_hint_outgoing) } @Composable @@ -241,9 +245,9 @@ fun TabSelector( ) { Text( text = when (tab) { - RequestTab.FRIENDS -> "Friends" - RequestTab.INCOMING -> "Incoming" - RequestTab.OUTGOING -> "Outgoing" + RequestTab.FRIENDS -> stringResource(R.string.friends_tab_friends) + RequestTab.INCOMING -> stringResource(R.string.friends_tab_incoming) + RequestTab.OUTGOING -> stringResource(R.string.friends_tab_outgoing) }, style = if (isSelected) DesignTheme.Typography.bodySmall.copy(fontWeight = FontWeight.SemiBold) else DesignTheme.Typography.bodySmall, diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsScreen.kt index 6d94d36..c81c56c 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/friends/FriendsScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.components.LoadingView @@ -74,7 +76,8 @@ fun FriendsScreen( ) { ErrorBanner( message = currentState.message, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + onRetry = { viewModel.obtainEvent(FriendsEvent.ReloadCurrentTab) } ) } } @@ -128,7 +131,7 @@ private fun FriendsContent( CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) Spacer(modifier = Modifier.height(DesignTheme.Spacing.lg)) Text( - text = "Searching...", + text = stringResource(R.string.loading_searching), style = DesignTheme.Typography.body, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/main/MainView.kt b/composeApp/src/androidMain/kotlin/friends/mobile/main/MainView.kt index 026bc79..f92ae73 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/main/MainView.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/main/MainView.kt @@ -60,7 +60,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory +import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.feature.events.domain.model.Event import friends.mobile.feature.main.presentation.MainViewAction as MainEventAction @@ -124,12 +127,12 @@ fun MainView( ) { Column(verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.xs)) { Text( - text = "My Events", + text = stringResource(R.string.main_my_events), style = DesignTheme.Typography.heading, color = MaterialTheme.colorScheme.onSurface ) Text( - text = "Manage active and upcoming plans", + text = stringResource(R.string.main_manage_events), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -143,7 +146,7 @@ fun MainView( IconButton(onClick = onPendingEventsClick) { Icon( imageVector = Icons.Default.Notifications, - contentDescription = "Pending Invitations", + contentDescription = stringResource(R.string.main_pending_invitations), tint = primaryColor ) } @@ -163,31 +166,15 @@ fun MainView( } errorMessage != null -> { - Column( + Box( modifier = Modifier .fillMaxSize() .padding(DesignTheme.Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + contentAlignment = Alignment.Center ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - Spacer(Modifier.height(DesignTheme.Spacing.lg)) - Text( - text = errorMessage, - style = DesignTheme.Typography.body, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(DesignTheme.Spacing.lg)) - ButtonFactory.Primary( - text = "Retry", - onClick = { viewModel.obtainEvent(MainEventAction.OnRefresh) }, - modifier = Modifier.fillMaxWidth() + ErrorBanner( + message = errorMessage, + onRetry = { viewModel.obtainEvent(MainEventAction.OnRefresh) } ) } } @@ -206,13 +193,13 @@ fun MainView( ) Spacer(Modifier.height(DesignTheme.Spacing.lg)) Text( - text = "No events yet", + text = stringResource(R.string.main_no_events), style = DesignTheme.Typography.captionSemibold, color = MaterialTheme.colorScheme.onSurface ) Spacer(Modifier.height(DesignTheme.Spacing.sm)) Text( - text = "Tap + to create your first event", + text = stringResource(R.string.main_create_first_event), style = DesignTheme.Typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -238,7 +225,7 @@ fun MainView( if (activeEvents.isNotEmpty()) { item(key = "active_header") { EventSectionHeader( - title = "Active", + title = stringResource(R.string.main_section_active), count = activeEvents.size, tint = tertiaryColor ) @@ -260,7 +247,7 @@ fun MainView( } item(key = "pending_header") { EventSectionHeader( - title = "Pending", + title = stringResource(R.string.main_section_pending), count = pendingEvents.size, tint = pendingColor ) @@ -292,7 +279,7 @@ fun MainView( ) { Icon( imageVector = Icons.Default.Add, - contentDescription = "Create Event" + contentDescription = stringResource(R.string.main_create_event_fab) ) } } @@ -304,10 +291,10 @@ fun MainView( Button(onClick = { showDatePickerDialog = false showTimePickerDialog = true - }) { Text("Next") } + }) { Text(stringResource(R.string.next)) } }, dismissButton = { - Button(onClick = { showDatePickerDialog = false }) { Text("Cancel") } + Button(onClick = { showDatePickerDialog = false }) { Text(stringResource(R.string.cancel)) } }, ) { DatePicker(state = datePickerState) @@ -351,7 +338,7 @@ fun MainView( if (isCheckingAvailability) { CircularProgressIndicator(modifier = Modifier.size(20.dp)) } else { - Text("Create") + Text(stringResource(R.string.create)) } } }, @@ -359,7 +346,7 @@ fun MainView( Button( onClick = { showTimePickerDialog = false }, enabled = !isCheckingAvailability, - ) { Text("Cancel") } + ) { Text(stringResource(R.string.cancel)) } }, timePickerState = timePickerState, ) @@ -368,9 +355,9 @@ fun MainView( if (showBusyAlert) { androidx.compose.material3.AlertDialog( onDismissRequest = { showBusyAlert = false }, - title = { Text("You are busy on this day") }, + title = { Text(stringResource(R.string.main_busy_day_title)) }, confirmButton = { - Button(onClick = { showBusyAlert = false }) { Text("OK") } + Button(onClick = { showBusyAlert = false }) { Text(stringResource(R.string.ok)) } }, ) } @@ -410,7 +397,7 @@ private fun EventCard( ) { val tintColor = if (isPending) Color(0xFFFF9800) else MaterialTheme.colorScheme.tertiary val badgeIcon = if (isPending) Icons.Default.Schedule else Icons.Default.CheckCircle - val badgeText = if (isPending) "Pending" else "Active" + val badgeText = if (isPending) stringResource(R.string.main_section_pending) else stringResource(R.string.main_section_active) Column( modifier = Modifier @@ -503,7 +490,7 @@ private fun DateTimePickerDialog( ) { androidx.compose.material3.AlertDialog( onDismissRequest = onDismissRequest, - title = { Text("Select Time") }, + title = { Text(stringResource(R.string.main_select_time)) }, text = { TimePicker( state = timePickerState, diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt index 1bd9673..e31213f 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/profile/EditProfileScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.components.FormTextField @@ -63,12 +65,12 @@ fun EditProfileScreen( snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { CenterAlignedTopAppBar( - title = { Text("Edit Profile") }, + title = { Text(stringResource(R.string.profile_edit)) }, navigationIcon = { IconButton(onClick = { viewModel.obtainEvent(EditProfileEvent.OnBackClick) }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.primary ) } @@ -96,26 +98,26 @@ fun EditProfileScreen( FormTextField( value = currentState.username, onValueChange = { viewModel.obtainEvent(EditProfileEvent.OnUsernameChanged(it)) }, - placeholder = "Username", + placeholder = stringResource(R.string.label_username), modifier = Modifier.fillMaxWidth() ) FormTextField( value = currentState.bio, onValueChange = { viewModel.obtainEvent(EditProfileEvent.OnBioChanged(it)) }, - placeholder = "Bio", + placeholder = stringResource(R.string.label_bio), modifier = Modifier.fillMaxWidth() ) FormTextField( value = currentState.avatarUrl, onValueChange = { viewModel.obtainEvent(EditProfileEvent.OnAvatarUrlChanged(it)) }, - placeholder = "Avatar URL", + placeholder = stringResource(R.string.label_avatar_url), modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.weight(1f)) ButtonFactory.Primary( - text = "Save", + text = stringResource(R.string.save), onClick = { viewModel.obtainEvent(EditProfileEvent.OnSaveClick) }, modifier = Modifier.fillMaxWidth(), isLoading = currentState.isSaving, @@ -134,7 +136,7 @@ fun EditProfileScreen( ) { ErrorBanner(message = currentState.message) ButtonFactory.Primary( - text = "Go Back", + text = stringResource(R.string.go_back), onClick = onBack, modifier = Modifier .fillMaxWidth() diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt b/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt index a7300b6..a6a48e6 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/profile/ProfileScreen.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import friends.mobile.calendar.BusyDaysView +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.Dimension import friends.mobile.designsystem.components.ErrorBanner @@ -135,14 +137,14 @@ private fun ProfileContent( verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { ButtonFactory.Primary( - text = "Edit Profile", + text = stringResource(R.string.profile_edit), onClick = onEditClick, modifier = Modifier.fillMaxWidth(), isEnabled = !isLoggingOut, buttonHeight = 44.dp ) ButtonFactory.Destructive( - text = "Log Out", + text = stringResource(R.string.profile_log_out), onClick = onLogoutClick, modifier = Modifier.fillMaxWidth(), isLoading = isLoggingOut, @@ -164,13 +166,9 @@ private fun ProfileErrorContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - ErrorBanner(message = message) - ButtonFactory.Primary( - text = "Retry", - onClick = onRetry, - modifier = Modifier - .fillMaxWidth() - .padding(top = DesignTheme.Spacing.lg) + ErrorBanner( + message = message, + onRetry = onRetry ) } } diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesComponents.kt b/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesComponents.kt index e5c76ab..665adc6 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesComponents.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesComponents.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.res.stringResource +import friends.mobile.R import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.designsystem.components.ButtonFactory import friends.mobile.designsystem.components.FormTextField @@ -101,7 +103,7 @@ fun WishPlaceItem( if (isDismissing) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete", + contentDescription = stringResource(R.string.delete), tint = Color.White ) } @@ -200,37 +202,37 @@ fun CreateWishPlaceBottomSheet( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(DesignTheme.Spacing.md) ) { - Text("Add Wish Place", style = DesignTheme.Typography.heading) + Text(stringResource(R.string.wish_places_add), style = DesignTheme.Typography.heading) FormTextField( value = title, onValueChange = { title = it }, - placeholder = "Title", + placeholder = stringResource(R.string.label_title), modifier = Modifier.fillMaxWidth() ) FormTextField( value = location, onValueChange = { location = it }, - placeholder = "Location", + placeholder = stringResource(R.string.label_location), modifier = Modifier.fillMaxWidth() ) FormTextField( value = description, onValueChange = { description = it }, - placeholder = "Description", + placeholder = stringResource(R.string.label_description), modifier = Modifier.fillMaxWidth() ) FormTextField( value = link, onValueChange = { link = it }, - placeholder = "Link", + placeholder = stringResource(R.string.label_link), modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(DesignTheme.Spacing.md)) ButtonFactory.Primary( - text = "Create", + text = stringResource(R.string.create), onClick = { onCreate( title, @@ -285,7 +287,7 @@ fun WishPlaceDetailBottomSheet(place: WishPlace, onDismiss: () -> Unit) { ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Close", + contentDescription = stringResource(R.string.close), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp) ) @@ -350,7 +352,7 @@ fun WishPlaceDetailBottomSheet(place: WishPlace, onDismiss: () -> Unit) { ) Spacer(modifier = Modifier.width(DesignTheme.Spacing.xs)) Text( - "Link", + stringResource(R.string.label_link), style = DesignTheme.Typography.bodySmall, color = Color.White ) @@ -361,7 +363,7 @@ fun WishPlaceDetailBottomSheet(place: WishPlace, onDismiss: () -> Unit) { } Text( - text = "Added ${place.createdAt.take(10)}", + text = stringResource(R.string.wish_places_added_date, place.createdAt.take(10)), style = DesignTheme.Typography.bodySmallest, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesSection.kt b/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesSection.kt index 0bfe681..06ec70c 100644 --- a/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesSection.kt +++ b/composeApp/src/androidMain/kotlin/friends/mobile/wishplaces/WishPlacesSection.kt @@ -26,6 +26,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.res.stringResource +import friends.mobile.R +import friends.mobile.designsystem.components.ErrorBanner import friends.mobile.designsystem.theme.DesignTheme import friends.mobile.feature.wishplaces.domain.model.WishPlace import friends.mobile.feature.wishplaces.presentation.WishPlacesAction @@ -69,7 +72,7 @@ fun WishPlacesSection( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "My Wish Places", + text = stringResource(R.string.wish_places_my), style = DesignTheme.Typography.heading, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f) @@ -80,7 +83,7 @@ fun WishPlacesSection( ) { Icon( imageVector = Icons.Default.AddCircle, - contentDescription = "Add Wish Place", + contentDescription = stringResource(R.string.wish_places_add), modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary ) @@ -99,17 +102,17 @@ fun WishPlacesSection( } is WishPlacesViewState.Error -> { - Text( - text = current.message, - style = DesignTheme.Typography.bodySmall, - color = MaterialTheme.colorScheme.error + ErrorBanner( + message = current.message, + modifier = Modifier.fillMaxWidth(), + onRetry = { viewModel.obtainEvent(WishPlacesEvent.LoadPlaces(userId)) } ) } is WishPlacesViewState.Content -> { if (current.places.isEmpty()) { Text( - text = "No wish places yet", + text = stringResource(R.string.wish_places_empty), style = DesignTheme.Typography.body, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth(), diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml index 122aff1..a28f8a0 100644 --- a/composeApp/src/androidMain/res/values/strings.xml +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -1,3 +1,139 @@ Mobile + + + Back + Retry + Cancel + Create + Done + Save + OK + Next + Remove + Delete + Close + Go Back + + + Title + Description + Location + Date + Time + Link + Username + Password + Bio + Avatar URL + + Clear + + + Loading… + Loading friends… + Removing… + Searching… + Refreshing… + + + Home + Friends + Profile + Archive + + + Login + Sign up + Create Account + Don\'t have an account? + Already have an account? + + + My Events + Manage active and upcoming plans + Pending Invitations + No events yet + Tap + to create your first event + Active + Pending + You are busy on this day + Select Time + Create Event + + + Archive + No archived events + Completed + + + Create Event + Event title + Event description + Event location + Event Date + Select Friends + Selected Friends + %d selected + Loading available friends… + No Friends Available + No friends available on selected date + + + Loading event details… + No participants yet + Participants + + + Pending Invitations + Loading invitations… + No Pending Invitations + You\'re all caught up! + Awaiting your response + Something went wrong + EVENT DETAILS + PARTICIPANTS + Pending + Accept Invitation + Decline Invitation + + + Search users + Add Friend + Request Sent + Accept + Decline + Remove Friend + Friends + Incoming + Outgoing + No users found + Try searching with a different name + No friends yet + No incoming requests + No outgoing requests + Search and send friend requests to get started + You will see incoming requests here + Requests you have sent will appear here + + + Edit Profile + Log Out + + + My Wish Places + Add Wish Place + No wish places yet + Added %s + Wish Places + + + S + M + T + W + T + F + S + No busy days recorded \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b1463a..828517c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ material3 = "1.10.0-alpha05" androidx-navigation = "2.8.5" accompanist = "0.34.0" sqlite-bundled = "2.5.1" +sqldelight = "2.0.2" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -67,6 +68,9 @@ accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiper firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -78,3 +82,4 @@ kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 2c05a54..2cff330 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) + alias(libs.plugins.sqldelight) kotlin("plugin.serialization") version libs.versions.kotlin.get() } @@ -36,6 +37,7 @@ kotlin { implementation(libs.koin.core) implementation(libs.multiplatform.settings) implementation(libs.kotlinx.datetime) + implementation(libs.sqldelight.coroutines) } commonTest.dependencies { implementation(libs.kotlin.test) @@ -45,9 +47,11 @@ kotlin { implementation(libs.koin.android) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) + implementation(libs.sqldelight.android.driver) } iosMain.dependencies { implementation(libs.ktor.client.darwin) + implementation(libs.sqldelight.native.driver) } } } @@ -56,6 +60,14 @@ dependencies { "androidMainImplementation"(platform(libs.firebase.bom)) } +sqldelight { + databases { + create("AppDatabase") { + packageName.set("friends.mobile.core.db") + } + } +} + android { namespace = "friends.mobile.shared" compileSdk = libs.versions.android.compileSdk.get().toInt() diff --git a/shared/src/androidMain/kotlin/friends/mobile/core/di/PlatformModule.android.kt b/shared/src/androidMain/kotlin/friends/mobile/core/di/PlatformModule.android.kt index 8b7da73..7a798a2 100644 --- a/shared/src/androidMain/kotlin/friends/mobile/core/di/PlatformModule.android.kt +++ b/shared/src/androidMain/kotlin/friends/mobile/core/di/PlatformModule.android.kt @@ -1,10 +1,13 @@ package friends.mobile.core.di import android.content.Context +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.android.AndroidSqliteDriver import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings import friends.mobile.core.analytics.AnalyticsService import friends.mobile.core.analytics.FirebaseAnalyticsServiceImpl +import friends.mobile.core.db.AppDatabase import org.koin.android.ext.koin.androidContext import org.koin.core.module.Module import org.koin.core.qualifier.named @@ -20,5 +23,12 @@ actual val platformModule: Module = module { ) ) } + single { + AndroidSqliteDriver( + schema = AppDatabase.Schema, + context = androidContext(), + name = get(named()), + ) + } single { FirebaseAnalyticsServiceImpl() } } diff --git a/shared/src/commonMain/kotlin/friends/mobile/core/db/ProfileCacheStorage.kt b/shared/src/commonMain/kotlin/friends/mobile/core/db/ProfileCacheStorage.kt index 5be0b4f..6fe2246 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/core/db/ProfileCacheStorage.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/core/db/ProfileCacheStorage.kt @@ -1,12 +1,8 @@ package friends.mobile.core.db -import com.russhwolf.settings.Settings import friends.mobile.feature.profile.domain.model.Profile -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -private const val PROFILE_CACHE_KEY = "profile_cache" +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext interface ProfileCacheStorage { suspend fun getProfile(): Profile? @@ -14,25 +10,31 @@ interface ProfileCacheStorage { suspend fun clearProfile() } -class ProfileCacheStorageImpl( - private val settings: Settings, - private val json: Json, +internal class ProfileCacheStorageImpl( + private val database: AppDatabase, ) : ProfileCacheStorage { - override suspend fun getProfile(): Profile? { - val cached = settings.getStringOrNull(PROFILE_CACHE_KEY) ?: return null - return try { - json.decodeFromString(cached) - } catch (e: Exception) { - null + + override suspend fun getProfile(): Profile? = withContext(Dispatchers.Default) { + database.profileQueries.getProfile().executeAsOneOrNull()?.let { + Profile( + id = it.id, + username = it.username, + avatarUrl = it.avatar_url, + bio = it.bio, + ) } } - override suspend fun saveProfile(profile: Profile) { - val serialized = json.encodeToString(profile) - settings.putString(PROFILE_CACHE_KEY, serialized) + override suspend fun saveProfile(profile: Profile) = withContext(Dispatchers.Default) { + database.profileQueries.upsertProfile( + id = profile.id, + username = profile.username, + avatarUrl = profile.avatarUrl, + bio = profile.bio, + ) } - override suspend fun clearProfile() { - settings.remove(PROFILE_CACHE_KEY) + override suspend fun clearProfile() = withContext(Dispatchers.Default) { + database.profileQueries.clearProfile() } } diff --git a/shared/src/commonMain/kotlin/friends/mobile/core/db/di/DatabaseModule.kt b/shared/src/commonMain/kotlin/friends/mobile/core/db/di/DatabaseModule.kt index 907a135..f03cdb5 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/core/db/di/DatabaseModule.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/core/db/di/DatabaseModule.kt @@ -1,13 +1,16 @@ package friends.mobile.core.db.di -import com.russhwolf.settings.Settings +import app.cash.sqldelight.db.SqlDriver +import friends.mobile.core.db.AppDatabase import friends.mobile.core.db.ProfileCacheStorage import friends.mobile.core.db.ProfileCacheStorageImpl -import kotlinx.serialization.json.Json import org.koin.dsl.module val databaseModule = module { + single { + AppDatabase(driver = get()) + } single { - ProfileCacheStorageImpl(get(), get()) + ProfileCacheStorageImpl(database = get()) } } diff --git a/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootEvent.kt b/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootEvent.kt index 5876984..ec1dd35 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootEvent.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootEvent.kt @@ -5,4 +5,5 @@ import friends.mobile.feature.auth.domain.model.AuthSession sealed class RootEvent { data class OnSessionStarted(val session: AuthSession) : RootEvent() data object OnLogoutClick : RootEvent() + data object OnRetry : RootEvent() } diff --git a/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootViewModel.kt b/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootViewModel.kt index 55d4aad..440011d 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootViewModel.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/feature/auth/presentation/RootViewModel.kt @@ -24,6 +24,9 @@ class RootViewModel( is RootEvent.OnLogoutClick -> { onLogoutClick() } + is RootEvent.OnRetry -> { + loadSession() + } } } diff --git a/shared/src/commonMain/kotlin/friends/mobile/feature/main/presentation/MainViewModel.kt b/shared/src/commonMain/kotlin/friends/mobile/feature/main/presentation/MainViewModel.kt index 0a9e361..687a059 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/feature/main/presentation/MainViewModel.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/feature/main/presentation/MainViewModel.kt @@ -84,14 +84,11 @@ class MainViewModel : BaseViewModel< private fun onRefresh() { if (isLoadingInProgress) return - val currentState = viewState - if (currentState is MainViewState.Content) { - viewState = currentState.copy(isRefreshing = true) - } else if (currentState is MainViewState.Error) { - viewState = MainViewState.Content(isRefreshing = true) + if (viewState is MainViewState.Content) { + loadEvents(showLoading = false) + } else { + loadEvents(showLoading = true) } - - loadEvents(showLoading = false) } suspend fun checkAvailability( diff --git a/shared/src/commonMain/kotlin/friends/mobile/feature/profile/di/ProfileModule.kt b/shared/src/commonMain/kotlin/friends/mobile/feature/profile/di/ProfileModule.kt index e162677..4c74962 100644 --- a/shared/src/commonMain/kotlin/friends/mobile/feature/profile/di/ProfileModule.kt +++ b/shared/src/commonMain/kotlin/friends/mobile/feature/profile/di/ProfileModule.kt @@ -1,6 +1,5 @@ package friends.mobile.feature.profile.di -import friends.mobile.core.db.ProfileCacheStorage import friends.mobile.core.di.QualifierAuthClient import friends.mobile.feature.profile.data.mapper.ProfileMapper import friends.mobile.feature.profile.data.remote.ProfileApi diff --git a/shared/src/commonMain/sqldelight/friends/mobile/core/db/Profile.sq b/shared/src/commonMain/sqldelight/friends/mobile/core/db/Profile.sq new file mode 100644 index 0000000..eea611a --- /dev/null +++ b/shared/src/commonMain/sqldelight/friends/mobile/core/db/Profile.sq @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS ProfileEntity ( + id TEXT NOT NULL PRIMARY KEY, + username TEXT NOT NULL, + avatar_url TEXT, + bio TEXT +); + +getProfile: +SELECT * FROM ProfileEntity LIMIT 1; + +upsertProfile: +INSERT OR REPLACE INTO ProfileEntity(id, username, avatar_url, bio) +VALUES (:id, :username, :avatarUrl, :bio); + +clearProfile: +DELETE FROM ProfileEntity; diff --git a/shared/src/iosMain/kotlin/friends/mobile/core/di/PlatformModule.ios.kt b/shared/src/iosMain/kotlin/friends/mobile/core/di/PlatformModule.ios.kt index 89b6808..1662479 100644 --- a/shared/src/iosMain/kotlin/friends/mobile/core/di/PlatformModule.ios.kt +++ b/shared/src/iosMain/kotlin/friends/mobile/core/di/PlatformModule.ios.kt @@ -1,7 +1,10 @@ package friends.mobile.core.di +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings +import friends.mobile.core.db.AppDatabase import org.koin.core.module.Module import org.koin.core.qualifier.named import org.koin.dsl.module @@ -12,4 +15,10 @@ actual val platformModule: Module = module { name = get(named()), ) } + single { + NativeSqliteDriver( + schema = AppDatabase.Schema, + name = get(named()), + ) + } }