diff --git a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt index efcdf664..35fc7dfc 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt @@ -2,6 +2,7 @@ package com.example.speechbuddy import androidx.appcompat.app.AppCompatActivity import com.example.speechbuddy.domain.SessionManager +import com.example.speechbuddy.repository.SettingsRepository import javax.inject.Inject abstract class BaseActivity : AppCompatActivity() { @@ -9,4 +10,7 @@ abstract class BaseActivity : AppCompatActivity() { @Inject lateinit var sessionManager: SessionManager + @Inject + lateinit var settingsRepository: SettingsRepository + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt index 44dbe146..b844fec3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt @@ -1,32 +1,55 @@ package com.example.speechbuddy import android.content.Intent +import android.os.Build import android.os.Bundle import android.view.MotionEvent import android.view.inputmethod.InputMethodManager import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.core.view.WindowCompat +import androidx.lifecycle.Observer import com.example.speechbuddy.compose.SpeechBuddyHome import com.example.speechbuddy.ui.SpeechBuddyTheme +import com.example.speechbuddy.viewmodel.DisplaySettingsViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class HomeActivity : BaseActivity() { + private val displaySettingsViewModel: DisplaySettingsViewModel by viewModels() + + @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) // Displaying edge-to-edge WindowCompat.setDecorFitsSystemWindows(window, false) + val isBeingReloadedForDarkModeChange = intent.getBooleanExtra("isBeingReloadedForDarkModeChange", false) + setContent { - SpeechBuddyTheme { - SpeechBuddyHome() + SpeechBuddyTheme( + darkTheme = getDarkMode() + ) { + SpeechBuddyHome(getInitialPage(), isBeingReloadedForDarkModeChange) } } subscribeObservers() + + val previousDarkMode = getDarkMode() + + val darkModeObserver = Observer { darkMode -> + if (darkMode != previousDarkMode) { + recreateHomeActivity() + } + } + + settingsRepository.darkModeLiveData.observeForever(darkModeObserver) + } private fun subscribeObservers() { @@ -41,6 +64,21 @@ class HomeActivity : BaseActivity() { finish() } + private fun recreateHomeActivity() { + val intent = Intent(this, HomeActivity::class.java) + intent.putExtra("isBeingReloadedForDarkModeChange", true) + startActivity(intent) + finish() + } + + private fun getDarkMode(): Boolean { + return displaySettingsViewModel.getDarkMode() + } + + private fun getInitialPage(): Boolean { + return displaySettingsViewModel.getInitialPage() + } + // hides keyboard override fun dispatchTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_DOWN) { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt index 787fd2dc..f5eca6c2 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt @@ -1,5 +1,7 @@ package com.example.speechbuddy.compose +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height @@ -36,9 +38,13 @@ data class BottomNavItem( val iconResId: Int ) +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SpeechBuddyHome() { +fun SpeechBuddyHome( + initialPage: Boolean, + isBeingReloadedForDarkModeChange: Boolean +) { val navController = rememberNavController() val navItems = listOf( BottomNavItem( @@ -77,7 +83,9 @@ fun SpeechBuddyHome() { ) { paddingValues -> SpeechBuddyHomeNavHost( navController = navController, - bottomPaddingValues = paddingValues + bottomPaddingValues = paddingValues, + initialPage = initialPage, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } } @@ -117,12 +125,24 @@ private fun BottomNavigationBar( } } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun SpeechBuddyHomeNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + initialPage: Boolean, + isBeingReloadedForDarkModeChange: Boolean ) { - NavHost(navController = navController, startDestination = "symbol_selection") { + val startDestination = if (isBeingReloadedForDarkModeChange) { + "settings" + } else if (initialPage) { + "symbol_selection" + } else { + "text_to_speech" + } + + + NavHost(navController = navController, startDestination = startDestination) { composable("symbol_selection") { SymbolSelectionScreen( bottomPaddingValues = bottomPaddingValues @@ -140,7 +160,8 @@ private fun SpeechBuddyHomeNavHost( } composable("settings") { SettingsScreen( - bottomPaddingValues = bottomPaddingValues + bottomPaddingValues = bottomPaddingValues, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt index 1eca7308..59d5a0dc 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt @@ -1,5 +1,8 @@ package com.example.speechbuddy.compose.settings +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -7,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -14,6 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -26,6 +32,7 @@ import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.ui.models.AccountSettingsAlert import com.example.speechbuddy.viewmodel.AccountSettingsViewModel +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountSettings( @@ -92,7 +99,7 @@ fun AccountSettings( ) { ButtonUi( text = stringResource(id = R.string.logout), - onClick = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) } + onClick = { viewModel.showAlert(AccountSettingsAlert.BACKUP) } ) ButtonUi( @@ -108,6 +115,36 @@ fun AccountSettings( uiState.alert?.let { alert -> when (alert) { + AccountSettingsAlert.BACKUP -> { + AlertDialogUi( + title = stringResource(id = R.string.logout), + text = stringResource(id = R.string.logout_backup), + dismissButtonText = stringResource(id = R.string.logout), + confirmButtonText = stringResource(id = R.string.backup), + onDismiss = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) }, + onConfirm = { viewModel.backup() }, + onDismissRequest = { viewModel.hideAlert() } + ) + } + + AccountSettingsAlert.LOADING -> { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + ) + } + + AccountSettingsAlert.BACKUP_SUCCESS -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.backup_success), + Toast.LENGTH_SHORT + ).show() + viewModel.showAlert(AccountSettingsAlert.LOGOUT) + } + + AccountSettingsAlert.LOGOUT -> { AlertDialogUi( title = stringResource(id = R.string.logout), @@ -144,14 +181,12 @@ fun AccountSettings( } AccountSettingsAlert.CONNECTION -> { - AlertDialogUi( - title = stringResource(id = R.string.no_connection), - text = stringResource(id = R.string.no_connection_warning), - dismissButtonText = stringResource(id = R.string.cancel), - confirmButtonText = stringResource(id = R.string.confirm), - onDismiss = { viewModel.hideAlert() }, - onConfirm = { viewModel.hideAlert() } - ) + viewModel.hideAlert() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.connection_error), + Toast.LENGTH_SHORT + ).show() } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt index f0bbd1a2..c88eadc8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt @@ -1,5 +1,8 @@ package com.example.speechbuddy.compose.settings +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -8,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -15,7 +20,9 @@ import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -23,8 +30,10 @@ import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.ButtonUi import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.compose.utils.TitleUi +import com.example.speechbuddy.ui.models.BackupSettingsAlert import com.example.speechbuddy.viewmodel.BackupSettingsViewModel +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable fun BackupSettings( @@ -34,6 +43,7 @@ fun BackupSettings( viewModel: BackupSettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + val loading by viewModel.loading.observeAsState() Surface( modifier = modifier.fillMaxSize() @@ -89,4 +99,36 @@ fun BackupSettings( } } } + + uiState.alert.let { alert -> + when (alert) { + BackupSettingsAlert.SUCCESS -> { + viewModel.toastDisplayed() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.backup_success), + Toast.LENGTH_SHORT + ).show() + } + + BackupSettingsAlert.CONNECTION -> { + viewModel.toastDisplayed() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.connection_error), + Toast.LENGTH_SHORT + ).show() + } + + else -> {} + } + } + + if (loading == true) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + ) + } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt new file mode 100644 index 00000000..c2b8ca20 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt @@ -0,0 +1,71 @@ +package com.example.speechbuddy.compose.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.speechbuddy.R +import com.example.speechbuddy.compose.utils.TitleUi +import com.example.speechbuddy.compose.utils.TopAppBarUi +import com.example.speechbuddy.ui.SpeechBuddyTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Copyright( + modifier: Modifier = Modifier, + onBackClick: () -> Unit, + bottomPaddingValues: PaddingValues +) { + Surface( + modifier = modifier.fillMaxSize() + ) { + Scaffold( + topBar = { + TopAppBarUi( + title = stringResource(id = R.string.settings), + onBackClick = onBackClick, + isBackClickEnabled = true + ) + } + ) { topPaddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = topPaddingValues.calculateTopPadding(), + bottom = bottomPaddingValues.calculateBottomPadding() + ) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.copyright_info)) + + Spacer(modifier = modifier.height(20.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(R.string.copyright), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt index 5f9adcd9..7ed4d248 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt @@ -3,6 +3,7 @@ package com.example.speechbuddy.compose.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding @@ -60,6 +61,8 @@ fun GuestSettings( onClick = { viewModel.exitGuestMode() }, modifier = Modifier.offset(y = 240.dp) ) + + Spacer(modifier = Modifier.padding(70.dp)) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt index 2ad4cfb2..2c170644 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt @@ -80,6 +80,9 @@ fun MainSettings( SettingsTextButton(text = stringResource(id = R.string.developers_info), onClick = { navController.navigate("developers") }) + + SettingsTextButton(text = stringResource(id = R.string.copyright_info), + onClick = { navController.navigate("copyright") }) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt index 54d9c206..0e3264e4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt @@ -1,31 +1,45 @@ package com.example.speechbuddy.compose.settings +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +@RequiresApi(Build.VERSION_CODES.O) @Composable fun SettingsScreen( - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + isBeingReloadedForDarkModeChange: Boolean ) { val navController = rememberNavController() SettingsScreenNavHost( navController = navController, - bottomPaddingValues = bottomPaddingValues + bottomPaddingValues = bottomPaddingValues, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun SettingsScreenNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + isBeingReloadedForDarkModeChange: Boolean ) { + val flag = remember{ mutableStateOf(false) } + val startDestination = if (isBeingReloadedForDarkModeChange && !flag.value) "display" else "main" + if (isBeingReloadedForDarkModeChange && !flag.value) { + flag.value = true + } val navigateToMain = { navController.navigate("main") } - NavHost(navController = navController, startDestination = "main") { + NavHost(navController = navController, startDestination = startDestination) { composable("main") { MainSettings( navController = navController, @@ -74,5 +88,12 @@ private fun SettingsScreenNavHost( bottomPaddingValues = bottomPaddingValues ) } + + composable("copyright") { + Copyright( + onBackClick = navigateToMain, + bottomPaddingValues = bottomPaddingValues + ) + } } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt index 81b28a3b..64e9d0e4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt @@ -16,10 +16,11 @@ fun AlertDialogUi( dismissButtonText: String, confirmButtonText: String, onDismiss: () -> Unit, - onConfirm: () -> Unit + onConfirm: () -> Unit, + onDismissRequest: () -> Unit = onDismiss ) { AlertDialog( - onDismissRequest = onDismiss, + onDismissRequest = onDismissRequest, confirmButton = { Button( onClick = onConfirm, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt new file mode 100644 index 00000000..7d72fb22 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt @@ -0,0 +1,82 @@ +package com.example.speechbuddy.data.local + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.createDataStore +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.AUTO_BACKUP +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.DARK_MODE +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.INITIAL_PAGE +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.LAST_BACKUP_DATE +import com.example.speechbuddy.domain.models.SettingsPreferences +import com.example.speechbuddy.utils.Constants +import com.example.speechbuddy.utils.Constants.Companion.SETTINGS_PREFS +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException +import javax.inject.Inject + +class SettingsPrefsManager @Inject constructor(context: Context) { + + private val dataStore = context.createDataStore(name = SETTINGS_PREFS) + + val settingsPreferencesFlow: Flow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { preferences -> + SettingsPreferences( + autoBackup = preferences[AUTO_BACKUP] ?: true, + darkMode = preferences[DARK_MODE] ?: false, + initialPage = preferences[INITIAL_PAGE] ?: true, + lastBackupDate = preferences[LAST_BACKUP_DATE] ?: "" + ) + } + + suspend fun saveAutoBackup(value: Boolean) { + dataStore.edit { preferences -> + preferences[AUTO_BACKUP] = value + } + } + + suspend fun saveDarkMode(value: Boolean) { + dataStore.edit { preferences -> + preferences[DARK_MODE] = value + } + } + + suspend fun saveInitialPage(value: Boolean) { + dataStore.edit { preferences -> + preferences[INITIAL_PAGE] = value + } + } + + suspend fun saveLastBackupDate(value: String) { + dataStore.edit { preferences -> + preferences[LAST_BACKUP_DATE] = value + } + } + + suspend fun resetSettings() { + dataStore.edit { preferences -> + preferences[AUTO_BACKUP] = true + preferences[DARK_MODE] = false + preferences[INITIAL_PAGE] = true + preferences[LAST_BACKUP_DATE] = "" + } + } + + private object PreferencesKeys { + val AUTO_BACKUP = booleanPreferencesKey(Constants.AUTO_BACKUP_PREF) + val DARK_MODE = booleanPreferencesKey(Constants.DARK_MODE_PREF) + val INITIAL_PAGE = booleanPreferencesKey(Constants.INITIAL_PAGE_PREF) + val LAST_BACKUP_DATE = stringPreferencesKey(Constants.LAST_BACKUP_DATE_PREF) + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt index eae93a07..3b577707 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt @@ -32,6 +32,12 @@ interface SymbolDao { @Query("SELECT * FROM symbols WHERE categoryId = :categoryId") fun getSymbolsByCategoryId(categoryId: Int): Flow> + @Query("SELECT id FROM symbols WHERE id > 500") + fun getUserSymbolsId(): Flow> + + @Query("SELECT id FROM symbols WHERE isFavorite = 1") + fun getFavoriteSymbolsId(): Flow> + @Update suspend fun updateSymbol(symbolEntity: SymbolEntity) @@ -40,4 +46,7 @@ interface SymbolDao { @Upsert suspend fun upsertAll(symbolEntities: List) + + @Query("DELETE FROM symbols") + suspend fun deleteAllSymbols() } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt index 08877251..042be11d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt @@ -21,5 +21,7 @@ interface WeightRowDao { @Upsert suspend fun upsertAll(weightRowEntities: List) + @Query("DELETE FROM weighttable") + suspend fun deleteAllWeightRows() } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt new file mode 100644 index 00000000..a1072734 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt @@ -0,0 +1,46 @@ +package com.example.speechbuddy.data.remote + +import com.example.speechbuddy.data.remote.models.FavoritesListDto +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.data.remote.models.SymbolListDto +import com.example.speechbuddy.service.BackupService +import com.example.speechbuddy.utils.ResponseHandler +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.Flow +import retrofit2.Response +import javax.inject.Inject + +class SettingsRemoteSource @Inject constructor( + private val backupService: BackupService, + private val responseHandler: ResponseHandler +){ + suspend fun getDisplaySettings(authHeader: String): Flow> = + flow { + try { + val result = backupService.getDisplaySettings(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun getSymbolList(authHeader: String): Flow> = + flow { + try { + val result = backupService.getSymbolList(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun getFavoritesList(authHeader: String): Flow> = + flow { + try { + val result = backupService.getFavoriteSymbolList(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt new file mode 100644 index 00000000..d9e36aa6 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt @@ -0,0 +1,14 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SymbolIdDto( + @Json(name = "id") val id: Int +) + +@JsonClass(generateAdapter = true) +data class FavoritesListDto( + @Json(name = "results") val results: List +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt new file mode 100644 index 00000000..989e3701 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt @@ -0,0 +1,10 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SettingsBackupDto( + @Json(name = "display_mode") val displayMode: Int? = null, + @Json(name = "default_menu") val defaultMenu: Int? = null +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt new file mode 100644 index 00000000..caf251af --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt @@ -0,0 +1,18 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SymbolDto( + @Json(name = "id") val id: Int, + @Json(name = "text") val text: String, + @Json(name = "category") val category: Int, + @Json(name = "image") val image: String, + @Json(name = "created_at") val createdAt: String +) + +@JsonClass(generateAdapter = true) +data class SymbolListDto( + @Json(name = "my_symbols") val mySymbols: List +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt new file mode 100644 index 00000000..409dbe94 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt @@ -0,0 +1,14 @@ +package com.example.speechbuddy.data.remote.requests + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BackupWeightTableRequest( + val weight_table: List +) + +@JsonClass(generateAdapter = true) +data class WeightTableEntity( + val id: Int, + val weight: String +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt index a3762fce..3daecef0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt @@ -4,12 +4,14 @@ import android.content.Context import com.example.speechbuddy.data.local.AppDatabase import com.example.speechbuddy.data.local.AuthTokenPrefsManager import com.example.speechbuddy.data.local.CategoryDao +import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.local.SymbolDao import com.example.speechbuddy.data.local.UserDao import com.example.speechbuddy.data.local.models.CategoryMapper import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.local.models.UserMapper import com.example.speechbuddy.data.local.WeightRowDao +import com.example.speechbuddy.domain.utils.Converters import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -69,10 +71,22 @@ class DatabaseModule { return CategoryMapper() } + @Singleton + @Provides + fun provideConverters(): Converters { + return Converters() + } + @Singleton @Provides fun provideAuthTokenPrefsManager(@ApplicationContext context: Context): AuthTokenPrefsManager { return AuthTokenPrefsManager(context) } + @Singleton + @Provides + fun provideSettingsPrefsManager(@ApplicationContext context: Context): SettingsPrefsManager { + return SettingsPrefsManager(context) + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt index 7a75e645..3c2e6c6a 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt @@ -12,6 +12,7 @@ import com.example.speechbuddy.data.remote.requests.AuthRefreshRequest import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.domain.models.AuthToken import com.example.speechbuddy.service.AuthService +import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.service.SymbolCreationService import com.example.speechbuddy.service.UserService import com.example.speechbuddy.utils.Constants @@ -71,6 +72,12 @@ class NetworkModule { return retrofit.create(AuthService::class.java) } + @Singleton + @Provides + fun provideBackupService(retrofit: Retrofit): BackupService { + return retrofit.create(BackupService::class.java) + } + @Singleton @Provides fun provideUserDtoMapper(): UserDtoMapper { @@ -117,7 +124,8 @@ class AuthInterceptor @Inject constructor( // private val sessionManager: SessionManager = SessionManager() override fun intercept(chain: Interceptor.Chain): Response { - if (!isInternetAvailable(context)) throw ConnectException() + //if (!isInternetAvailable(context)) throw ConnectException() + val builder = chain.request().newBuilder() try { val originalRequest = chain.request() diff --git a/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt new file mode 100644 index 00000000..9bb55c7f --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt @@ -0,0 +1,12 @@ +package com.example.speechbuddy.domain.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SettingsPreferences( + val darkMode: Boolean = false, + val autoBackup: Boolean = true, + val initialPage: Boolean = true, + val lastBackupDate: String = "" +) : Parcelable \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt new file mode 100644 index 00000000..0935e4db --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt @@ -0,0 +1,165 @@ +package com.example.speechbuddy.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.example.speechbuddy.data.local.SettingsPrefsManager +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.domain.SessionManager +import com.example.speechbuddy.service.BackupService +import com.example.speechbuddy.ui.models.InitialPage +import com.example.speechbuddy.utils.Resource +import com.example.speechbuddy.utils.ResponseHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SettingsRepository @Inject constructor( + private val settingsPrefManager: SettingsPrefsManager, + private val backupService: BackupService, + private val responseHandler: ResponseHandler, + private val sessionManager: SessionManager, + private val symbolRepository: SymbolRepository, + private val weightTableRepository: WeightTableRepository +) { + + private val _darkModeLiveData = MutableLiveData() + val darkModeLiveData: LiveData + get() = _darkModeLiveData + + suspend fun setDarkMode(value: Boolean) { + CoroutineScope(Dispatchers.Main).launch { + if (_darkModeLiveData.value != value) { + _darkModeLiveData.value = value + } + } + settingsPrefManager.saveDarkMode(value) + } + + suspend fun setInitialPage(page: InitialPage) { + if (page == InitialPage.SYMBOL_SELECTION) { + settingsPrefManager.saveInitialPage(true) + } else { + settingsPrefManager.saveInitialPage(false) + } + } + + suspend fun setAutoBackup(value: Boolean) { + settingsPrefManager.saveAutoBackup(value) + } + + suspend fun setLastBackupDate(value: String) { + settingsPrefManager.saveLastBackupDate(value) + } + + fun getDarkMode(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.darkMode) + } + } + + fun getInitialPage(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.initialPage) + } + } + + fun getAutoBackup(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.autoBackup) + } + } + + fun getLastBackupDate(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.lastBackupDate) + } + } + + suspend fun displayBackup(): Flow> = + flow { + try { + var darkMode: Int? = 0 + var initialPage: Int? = 1 + getDarkMode().first().data?.let { + darkMode = if (it) 1 else 0 + } + getInitialPage().first().data?.let{ + initialPage = if (it) 1 else 0 + } + val result = backupService.displayBackup( + getAuthHeader(), + SettingsBackupDto(darkMode, initialPage) + ) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun symbolListBackup(): Flow> = + flow { + try { + if (symbolRepository.getUserSymbolsIdString().isEmpty()) { + val result = backupService.symbolListBackup( + header = getAuthHeader() + ) + emit(result) + } else { + val result = backupService.symbolListBackup( + symbolRepository.getUserSymbolsIdString(), + getAuthHeader() + ) + emit(result) + } + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun favoriteSymbolBackup(): Flow> = + flow { + try { + if (symbolRepository.getFavoriteSymbolsIdString().isEmpty()) { + val result = backupService.favoriteSymbolBackup( + header = getAuthHeader() + ) + emit(result) + } else { + val result = backupService.favoriteSymbolBackup( + symbolRepository.getFavoriteSymbolsIdString(), + getAuthHeader() + ) + emit(result) + } + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun weightTableBackup(): Flow> = + flow { + try { + val result = backupService.weightTableBackup( + getAuthHeader(), + weightTableRepository.getBackupWeightTableRequest() + ) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + private fun getAuthHeader(): String { + val accessToken = sessionManager.cachedToken.value?.accessToken + return "Bearer $accessToken" + } + +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt index 5f0b4a47..6507f330 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt @@ -6,7 +6,6 @@ import com.example.speechbuddy.data.local.models.CategoryMapper import com.example.speechbuddy.data.local.models.SymbolEntity import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.remote.MySymbolRemoteSource -import com.example.speechbuddy.utils.ResponseHandler import com.example.speechbuddy.data.remote.models.MySymbolDtoMapper import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.domain.models.Category @@ -15,8 +14,10 @@ import com.example.speechbuddy.domain.models.MySymbol import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.utils.Resource import com.example.speechbuddy.utils.ResponseCode +import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import okhttp3.MultipartBody import javax.inject.Inject @@ -44,6 +45,10 @@ class SymbolRepository @Inject constructor( symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } + suspend fun deleteAllSymbols() { + symbolDao.deleteAllSymbols() + } + fun getLastSymbol() = symbolDao.getLastSymbol().map { symbolEntities -> symbolEntities.first().let { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } @@ -89,6 +94,16 @@ class SymbolRepository @Inject constructor( symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } + suspend fun getUserSymbolsIdString(): String { + val idList = symbolDao.getUserSymbolsId().firstOrNull() ?: emptyList() + return if (idList.isEmpty()) "" else idList.joinToString(separator = ",") + } + + suspend fun getFavoriteSymbolsIdString(): String { + val idList = symbolDao.getFavoriteSymbolsId().firstOrNull() ?: emptyList() + return if (idList.isEmpty()) "" else idList.joinToString(separator = ",") + } + suspend fun updateFavorite(symbol: Symbol, value: Boolean) { val symbolEntity = SymbolEntity( id = symbol.id, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt index f4640292..93fc6417 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt @@ -5,13 +5,17 @@ import com.example.speechbuddy.data.local.WeightRowDao import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.local.models.WeightRowEntity import com.example.speechbuddy.data.local.models.WeightRowMapper +import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest +import com.example.speechbuddy.data.remote.requests.WeightTableEntity import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.domain.models.WeightRow +import com.example.speechbuddy.domain.utils.Converters import com.example.speechbuddy.ui.models.SymbolItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -23,7 +27,8 @@ import javax.inject.Singleton @Singleton class WeightTableRepository @Inject constructor( private val symbolDao: SymbolDao, - private val weightRowDao: WeightRowDao + private val weightRowDao: WeightRowDao, + private val converters: Converters ) { private val symbolMapper = SymbolMapper() private val weightRowMapper = WeightRowMapper() @@ -41,6 +46,21 @@ class WeightTableRepository @Inject constructor( } } + suspend fun deleteAllWeightRows() { + weightRowDao.deleteAllWeightRows() + } + + suspend fun getBackupWeightTableRequest(): BackupWeightTableRequest { + val weightRowList = getAllWeightRows().firstOrNull() ?: emptyList() + val weightTableEntities = weightRowList.map { weightRow -> + WeightTableEntity( + id = weightRow.id, + weight = converters.fromList(weightRow.weights) + ) + } + return BackupWeightTableRequest(weight_table = weightTableEntities) + } + private fun getWeightRowById(rowId: Int) = weightRowDao.getWeightRowById(rowId).map { weightRowEntities -> weightRowEntities.map { weightRowEntity -> diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt new file mode 100644 index 00000000..c81a0381 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -0,0 +1,54 @@ +package com.example.speechbuddy.service + +import com.example.speechbuddy.data.remote.models.FavoritesListDto +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.data.remote.models.SymbolListDto +import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query + +interface BackupService { + + @POST("/setting/backup/") + suspend fun displayBackup( + @Header("Authorization") header: String, + @Body settingsBackupDto: SettingsBackupDto + ): Response + + @POST("/symbol/enable/") + suspend fun symbolListBackup( + @Query("id") id: String? = null, + @Header("Authorization") header: String + ): Response + + @POST("/symbol/favorite/backup/") + suspend fun favoriteSymbolBackup( + @Query("id") id: String? = null, + @Header("Authorization") header: String + ): Response + + @POST("/weight/backup/") + suspend fun weightTableBackup( + @Header("Authorization") header: String, + @Body backupWeightTableRequest: BackupWeightTableRequest + ): Response + + @GET("/setting/backup/") + suspend fun getDisplaySettings( + @Header("Authorization") header: String + ): Response + + @GET("/symbol/") + suspend fun getSymbolList( + @Header("Authorization") header: String + ): Response + + @GET("/symbol/favorite/backup/") + suspend fun getFavoriteSymbolList( + @Header("Authorization") header: String + ): Response +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt index 49e88498..475faae3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt @@ -8,6 +8,9 @@ data class AccountSettingsUiState( ) enum class AccountSettingsAlert { + BACKUP, + LOADING, + BACKUP_SUCCESS, LOGOUT, WITHDRAW, WITHDRAW_PROCEED, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt index a83efc44..c4eb300d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt @@ -1,7 +1,12 @@ package com.example.speechbuddy.ui.models data class BackupSettingsUiState( - /* TODO */ - val lastBackupDate: String = "2023.09.09", - val isAutoBackupEnabled: Boolean = true -) \ No newline at end of file + val lastBackupDate: String = "", + val isAutoBackupEnabled: Boolean = true, + val alert: BackupSettingsAlert? = null +) + +enum class BackupSettingsAlert { + SUCCESS, + CONNECTION +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt index db1d4277..196363ee 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt @@ -1,8 +1,7 @@ package com.example.speechbuddy.ui.models data class DisplaySettingsUiState( - /* TODO */ - val isDarkModeEnabled: Boolean = true, + val isDarkModeEnabled: Boolean = false, val initialPage: InitialPage = InitialPage.SYMBOL_SELECTION ) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt b/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt index e1fc424e..a630bd0d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt @@ -15,6 +15,12 @@ class Constants { const val ACCESS_TOKEN_PREF: String = "com.example.speechbuddy.ACCESS_TOKEN_PREF" const val REFRESH_TOKEN_PREF: String = "com.example.speechbuddy.REFRESH_TOKEN_PREF" + const val SETTINGS_PREFS: String = "com.example.speechbuddy.SETTINGS_PREFS" + const val AUTO_BACKUP_PREF: String = "com.example.speechbuddy.AUTO_BACKUP_PREF" + const val DARK_MODE_PREF: String = "com.example.speechbuddy.DARK_MODE_PREF" + const val INITIAL_PAGE_PREF: String = "com.example.speechbuddy.INITIAL_PAGE_PREF" + const val LAST_BACKUP_DATE_PREF: String = "com.example.speechbuddy.LAST_BACKUP_DATE_PREF" + const val MINIMUM_PASSWORD_LENGTH = 8 const val MAXIMUM_NICKNAME_LENGTH = 15 const val CODE_LENGTH = 6 diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt index 4eca506c..1fb80024 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt @@ -1,9 +1,12 @@ package com.example.speechbuddy.viewmodel +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.repository.AuthRepository +import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.repository.UserRepository import com.example.speechbuddy.ui.models.AccountSettingsAlert import com.example.speechbuddy.ui.models.AccountSettingsUiState @@ -15,11 +18,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel class AccountSettingsViewModel @Inject internal constructor( private val authRepository: AuthRepository, + private val settingsRepository: SettingsRepository, private val userRepository: UserRepository, private val sessionManager: SessionManager ) : ViewModel() { @@ -58,12 +63,14 @@ class AccountSettingsViewModel @Inject internal constructor( } fun logout() { + showAlert(AccountSettingsAlert.LOADING) viewModelScope.launch { authRepository.logout().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { /* TODO: 디바이스에 저장돼 있는 유저 정보 초기화(토큰 말고) */ sessionManager.logout() + hideAlert() } ResponseCode.NO_INTERNET_CONNECTION.value -> { @@ -76,11 +83,13 @@ class AccountSettingsViewModel @Inject internal constructor( fun withdraw() { viewModelScope.launch { + showAlert(AccountSettingsAlert.LOADING) authRepository.withdraw().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { /* TODO: 디바이스에 저장돼 있는 유저 정보 초기화(토큰 말고) */ sessionManager.logout() + hideAlert() } ResponseCode.NO_INTERNET_CONNECTION.value -> { @@ -97,4 +106,89 @@ class AccountSettingsViewModel @Inject internal constructor( } } + private fun displayBackup() { + viewModelScope.launch { + settingsRepository.displayBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + private fun symbolListBackup() { + + viewModelScope.launch { + settingsRepository.symbolListBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + + } + } + + } + + private fun favoriteSymbolBackup() { + viewModelScope.launch { + settingsRepository.favoriteSymbolBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun weightTableBackup() { + viewModelScope.launch { + settingsRepository.weightTableBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> { handleSuccess() } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun backup() { + _uiState.update { + it.copy( + alert = AccountSettingsAlert.LOADING + ) + } + displayBackup() + symbolListBackup() + favoriteSymbolBackup() + weightTableBackup() + } + + private fun handleNoInternetConnection() { + _uiState.update { currentState -> + currentState.copy( + alert = AccountSettingsAlert.CONNECTION + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun handleSuccess() { + viewModelScope.launch { + settingsRepository.setLastBackupDate(LocalDate.now().toString()) + } + _uiState.update { currentState -> + currentState.copy( + alert = AccountSettingsAlert.BACKUP_SUCCESS + ) + } + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 6a76ad0b..0bfd52c0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -1,36 +1,171 @@ package com.example.speechbuddy.viewmodel +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.speechbuddy.repository.AuthRepository +import com.example.speechbuddy.repository.SettingsRepository +import com.example.speechbuddy.ui.models.BackupSettingsAlert import com.example.speechbuddy.ui.models.BackupSettingsUiState +import com.example.speechbuddy.utils.ResponseCode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel class BackupSettingsViewModel @Inject internal constructor( - private val repository: AuthRepository + private val repository: SettingsRepository, ) : ViewModel() { - private val _uiState = MutableStateFlow(BackupSettingsUiState()) + private val _uiState = MutableStateFlow( + BackupSettingsUiState( + lastBackupDate = getLastBackupDate(), + isAutoBackupEnabled = getAutoBackup() + ) + ) val uiState: StateFlow = _uiState.asStateFlow() + private val _loading = MutableLiveData(false) + val loading: LiveData get() = _loading + fun setAutoBackup(value: Boolean) { _uiState.update { currentState -> currentState.copy( isAutoBackupEnabled = value ) } + viewModelScope.launch { + repository.setAutoBackup(value) + } + // TODO: Implement automated backup } - fun backup() { + private fun setLastBackupDate(value: String) { + _uiState.update { currentState -> + currentState.copy( + lastBackupDate = value + ) + } + viewModelScope.launch { + repository.setLastBackupDate(value) + } + } + + private fun getAutoBackup(): Boolean { + var autoBackup = false + viewModelScope.launch { + repository.getAutoBackup().collect { + autoBackup = it.data?: false + } + } + return autoBackup + } + + private fun getLastBackupDate(): String { + var lastBackupDate = "" + viewModelScope.launch { + repository.getLastBackupDate().collect { + lastBackupDate = it.data?: "" + } + } + return lastBackupDate + } + + fun toastDisplayed() { + _uiState.update { currentState -> + currentState.copy( + alert = null + ) + } + } + + private fun displayBackup() { + viewModelScope.launch { + repository.displayBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + private fun symbolListBackup() { + viewModelScope.launch { - //repository.backup() + repository.symbolListBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + + } + } + + } + + private fun favoriteSymbolBackup() { + viewModelScope.launch { + repository.favoriteSymbolBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun weightTableBackup() { + viewModelScope.launch { + repository.weightTableBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> { handleSuccess() } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } } } + + @RequiresApi(Build.VERSION_CODES.O) + fun backup() { + _loading.value = true + displayBackup() + symbolListBackup() + favoriteSymbolBackup() + weightTableBackup() + } + + private fun handleNoInternetConnection() { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.CONNECTION + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun handleSuccess() { + _loading.value = false + setLastBackupDate(LocalDate.now().toString()) + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.SUCCESS, + ) + } + } + + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt index 4236ddf8..d5191b36 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.ui.models.DisplaySettingsUiState import com.example.speechbuddy.ui.models.InitialPage import dagger.hilt.android.lifecycle.HiltViewModel @@ -8,12 +10,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { +class DisplaySettingsViewModel @Inject internal constructor( + private val repository: SettingsRepository +) : ViewModel() { - private val _uiState = MutableStateFlow(DisplaySettingsUiState()) + private val initialPageBoolean = getInitialPage() + private val initialInitialPage = if (initialPageBoolean) { + InitialPage.SYMBOL_SELECTION + } else { + InitialPage.TEXT_TO_SPEECH + } + + private val _uiState = MutableStateFlow(DisplaySettingsUiState( + getDarkMode(), initialInitialPage)) val uiState: StateFlow = _uiState.asStateFlow() fun setDarkMode(value: Boolean) { @@ -22,6 +35,9 @@ class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { isDarkModeEnabled = value ) } + viewModelScope.launch { + repository.setDarkMode(value) + } } fun setInitialPage(page: InitialPage) { @@ -30,5 +46,28 @@ class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { initialPage = page ) } + viewModelScope.launch { + repository.setInitialPage(page) + } + } + + fun getDarkMode(): Boolean { + var darkMode = false + viewModelScope.launch { + repository.getDarkMode().collect { + darkMode = it.data?: false + } + } + return darkMode + } + + fun getInitialPage(): Boolean { + var initialPage = true + viewModelScope.launch { + repository.getInitialPage().collect { + initialPage = it.data?: true + } + } + return initialPage } } \ No newline at end of file diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index b75ece4d..65d6871d 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -97,11 +97,13 @@ 계정 로그아웃 정말로 로그아웃하시겠습니까? + 로그아웃하기 전 백업하시겠습니까? 변경사항을 백업하지 않으면 이용 기록이 손실될 수 있습니다. 회원탈퇴 회원탈퇴를 하면 지금까지의 이용 기록이 모두 사라지며 복구할 수 없습니다. SpeechBuddy를 다시 이용하시려면 게스트 모드를 이용하거나 회원가입을 새로 해야 합니다. 정말로 탈퇴하시겠습니까? 정말로 탈퇴하시겠습니까? 이 동작은 취소할 수 없습니다. 인터넷 연결 없음 - 인터넷 연결을 확인하세요 + 인터넷 연결을 확인하세요업 + 백업 현재 게스트 모드를 사용 중입니다. @@ -112,6 +114,7 @@ 마지막 백업 날짜 자동 백업 활성화 지금 백업하기 + 백업 성공 디스플레이 @@ -131,4 +134,8 @@ 개발자 정보 + + + 저작권 정보 + 본 서비스의 그림상징은 \'한국형 보완대체의사소통용 기본상징 체계집\'의 일부로 박은혜, 김영태(이화여자대학교), 홍기형(성신여자대학교)에게 저작권이 있습니다.\n자료의 무단 사용 및 2차 가공, 배포, 상업적 용도로 사용하는 것을 금합니다. \ No newline at end of file