diff --git a/frontend/app/src/main/AndroidManifest.xml b/frontend/app/src/main/AndroidManifest.xml index 9a2b674a..10cb308a 100644 --- a/frontend/app/src/main/AndroidManifest.xml +++ b/frontend/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ android:theme="@style/Theme.SpeechBuddy" android:usesCleartextTraffic="true" android:windowSoftInputMode="adjustResize" + android:enableOnBackInvokedCallback="true" tools:targetApi="31"> SpeechBuddyHomeNavHost( navController = navController, - bottomPaddingValues = paddingValues, + paddingValues = paddingValues, initialPage = initialPage, - showBottomNavBar = { bottomNavBarState.value = true }, - hideBottomNavBar = { bottomNavBarState.value = false } + topAppBarState = topAppBarState, + bottomNavBarState = bottomNavBarState + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBar( + topAppBarState: MutableState, + items: List, + navController: NavController +) { + val backStackEntry = navController.currentBackStackEntryAsState() + var titleResId: Int? = null + items.forEach { item -> + if (item.route == backStackEntry.value?.destination?.route) titleResId = item.nameResId + } + + AnimatedVisibility( + visible = topAppBarState.value, + enter = slideInVertically(initialOffsetY = { -it }), + exit = slideOutVertically(targetOffsetY = { -it }) + ) { + CenterAlignedTopAppBar( + title = { + if (titleResId != null) { // ensure that titleResId is properly initialized + Text( + text = stringResource(id = titleResId!!), + style = MaterialTheme.typography.titleLarge + ) + } + }, + navigationIcon = { + Image( + painter = painterResource(id = R.drawable.speechbuddy_parrot), + contentDescription = stringResource(id = R.string.app_name), + modifier = Modifier + .padding(6.dp) + .size(40.dp), + contentScale = ContentScale.Fit + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + titleContentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) ) } } @@ -145,10 +204,10 @@ private fun BottomNavigationBar( @Composable private fun SpeechBuddyHomeNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, initialPage: Boolean, - showBottomNavBar: () -> Unit, - hideBottomNavBar: () -> Unit, + topAppBarState: MutableState, + bottomNavBarState: MutableState ) { val startDestination = if (initialPage) { @@ -160,24 +219,24 @@ private fun SpeechBuddyHomeNavHost( NavHost(navController = navController, startDestination = startDestination) { composable("symbol_selection") { SymbolSelectionScreen( - bottomPaddingValues = bottomPaddingValues, - showBottomNavBar = showBottomNavBar, - hideBottomNavBar = hideBottomNavBar + paddingValues = paddingValues, + topAppBarState = topAppBarState, + bottomNavBarState = bottomNavBarState ) } composable("text_to_speech") { TextToSpeechScreen( - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("symbol_creation") { SymbolCreationScreen( - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("settings") { SettingsScreen( - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } } 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 a5089590..f0bd7c34 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 @@ -12,8 +12,6 @@ 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 import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -28,88 +26,75 @@ import com.example.speechbuddy.compose.utils.AlertDialogUi import com.example.speechbuddy.compose.utils.ButtonLevel import com.example.speechbuddy.compose.utils.ButtonUi import com.example.speechbuddy.compose.utils.TitleUi -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( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: AccountSettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() /** * Wait until user info is successfully retrieved - * in init{} of the viewmodel. + * in init{} of the view model. */ uiState.user?.let { user -> Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.account)) + + Spacer(modifier = Modifier.height(20.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + SettingsRow( + label = stringResource(id = R.string.email), + content = { + SettingsRowText(text = user.email) + } + ) + + SettingsRow( + label = stringResource(id = R.string.nickname), + content = { + SettingsRowText(text = user.nickname) + } ) } - ) { topPaddingValues -> + + Spacer(modifier = Modifier.height(80.dp)) + Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.spacedBy(14.dp) ) { - TitleUi(title = stringResource(id = R.string.account)) - - Spacer(modifier = Modifier.height(20.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - SettingsRow( - label = stringResource(id = R.string.email), - content = { - SettingsRowText(text = user.email) - } - ) - - SettingsRow( - label = stringResource(id = R.string.nickname), - content = { - SettingsRowText(text = user.nickname) - } - ) - } + ButtonUi( + text = stringResource(id = R.string.logout), + onClick = { viewModel.showAlert(AccountSettingsAlert.BACKUP) }, + isEnabled = uiState.buttonEnabled + ) - Spacer(modifier = Modifier.height(80.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - ButtonUi( - text = stringResource(id = R.string.logout), - onClick = { viewModel.showAlert(AccountSettingsAlert.BACKUP) }, - isEnabled = uiState.buttonEnabled - ) - - ButtonUi( - text = stringResource(id = R.string.withdraw), - onClick = { viewModel.showAlert(AccountSettingsAlert.WITHDRAW) }, - level = ButtonLevel.QUATERNARY, - isEnabled = uiState.buttonEnabled - ) - } + ButtonUi( + text = stringResource(id = R.string.withdraw), + onClick = { viewModel.showAlert(AccountSettingsAlert.WITHDRAW) }, + level = ButtonLevel.QUATERNARY, + isEnabled = uiState.buttonEnabled + ) } } } 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 0402076b..57a7b20a 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 @@ -13,14 +13,11 @@ 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 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 @@ -28,18 +25,15 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel 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( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: BackupSettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -47,57 +41,49 @@ fun BackupSettings( 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.backup_to_server)) - - Spacer(modifier = Modifier.height(20.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - SettingsRow( - label = stringResource(id = R.string.last_backup_date), - content = { - SettingsRowText(text = uiState.lastBackupDate) - } - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.backup_to_server)) - SettingsRow(label = stringResource(id = R.string.enable_auto_backup), - content = { - Switch( - checked = uiState.isAutoBackupEnabled, - onCheckedChange = { viewModel.setAutoBackup(it) }, - modifier = Modifier.heightIn(max = 32.dp), - enabled = uiState.buttonEnabled - ) - } - ) - } + Spacer(modifier = Modifier.height(20.dp)) - Spacer(modifier = Modifier.height(80.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + SettingsRow( + label = stringResource(id = R.string.last_backup_date), + content = { + SettingsRowText(text = uiState.lastBackupDate) + } + ) - ButtonUi( - text = stringResource(id = R.string.backup_now), - onClick = { viewModel.backup() }, - isEnabled = uiState.buttonEnabled + SettingsRow(label = stringResource(id = R.string.enable_auto_backup), + content = { + Switch( + checked = uiState.isAutoBackupEnabled, + onCheckedChange = { viewModel.setAutoBackup(it) }, + modifier = Modifier.heightIn(max = 32.dp), + enabled = uiState.buttonEnabled + ) + } ) } + + Spacer(modifier = Modifier.height(80.dp)) + + ButtonUi( + text = stringResource(id = R.string.backup_now), + onClick = { viewModel.backup() }, + isEnabled = uiState.buttonEnabled + ) } } 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 index c2b8ca20..49978d2f 100644 --- 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 @@ -1,71 +1,48 @@ 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 + paddingValues: PaddingValues ) { Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) - } - ) { 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)) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.copyright_info)) - Spacer(modifier = modifier.height(20.dp)) + Spacer(modifier = modifier.height(20.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Text( - text = stringResource(R.string.copyright), - style = MaterialTheme.typography.bodyMedium - ) - } - } + 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/DevelopersInfo.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DevelopersInfo.kt index 6ad3d372..7d888e33 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DevelopersInfo.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DevelopersInfo.kt @@ -7,57 +7,46 @@ 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.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.example.speechbuddy.R -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.compose.utils.TitleUi -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DevelopersInfo( - modifier: Modifier = Modifier, onBackClick: () -> Unit, bottomPaddingValues: PaddingValues + modifier: Modifier = Modifier, + paddingValues: 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.developers_info)) + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ) + .padding(24.dp), verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.developers_info)) - Spacer(modifier = modifier.height(20.dp)) + Spacer(modifier = modifier.height(20.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - /* TODO */ - SettingsRow(label = "서울대학교 소개원실 team 6") - SettingsRow(label = "김연정") - SettingsRow(label = "류명현") - SettingsRow(label = "오준형") - SettingsRow(label = "이민영") - SettingsRow(label = "이석찬") - SettingsRow(label = "주승민") - } + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + /* TODO */ + SettingsRow(label = "서울대학교 소개원실 team 6") + SettingsRow(label = "김연정") + SettingsRow(label = "류명현") + SettingsRow(label = "오준형") + SettingsRow(label = "이민영") + SettingsRow(label = "이석찬") + SettingsRow(label = "주승민") } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt index 97712d2a..def6abee 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/DisplaySettings.kt @@ -1,6 +1,7 @@ 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 @@ -8,31 +9,28 @@ 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.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.example.speechbuddy.R -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.compose.utils.TitleUi import com.example.speechbuddy.ui.models.InitialPage import com.example.speechbuddy.viewmodel.DisplaySettingsViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DisplaySettings( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: DisplaySettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -40,55 +38,45 @@ fun DisplaySettings( Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) - } - ) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center - ) { - TitleUi(title = stringResource(id = R.string.display_settings)) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.display_settings)) - Spacer(modifier = modifier.height(20.dp)) + Spacer(modifier = modifier.height(20.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - SettingsRow( - label = stringResource(id = R.string.dark_mode), - content = { - Switch( - checked = uiState.isDarkModeEnabled, - onCheckedChange = { viewModel.setDarkMode(it) }, - modifier = Modifier.heightIn(max = 32.dp) - ) - } - ) + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + SettingsRow( + label = stringResource(id = R.string.dark_mode), + content = { + Switch( + checked = uiState.isDarkModeEnabled, + onCheckedChange = { viewModel.setDarkMode(it) }, + modifier = Modifier.heightIn(max = 32.dp) + ) + } + ) - SettingsRow( - label = stringResource(id = R.string.initial_page), - content = { - InitialPageColumn( - initialPage = uiState.initialPage, - onSelectInitialPage = { - viewModel.setInitialPage(it) - } - ) - } - ) - } + SettingsRow( + label = stringResource(id = R.string.initial_page), + content = { + InitialPageColumn( + initialPage = uiState.initialPage, + onSelectInitialPage = { + viewModel.setInitialPage(it) + } + ) + } + ) } } } @@ -101,27 +89,37 @@ fun InitialPageColumn( ) { Column( modifier = Modifier.width(140.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.spacedBy(10.dp), ) { SettingsRow( label = stringResource(id = R.string.talk_with_symbols), content = { - RadioButton( - selected = initialPage == InitialPage.SYMBOL_SELECTION, - onClick = { onSelectInitialPage(InitialPage.SYMBOL_SELECTION) }, - modifier = Modifier.heightIn(max = 32.dp) - ) + Box( + modifier = Modifier.size(32.dp), + contentAlignment = Alignment.CenterEnd + ) { + RadioButton( + selected = initialPage == InitialPage.SYMBOL_SELECTION, + onClick = { onSelectInitialPage(InitialPage.SYMBOL_SELECTION) }, + modifier = Modifier.size(20.dp) + ) + } } ) SettingsRow( label = stringResource(id = R.string.talk_with_speech), content = { - RadioButton( - selected = initialPage == InitialPage.TEXT_TO_SPEECH, - onClick = { onSelectInitialPage(InitialPage.TEXT_TO_SPEECH) }, - modifier = Modifier.heightIn(max = 32.dp) - ) + Box( + modifier = Modifier.size(32.dp), + contentAlignment = Alignment.CenterEnd + ) { + RadioButton( + selected = initialPage == InitialPage.TEXT_TO_SPEECH, + onClick = { onSelectInitialPage(InitialPage.TEXT_TO_SPEECH) }, + modifier = Modifier.size(20.dp) + ) + } } ) } 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 7ed4d248..fa7e6abf 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,12 +3,9 @@ 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.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,38 +15,30 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.ButtonUi import com.example.speechbuddy.compose.utils.TitleUi -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.viewmodel.AccountSettingsViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun GuestSettings( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: AccountSettingsViewModel = hiltViewModel() ) { Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) - } - ) { topPaddingValues -> + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center + modifier = Modifier.height(240.dp), + verticalArrangement = Arrangement.SpaceBetween ) { TitleUi( title = stringResource(id = R.string.account), @@ -58,11 +47,8 @@ fun GuestSettings( ButtonUi( text = stringResource(id = R.string.exit_guest_mode), - onClick = { viewModel.exitGuestMode() }, - modifier = Modifier.offset(y = 240.dp) + onClick = { viewModel.exitGuestMode() } ) - - 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 2c170644..bbc3990f 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 @@ -8,9 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonDefaults -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.material3.TextButton @@ -24,15 +22,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.NoRippleInteractionSource -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.viewmodel.AccountSettingsViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainSettings( modifier: Modifier = Modifier, navController: NavHostController, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: AccountSettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -41,49 +37,43 @@ fun MainSettings( Surface( modifier = modifier.fillMaxSize() ) { - Scaffold(topBar = { - TopAppBarUi(title = stringResource(id = R.string.settings)) - }) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp) - ) { - SettingsTextButton( - text = stringResource(id = R.string.account), - onClick = { - if (isGuestMode) navController.navigate("guest") - else navController.navigate("account") - }, + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) + .padding(24.dp) + ) { + SettingsTextButton( + text = stringResource(id = R.string.account), + onClick = { + if (isGuestMode) navController.navigate("guest") + else navController.navigate("account") + }, + ) - SettingsTextButton(text = stringResource(id = R.string.display), - onClick = { navController.navigate("display") }) + SettingsTextButton(text = stringResource(id = R.string.display), + onClick = { navController.navigate("display") }) - SettingsTextButton(text = stringResource(id = R.string.manage_symbols), - onClick = { navController.navigate("my_symbol") }, - enabled = !isGuestMode - ) + SettingsTextButton(text = stringResource(id = R.string.manage_symbols), + onClick = { navController.navigate("my_symbol") }) - SettingsTextButton( - text = stringResource(id = R.string.backup_to_server), - onClick = { navController.navigate("backup") }, - enabled = !isGuestMode - ) + SettingsTextButton( + text = stringResource(id = R.string.backup_to_server), + onClick = { navController.navigate("backup") }, + enabled = !isGuestMode + ) - SettingsTextButton(text = stringResource(id = R.string.version_info), - onClick = { navController.navigate("version") }) + SettingsTextButton(text = stringResource(id = R.string.version_info), + onClick = { navController.navigate("version") }) - SettingsTextButton(text = stringResource(id = R.string.developers_info), - onClick = { navController.navigate("developers") }) + SettingsTextButton(text = stringResource(id = R.string.developers_info), + onClick = { navController.navigate("developers") }) - SettingsTextButton(text = stringResource(id = R.string.copyright_info), - onClick = { navController.navigate("copyright") }) - } + 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/MySymbolSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MySymbolSettings.kt index c23bb201..e518e7d0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MySymbolSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MySymbolSettings.kt @@ -1,55 +1,311 @@ package com.example.speechbuddy.compose.settings +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable 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.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage import com.example.speechbuddy.R -import com.example.speechbuddy.compose.utils.TopAppBarUi +import com.example.speechbuddy.compose.symbolselection.SymbolSearchTextField +import com.example.speechbuddy.compose.utils.NoRippleInteractionSource +import com.example.speechbuddy.compose.utils.SymbolUi +import com.example.speechbuddy.domain.models.Symbol +import com.example.speechbuddy.ui.models.MySymbolSettingsDisplayMode +import com.example.speechbuddy.utils.Constants.Companion.DEFAULT_SYMBOL_COUNT +import com.example.speechbuddy.utils.Constants.Companion.DEFAULT_SYMBOL_IMAGE_PATH +import com.example.speechbuddy.utils.Constants.Companion.MAXIMUM_LINES_FOR_SYMBOL_TEXT +import com.example.speechbuddy.viewmodel.MySymbolSettingsViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun MySymbolSettings( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues + paddingValues: PaddingValues, + viewModel: MySymbolSettingsViewModel = hiltViewModel() ) { + val uiState by viewModel.uiState.collectAsState() + val symbols by viewModel.symbols.observeAsState(initial = emptyList()) + + val lazyGridState = rememberLazyGridState() + Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) - } - ) { topPaddingValues -> + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SymbolSearchTextField( + value = viewModel.queryInput, + onValueChange = { viewModel.setQuery(it) } + ) + Column( + modifier = Modifier.weight(1f) + ) { + DisplayModeMenu( + currentDisplayMode = uiState.mySymbolSettingsDisplayMode, + onSelectDisplayMode = { viewModel.selectDisplayMode(it) } + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxSize() + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape( + topEnd = 20.dp, + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ), + state = lazyGridState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (uiState.mySymbolSettingsDisplayMode == MySymbolSettingsDisplayMode.SYMBOL) { + items(symbols ?: emptyList()) { symbol -> + MySymbolUi( + symbol = symbol, + viewModel = viewModel + ) + } + } else { + items(symbols ?: emptyList()) { symbol -> + SymbolUi( + symbol = symbol, + onSelect = {}, + onFavoriteChange = { viewModel.updateFavorite(symbol, it) } + ) + } + } + } + } + if (uiState.mySymbolSettingsDisplayMode == MySymbolSettingsDisplayMode.SYMBOL) { + DeleteButtonUi( + text = stringResource(id = R.string.delete_my_symbol), + onClick = { viewModel.deleteCheckedSymbols() }, + isEnabled = viewModel.checkedSymbols.isNotEmpty() + ) + } + } + } +} + +data class DisplayModeItem( + val textResId: Int, + val displayMode: MySymbolSettingsDisplayMode, + val containerColor: Color, + val contentColor: Color +) + +@Composable +fun DisplayModeMenu( + currentDisplayMode: MySymbolSettingsDisplayMode, + onSelectDisplayMode: (MySymbolSettingsDisplayMode) -> Unit +) { + val displayModeItems = listOf( + DisplayModeItem( + R.string.my_symbol, + MySymbolSettingsDisplayMode.SYMBOL, + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.colorScheme.onSecondaryContainer + ), + DisplayModeItem( + R.string.favorite, + MySymbolSettingsDisplayMode.FAVORITE, + MaterialTheme.colorScheme.errorContainer, + MaterialTheme.colorScheme.onErrorContainer + ) + ) + + Row { + displayModeItems.forEach { item -> + val selected = item.displayMode == currentDisplayMode + Card( + modifier = Modifier.clickable( + interactionSource = NoRippleInteractionSource(), + indication = null, + onClick = { onSelectDisplayMode(item.displayMode) }), + shape = RoundedCornerShape( + topStart = 10.dp, topEnd = 10.dp + ), + colors = CardDefaults.cardColors( + containerColor = + if (selected) MaterialTheme.colorScheme.surfaceVariant + else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Card( + modifier = Modifier + .padding(8.dp), + shape = RoundedCornerShape(5.dp), + colors = CardDefaults.cardColors( + containerColor = + if (selected) item.containerColor + else item.containerColor.copy(alpha = 0.5f), + contentColor = + if (selected) item.contentColor + else item.contentColor.copy(alpha = 0.5f) + ) + ) { + Text( + text = stringResource(id = item.textResId), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@ExperimentalMaterial3Api +@Composable +fun MySymbolUi( + symbol: Symbol, + modifier: Modifier = Modifier, + viewModel: MySymbolSettingsViewModel +) { + val filepath = + if (symbol.id > DEFAULT_SYMBOL_COUNT) + LocalContext.current.filesDir.toString().plus("/") + else + DEFAULT_SYMBOL_IMAGE_PATH + + val isChecked = viewModel.checkedSymbols.contains(symbol) + + Card( + onClick = { viewModel.toggleSymbolChecked(symbol) }, + modifier = modifier.size(140.dp), + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) { + Box( + contentAlignment = Alignment.TopEnd, + modifier = Modifier.fillMaxSize() + ) { + Box( modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() + .padding(6.dp) + .size(16.dp) + .border(1.dp, Color.Black) + ) { + Checkbox( + checked = isChecked, + onCheckedChange = { viewModel.toggleSymbolChecked(symbol) }, + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + colors = CheckboxDefaults.colors( + checkedColor = Color.White, + uncheckedColor = Color.White, + checkmarkColor = Color.Black ) - .padding(24.dp), - verticalArrangement = Arrangement.Center, + ) + } + + Column( + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - /* TODO */ - Text(text = "My Symbol Settings") + GlideImage( + model = filepath.plus("symbol_${symbol.id}.png"), + contentDescription = symbol.text, + modifier = Modifier.height(90.dp), + contentScale = ContentScale.FillHeight + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .background(color = MaterialTheme.colorScheme.secondaryContainer) + .padding(horizontal = 10.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol.text, + textAlign = TextAlign.Center, + maxLines = MAXIMUM_LINES_FOR_SYMBOL_TEXT, + style = MaterialTheme.typography.bodyMedium + ) + } } } } +} + +@Composable +fun DeleteButtonUi( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(48.dp), + enabled = isEnabled, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ) + ) { + Text(text = text, style = MaterialTheme.typography.titleMedium) + } } \ No newline at end of file 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 f541ed2a..93d6ced0 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 @@ -4,8 +4,6 @@ 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 @@ -14,12 +12,12 @@ import androidx.navigation.compose.rememberNavController @RequiresApi(Build.VERSION_CODES.O) @Composable fun SettingsScreen( - bottomPaddingValues: PaddingValues + paddingValues: PaddingValues ) { val navController = rememberNavController() SettingsScreenNavHost( navController = navController, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } @@ -27,64 +25,54 @@ fun SettingsScreen( @Composable private fun SettingsScreenNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues + paddingValues: PaddingValues ) { - val navigateToMain = { navController.navigate("main") } - NavHost(navController = navController, startDestination = "main") { composable("main") { MainSettings( navController = navController, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("account") { AccountSettings( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("guest") { GuestSettings( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("display") { DisplaySettings( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("my_symbol") { MySymbolSettings( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("backup") { BackupSettings( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("version") { VersionInfo( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("developers") { DevelopersInfo( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } composable("copyright") { Copyright( - onBackClick = navigateToMain, - bottomPaddingValues = bottomPaddingValues + paddingValues = paddingValues ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/VersionInfoScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/VersionInfoScreen.kt index 1b5e9097..7f8a3ad7 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/VersionInfoScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/VersionInfoScreen.kt @@ -7,69 +7,54 @@ 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.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.example.speechbuddy.R -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.compose.utils.TitleUi -@OptIn(ExperimentalMaterial3Api::class) @Composable fun VersionInfo( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - bottomPaddingValues: PaddingValues + paddingValues: PaddingValues ) { Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi( - title = stringResource(id = R.string.settings), - onBackClick = onBackClick, - isBackClickEnabled = true + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) - } - ) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center - ) { - TitleUi(title = stringResource(id = R.string.version_info)) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.version_info)) - Spacer(modifier = modifier.height(20.dp)) + Spacer(modifier = modifier.height(20.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - SettingsRow( - label = stringResource(id = R.string.version), - content = { - /* TODO */ - SettingsRowText(text = "1.0.0") - } - ) + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + SettingsRow( + label = stringResource(id = R.string.version), + content = { + /* TODO */ + SettingsRowText(text = "1.0.0") + } + ) - SettingsRow( - label = stringResource(id = R.string.developers_email), - content = { - /* TODO */ - SettingsRowText(text = "speechbuddy@gmail.com") - } - ) - } + SettingsRow( + label = stringResource(id = R.string.developers_email), + content = { + /* TODO */ + SettingsRowText(text = "speechbuddy@gmail.com") + } + ) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolcreation/SymbolCreationScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolcreation/SymbolCreationScreen.kt index 3ecff7d7..b929092c 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolcreation/SymbolCreationScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolcreation/SymbolCreationScreen.kt @@ -40,10 +40,8 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon 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 @@ -66,7 +64,6 @@ import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.ButtonUi import com.example.speechbuddy.compose.utils.TextFieldUi import com.example.speechbuddy.compose.utils.TitleUi -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.domain.models.Category import com.example.speechbuddy.ui.models.DialogState import com.example.speechbuddy.ui.models.PhotoType @@ -75,11 +72,10 @@ import com.example.speechbuddy.ui.models.SymbolCreationUiState import com.example.speechbuddy.utils.Constants import com.example.speechbuddy.viewmodel.SymbolCreationViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SymbolCreationScreen( modifier: Modifier = Modifier, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: SymbolCreationViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -131,101 +127,95 @@ fun SymbolCreationScreen( Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi(title = stringResource(id = R.string.create_new_symbol)) - } - ) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - TitleUi( - title = stringResource(id = R.string.create_new_symbol), - description = stringResource(id = R.string.symbol_creation_description) + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TitleUi( + title = stringResource(id = R.string.create_new_symbol), + description = stringResource(id = R.string.symbol_creation_description) + ) - Spacer(modifier = Modifier.height(30.dp)) + Spacer(modifier = Modifier.height(30.dp)) - AddPhotoButton( - onClick = { viewModel.updateDialogState("show") }, - isError = isPhotoInputError, - viewModel = viewModel - ) + AddPhotoButton( + onClick = { viewModel.updateDialogState("show") }, + isError = isPhotoInputError, + viewModel = viewModel + ) - if (viewModel.dialogState == DialogState.SHOW) { - PhotoOptionDialog( - onDismissRequest = { viewModel.updateDialogState("hide") }, - onCameraClick = { - // Check if permission is already granted - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.CAMERA - ) -> { - cameraLauncher.launch(null) - } + if (viewModel.dialogState == DialogState.SHOW) { + PhotoOptionDialog( + onDismissRequest = { viewModel.updateDialogState("hide") }, + onCameraClick = { + // Check if permission is already granted + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.CAMERA + ) -> { + cameraLauncher.launch(null) + } - else -> { - requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) - } + else -> { + requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) } - viewModel.photoType = PhotoType.CAMERA - }, - onGalleryClick = { - galleryLauncher.launch("image/*") - viewModel.photoType = PhotoType.GALLERY - }, - onCancelClick = { viewModel.updateDialogState("hide") } - ) - } + } + viewModel.photoType = PhotoType.CAMERA + }, + onGalleryClick = { + galleryLauncher.launch("image/*") + viewModel.photoType = PhotoType.GALLERY + }, + onCancelClick = { viewModel.updateDialogState("hide") } + ) + } - Spacer(modifier = Modifier.height(30.dp)) + Spacer(modifier = Modifier.height(30.dp)) - // Symbol Category Field - DropdownUi( - selectedValue = viewModel.categoryInput, - onValueChange = { viewModel.setCategory(it) }, - items = categories, - modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(R.string.category)) }, - isError = isCategoryError, - viewModel = viewModel, - uiState = uiState - ) + // Symbol Category Field + DropdownUi( + selectedValue = viewModel.categoryInput, + onValueChange = { viewModel.setCategory(it) }, + items = categories, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.category)) }, + isError = isCategoryError, + viewModel = viewModel, + uiState = uiState + ) - Spacer(modifier = Modifier.height(15.dp)) + Spacer(modifier = Modifier.height(15.dp)) - // Symbol Text Field - TextFieldUi( - value = viewModel.symbolTextInput, - onValueChange = { viewModel.setSymbolText(it) }, - label = { Text(stringResource(R.string.new_symbol_name)) }, - supportingText = { - if (isSymbolTextError || isConnectionError) { - Text(stringResource(id = uiState.error!!.messageId)) - } - }, - isError = isSymbolTextError || isConnectionError, - isValid = uiState.isValidSymbolText - ) + // Symbol Text Field + TextFieldUi( + value = viewModel.symbolTextInput, + onValueChange = { viewModel.setSymbolText(it) }, + label = { Text(stringResource(R.string.new_symbol_name)) }, + supportingText = { + if (isSymbolTextError || isConnectionError) { + Text(stringResource(id = uiState.error!!.messageId)) + } + }, + isError = isSymbolTextError || isConnectionError, + isValid = uiState.isValidSymbolText + ) - // Symbol Creation Button - ButtonUi( - text = stringResource(id = R.string.create), - onClick = { viewModel.createSymbol(context) }, - modifier = Modifier.offset(y = 50.dp), - isEnabled = uiState.buttonEnabled, - isError = false - ) - } + // Symbol Creation Button + ButtonUi( + text = stringResource(id = R.string.create), + onClick = { viewModel.createSymbol(context) }, + modifier = Modifier.offset(y = 50.dp), + isEnabled = uiState.buttonEnabled, + isError = false + ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/DisplayMaxScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/DisplayMaxScreen.kt index 7563ad9b..e1b6568b 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/DisplayMaxScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/DisplayMaxScreen.kt @@ -1,9 +1,8 @@ package com.example.speechbuddy.compose.symbolselection import android.app.Activity -import android.content.Context -import android.content.ContextWrapper import android.content.pm.ActivityInfo +import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -28,15 +27,18 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.LifecycleOwner import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.example.speechbuddy.R @@ -48,10 +50,29 @@ import com.example.speechbuddy.viewmodel.SymbolSelectionViewModel @Composable fun DisplayMaxScreen( - onExit: () -> Unit, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + showAppBars: () -> Unit, + hideAppBars: () -> Unit, viewModel: SymbolSelectionViewModel = hiltViewModel() ) { - LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + val activity = LocalContext.current as Activity + + BackHandler(true) { + viewModel.exitDisplayMax() + } + + LaunchedEffect(true) { + hideAppBars() + } + + DisposableEffect(lifecycleOwner) { + val originalOrientation = activity.requestedOrientation + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onDispose { + activity.requestedOrientation = originalOrientation + showAppBars() + } + } Surface( modifier = Modifier @@ -96,7 +117,7 @@ fun DisplayMaxScreen( ButtonUi( text = stringResource(id = R.string.exit), - onClick = onExit, + onClick = { viewModel.exitDisplayMax() }, modifier = Modifier.widthIn(max = 312.dp), level = ButtonLevel.QUINARY ) @@ -153,24 +174,4 @@ fun DisplayMaxSymbolUi( } } } -} - -@Composable -fun LockScreenOrientation(orientation: Int) { - val context = LocalContext.current - DisposableEffect(orientation) { - val activity = context.findActivity() ?: return@DisposableEffect onDispose {} - val originalOrientation = activity.requestedOrientation - activity.requestedOrientation = orientation - onDispose { - // restore original orientation when view disappears - activity.requestedOrientation = originalOrientation - } - } -} - -fun Context.findActivity(): Activity? = when (this) { - is Activity -> this - is ContextWrapper -> baseContext.findActivity() - else -> null } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/SymbolSelectionScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/SymbolSelectionScreen.kt index b9df4e82..a3a73f56 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/SymbolSelectionScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/symbolselection/SymbolSelectionScreen.kt @@ -14,21 +14,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.CategoryUi import com.example.speechbuddy.compose.utils.SymbolUi -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.domain.models.Category import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.viewmodel.SymbolSelectionViewModel @@ -38,9 +35,9 @@ import kotlinx.coroutines.launch @Composable fun SymbolSelectionScreen( modifier: Modifier = Modifier, - bottomPaddingValues: PaddingValues, - showBottomNavBar: () -> Unit, - hideBottomNavBar: () -> Unit, + paddingValues: PaddingValues, + topAppBarState: MutableState, + bottomNavBarState: MutableState, viewModel: SymbolSelectionViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -54,84 +51,74 @@ fun SymbolSelectionScreen( Surface( modifier = modifier.fillMaxSize() ) { - Scaffold( - topBar = { - TopAppBarUi(title = stringResource(id = R.string.talk_with_symbols)) - } - ) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(start = 24.dp, top = 24.dp, end = 24.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - SymbolSearchTextField( - value = viewModel.queryInput, - onValueChange = { viewModel.setQuery(it) } + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) + .padding(start = 24.dp, top = 24.dp, end = 24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + SymbolSearchTextField( + value = viewModel.queryInput, + onValueChange = { viewModel.setQuery(it) } + ) - SelectedSymbolsBox( - selectedSymbols = viewModel.selectedSymbols, - lazyListState = lazyListState, - onClear = { viewModel.clear(it) }, - onClearAll = { viewModel.clearAll() }, - onDisplayMax = { viewModel.enterDisplayMax() } - ) + SelectedSymbolsBox( + selectedSymbols = viewModel.selectedSymbols, + lazyListState = lazyListState, + onClear = { viewModel.clear(it) }, + onClearAll = { viewModel.clearAll() }, + onDisplayMax = { viewModel.enterDisplayMax() } + ) - Column { - DisplayModeMenu( - currentDisplayMode = uiState.displayMode, - onSelectDisplayMode = { viewModel.selectDisplayMode(it) } - ) + Column { + DisplayModeMenu( + currentDisplayMode = uiState.displayMode, + onSelectDisplayMode = { viewModel.selectDisplayMode(it) } + ) - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier - .fillMaxSize() - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(topEnd = 20.dp) - ), - state = lazyGridState, - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - /** - * Without the elvis operator, null pointer exception arises. - * Do NOT erase the elvis operator although they seem useless! - */ - /** - * Without the elvis operator, null pointer exception arises. - * Do NOT erase the elvis operator although they seem useless! - */ - items(entries ?: emptyList()) { entry -> - when (entry) { - is Symbol -> SymbolUi( - symbol = entry, - onSelect = { - coroutineScope.launch { - val id = viewModel.selectSymbol(entry) - lazyListState.animateScrollToItem(id) - } - }, - onFavoriteChange = { viewModel.updateFavorite(entry, it) } - ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxSize() + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(topEnd = 20.dp) + ), + state = lazyGridState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + /** + * Without the elvis operator, null pointer exception arises. + * Do NOT erase the elvis operator although it seems useless! + */ + items(entries ?: emptyList()) { entry -> + when (entry) { + is Symbol -> SymbolUi( + symbol = entry, + onSelect = { + coroutineScope.launch { + val id = viewModel.selectSymbol(entry) + lazyListState.animateScrollToItem(id) + } + }, + onFavoriteChange = { viewModel.updateFavorite(entry, it) } + ) - is Category -> CategoryUi( - category = entry, - onSelect = { - coroutineScope.launch { - viewModel.selectCategory(entry) - lazyGridState.animateScrollToItem(0) - } + is Category -> CategoryUi( + category = entry, + onSelect = { + coroutineScope.launch { + viewModel.selectCategory(entry) + lazyGridState.animateScrollToItem(0) } - ) - } + } + ) } } } @@ -140,11 +127,14 @@ fun SymbolSelectionScreen( } if (uiState.isDisplayMax) { - hideBottomNavBar() DisplayMaxScreen( - onExit = { - viewModel.exitDisplayMax() - showBottomNavBar() + showAppBars = { + topAppBarState.value = true + bottomNavBarState.value = true + }, + hideAppBars = { + topAppBarState.value = false + bottomNavBarState.value = false } ) } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt index 56184a0b..9afe6802 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults @@ -37,7 +36,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.example.speechbuddy.R -import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.compose.utils.TitleUi import com.example.speechbuddy.ui.models.ButtonStatusType import com.example.speechbuddy.viewmodel.TextToSpeechViewModel @@ -46,7 +44,7 @@ import com.example.speechbuddy.viewmodel.TextToSpeechViewModel @Composable fun TextToSpeechScreen( modifier: Modifier = Modifier, - bottomPaddingValues: PaddingValues, + paddingValues: PaddingValues, viewModel: TextToSpeechViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -58,72 +56,68 @@ fun TextToSpeechScreen( Surface( modifier = modifier.fillMaxSize() ) { - Scaffold(topBar = { - TopAppBarUi(title = stringResource(id = R.string.talk_with_speech)) - }) { topPaddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = topPaddingValues.calculateTopPadding(), - bottom = bottomPaddingValues.calculateBottomPadding() - ) - .padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - TitleUi( - title = stringResource(id = R.string.talk_with_sound), - description = stringResource(id = R.string.tts_description) + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() ) + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TitleUi( + title = stringResource(id = R.string.talk_with_sound), + description = stringResource(id = R.string.tts_description) + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - OutlinedTextField( - value = viewModel.textInput, - onValueChange = { - viewModel.setText(it) - }, - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .height(200.dp), - textStyle = MaterialTheme.typography.bodyMedium, - shape = RoundedCornerShape(10.dp), - colors = TextFieldDefaults.outlinedTextFieldColors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) + OutlinedTextField( + value = viewModel.textInput, + onValueChange = { + viewModel.setText(it) + }, + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .height(200.dp), + textStyle = MaterialTheme.typography.bodyMedium, + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline ) + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.weight(1f)) - TextToSpeechButton( - buttonStatus = uiState.buttonStatus, - activatedColor = activatedColor, - deactivatedColor = deactivatedColor, - onPlay = { viewModel.ttsStart(context) }, - onStop = { viewModel.ttsStop() }, - enabled = viewModel.textInput.isNotEmpty() - ) + TextToSpeechButton( + buttonStatus = uiState.buttonStatus, + activatedColor = activatedColor, + deactivatedColor = deactivatedColor, + onPlay = { viewModel.ttsStart(context) }, + onStop = { viewModel.ttsStop() }, + enabled = viewModel.textInput.isNotEmpty() + ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - TextClearButton( - activatedColor = activatedColor, - deactivatedColor = deactivatedColor, - onClick = { viewModel.clearText() }, - enabled = uiState.buttonStatus == ButtonStatusType.PLAY && !viewModel.textInput.isEmpty() - ) + TextClearButton( + activatedColor = activatedColor, + deactivatedColor = deactivatedColor, + onClick = { viewModel.clearText() }, + enabled = uiState.buttonStatus == ButtonStatusType.PLAY && viewModel.textInput.isNotEmpty() + ) - Spacer(modifier = Modifier.weight(1f)) - } + Spacer(modifier = Modifier.weight(1f)) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/TopAppBarUi.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/TopAppBarUi.kt deleted file mode 100644 index 09de2469..00000000 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/TopAppBarUi.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.example.speechbuddy.compose.utils - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -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.ui.SpeechBuddyTheme - -/** - * Custom UI designed for top app bars used in main activity. - * - * @param title title to be displayed in the center of this top app bar - * @param modifier the Modifier to be applied to this top app bar - * @param onBackClick called when the back icon of this top app bar is clicked - * @param isBackClickEnabled decides what to show in the left edge of this top app bar. If false, app logo is displayed instead. - * @param actions the actions displayed at the end of the top app bar - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopAppBarUi( - title: String, - modifier: Modifier = Modifier, - onBackClick: () -> Unit = {}, - isBackClickEnabled: Boolean = false, - actions: @Composable (RowScope.() -> Unit) = {} -) { - CenterAlignedTopAppBar( - modifier = modifier, - title = { - Text( - text = title, - style = MaterialTheme.typography.titleLarge - ) - }, - navigationIcon = { - if (isBackClickEnabled) - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = null - ) - } - else - Image( - painter = painterResource(id = R.drawable.speechbuddy_parrot), - contentDescription = stringResource(id = R.string.app_name), - modifier = Modifier - .padding(6.dp) - .size(40.dp), - contentScale = ContentScale.Fit - ) - }, - actions = actions, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - titleContentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) -} \ 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 ff0b4363..f3f98132 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 @@ -23,7 +23,13 @@ interface SymbolDao { @Query("SELECT * FROM symbols WHERE isFavorite = 1") fun getFavoriteSymbols(): Flow> - @Query("SELECT * FROM symbols WHERE text LIKE '%' || :query || '%'") + @Query("SELECT * FROM symbols WHERE isMine = 1") + fun getUserSymbols(): Flow> + + @Query("SELECT * FROM symbols WHERE isMine = 1 AND text LIKE '%' || :query || '%'") + fun getUserSymbolsByQuery(query: String): Flow> + + @Query("SELECT * FROM symbols WHERE text LIKE '%' || :query || '%'") fun getSymbolsByQuery(query: String): Flow> @Query("SELECT * FROM symbols WHERE isFavorite = 1 AND text LIKE '%' || :query || '%'") @@ -50,6 +56,9 @@ interface SymbolDao { @Upsert suspend fun upsertAll(symbolEntities: List) + @Query("DELETE FROM symbols WHERE id = :symbolId") + suspend fun deleteSymbolById(symbolId: Int) + @Query("DELETE FROM symbols WHERE isMine = 1") suspend fun deleteAllMySymbols() diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserDao.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserDao.kt index d80a7ff1..36c664e5 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserDao.kt @@ -24,7 +24,4 @@ interface UserDao { @Query("DELETE FROM users WHERE id = :id") suspend fun deleteUserById(id: Int) - - @Query("DELETE FROM users") - suspend fun deleteUserInfo() } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserIdPrefsManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserIdPrefsManager.kt index bc34ba12..ab4ddab0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserIdPrefsManager.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/UserIdPrefsManager.kt @@ -18,7 +18,7 @@ class UserIdPrefsManager @Inject constructor(context: Context) { private val dataStore = context.createDataStore(name = USER_ID_PREFS) - val preferencesFlow: Flow = dataStore.data + val preferencesFlow: Flow = dataStore.data .catch { exception -> if (exception is IOException) { emit(emptyPreferences()) @@ -27,7 +27,7 @@ class UserIdPrefsManager @Inject constructor(context: Context) { } } .map { preferences -> - preferences[USER_ID] ?: -1 + preferences[USER_ID] } suspend fun saveUserId(id: Int) { 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 2e0b8eba..26322f00 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 @@ -24,6 +24,12 @@ interface WeightRowDao { @Query("DELETE FROM weighttable WHERE id > 500") suspend fun deleteMySymbolsWeightRows() + @Query("DELETE FROM weighttable WHERE id = :rowId") + suspend fun deleteWeightRowById(rowId: Int) + + @Query("SELECT COUNT(*) FROM weighttable WHERE id < :rowId") + suspend fun getCountOfRowsWithIdLessThan(rowId: Int): Int + @Query("UPDATE weighttable SET weights = :resetWeights WHERE id < 501") suspend fun resetOriginalSymbolsWeightRows(resetWeights: List) } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/domain/SessionManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/domain/SessionManager.kt index cd6facce..cde2f16d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/domain/SessionManager.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/domain/SessionManager.kt @@ -4,13 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.domain.models.AuthToken +import com.example.speechbuddy.utils.Constants.Companion.GUEST_ID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class SessionManager { - private val _cachedToken = MutableLiveData() + private val _cachedToken = MutableLiveData(null) private val _userId = MutableLiveData(null) val cachedToken: LiveData @@ -29,7 +30,7 @@ class SessionManager { } private fun checkAuthorization(): Boolean { - return _cachedToken.value?.refreshToken != null || _userId.value == GUEST + return _cachedToken.value?.refreshToken != null || _userId.value == GUEST_ID } fun setAuthToken(value: AuthToken) { @@ -57,7 +58,7 @@ class SessionManager { fun enterGuestMode() { CoroutineScope(Dispatchers.Main).launch { - _userId.value = GUEST + _userId.value = GUEST_ID } } @@ -67,8 +68,4 @@ class SessionManager { } } - companion object { - const val GUEST = -1 - } - } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/AuthRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/AuthRepository.kt index ec3886c0..3c21f827 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/AuthRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/AuthRepository.kt @@ -1,7 +1,6 @@ package com.example.speechbuddy.repository import com.example.speechbuddy.data.local.AuthTokenPrefsManager -import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.local.UserIdPrefsManager import com.example.speechbuddy.data.remote.AuthTokenRemoteSource import com.example.speechbuddy.data.remote.models.AccessTokenDtoMapper @@ -35,7 +34,6 @@ class AuthRepository @Inject constructor( private val authService: AuthService, private val userIdPrefsManager: UserIdPrefsManager, private val authTokenPrefsManager: AuthTokenPrefsManager, - private val settingsPrefsManager: SettingsPrefsManager, private val authTokenRemoteSource: AuthTokenRemoteSource, private val authTokenDtoMapper: AuthTokenDtoMapper, private val accessTokenDtoMapper: AccessTokenDtoMapper, @@ -146,11 +144,10 @@ class AuthRepository @Inject constructor( val refreshToken = sessionManager.cachedToken.value!!.refreshToken!! val result = authService.logout(getAuthHeader(), AuthRefreshRequest(refreshToken)) - CoroutineScope(Dispatchers.IO).launch { - userIdPrefsManager.clearUserId() - authTokenPrefsManager.clearAuthToken() - settingsPrefsManager.resetSettings() - } + if (result.isSuccessful && result.code() == ResponseCode.SUCCESS.value) + CoroutineScope(Dispatchers.IO).launch { + authTokenPrefsManager.clearAuthToken() + } emit(result) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) @@ -163,11 +160,10 @@ class AuthRepository @Inject constructor( val refreshToken = sessionManager.cachedToken.value!!.refreshToken!! val result = authService.withdraw(getAuthHeader(), AuthRefreshRequest(refreshToken)) - CoroutineScope(Dispatchers.IO).launch { - userIdPrefsManager.clearUserId() - authTokenPrefsManager.clearAuthToken() - settingsPrefsManager.resetSettings() - } + if (result.isSuccessful && result.code() == ResponseCode.SUCCESS.value) + CoroutineScope(Dispatchers.IO).launch { + authTokenPrefsManager.clearAuthToken() + } emit(result) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) @@ -180,8 +176,10 @@ class AuthRepository @Inject constructor( ) { userId, authToken -> Pair(userId, authToken) }.map { pair -> - if (pair.first != -1 && !pair.second.accessToken.isNullOrEmpty() && !pair.second.refreshToken.isNullOrEmpty()) - Resource.success(pair) + val userId = pair.first + val authToken = pair.second + if (userId != null && authToken.accessToken!!.isNotEmpty() && authToken.refreshToken!!.isNotEmpty()) + Resource.success(Pair(userId, authToken)) else Resource.error("Couldn't find previous user", null) } 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 index cc99d957..df3cba70 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt @@ -1,7 +1,5 @@ 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.SettingsRemoteSource import com.example.speechbuddy.data.remote.models.SettingsBackupDto @@ -27,7 +25,7 @@ import javax.inject.Singleton @Singleton class SettingsRepository @Inject constructor( - private val settingsPrefManager: SettingsPrefsManager, + private val settingsPrefsManager: SettingsPrefsManager, private val backupService: BackupService, private val responseHandler: ResponseHandler, private val sessionManager: SessionManager, @@ -38,61 +36,63 @@ class SettingsRepository @Inject constructor( ) { suspend fun setDarkMode(value: Boolean) { if (value) { - settingsPrefManager.saveDarkMode(true) + settingsPrefsManager.saveDarkMode(true) } else { - settingsPrefManager.saveDarkMode(false) + settingsPrefsManager.saveDarkMode(false) } - settingsPrefManager.saveDarkMode(value) + settingsPrefsManager.saveDarkMode(value) } suspend fun setInitialPage(page: InitialPage) { if (page == InitialPage.SYMBOL_SELECTION) { - settingsPrefManager.saveInitialPage(true) + settingsPrefsManager.saveInitialPage(true) } else { - settingsPrefManager.saveInitialPage(false) + settingsPrefsManager.saveInitialPage(false) } } suspend fun setAutoBackup(value: Boolean) { - settingsPrefManager.saveAutoBackup(value) + settingsPrefsManager.saveAutoBackup(value) } suspend fun setLastBackupDate(value: String) { - settingsPrefManager.saveLastBackupDate(value) + settingsPrefsManager.saveLastBackupDate(value) } fun getDarkMode(): Flow> { - return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + return settingsPrefsManager.settingsPreferencesFlow.map { settingsPreferences -> Resource.success(settingsPreferences.darkMode) } } fun getDarkModeForChange(): Flow { - return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + return settingsPrefsManager.settingsPreferencesFlow.map { settingsPreferences -> settingsPreferences.darkMode } } fun getInitialPage(): Flow> { - return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + return settingsPrefsManager.settingsPreferencesFlow.map { settingsPreferences -> Resource.success(settingsPreferences.initialPage) } } fun getAutoBackup(): Flow> { - return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + return settingsPrefsManager.settingsPreferencesFlow.map { settingsPreferences -> Resource.success(settingsPreferences.autoBackup) } } fun getLastBackupDate(): Flow> { - return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + return settingsPrefsManager.settingsPreferencesFlow.map { settingsPreferences -> Resource.success(settingsPreferences.lastBackupDate) } } suspend fun resetSettings() { - settingsPrefManager.resetSettings() + CoroutineScope(Dispatchers.IO).launch { + settingsPrefsManager.resetSettings() + } } suspend fun displayBackup(): Flow> = @@ -243,7 +243,6 @@ class SettingsRepository @Inject constructor( for (symbolIdDto in favoritesListDto.results) { val symbol = symbolRepository.getSymbolsById(symbolIdDto.id) symbolRepository.updateFavorite(symbol, true) - } Resource.success(null) } ?: returnUnknownError() 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 c737c1c9..73fde1c0 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 @@ -16,30 +16,29 @@ 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.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.MultipartBody import javax.inject.Inject import javax.inject.Singleton - @Singleton class SymbolRepository @Inject constructor( private val symbolDao: SymbolDao, private val categoryDao: CategoryDao, - // For remote-related processing private val mySymbolRemoteSource: MySymbolRemoteSource, private val mySymbolDtoMapper: MySymbolDtoMapper, private val responseHandler: ResponseHandler, private val sessionManager: SessionManager, - // For local mapping private val symbolMapper: SymbolMapper, private val categoryMapper: CategoryMapper, - // For image download private val proxyImageDownloader: ProxyImageDownloader ) { @@ -77,6 +76,16 @@ class SymbolRepository @Inject constructor( symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } + fun getUserSymbols(query: String) = + if (query.isBlank()) getAllUserSymbols() + else symbolDao.getUserSymbolsByQuery(query).map { symbolEntities -> + symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } + } + + private fun getAllUserSymbols() = symbolDao.getUserSymbols().map { symbolEntities -> + symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } + } + fun getSymbolsById(id: Int): Symbol { return runBlocking { symbolMapper.mapToDomainModel(symbolDao.getSymbolById(id).first()) } } @@ -120,18 +129,17 @@ class SymbolRepository @Inject constructor( ) symbolDao.updateSymbol(symbolEntity) } - } fun getNextSymbolId() = symbolDao.getLastSymbol().map { symbol -> symbol.id + 1 } - suspend fun clearAllMySymbols() { - symbolDao.deleteAllMySymbols() - } - - suspend fun resetFavoriteSymbols() { - symbolDao.resetFavoriteSymbols() + suspend fun resetSymbolsAndFavorites() { + CoroutineScope(Dispatchers.IO).launch { + // execute the following dao updates sequentially + symbolDao.deleteAllMySymbols() + symbolDao.resetFavoriteSymbols() + } } fun insertSymbol(symbol: Symbol) { @@ -141,6 +149,10 @@ class SymbolRepository @Inject constructor( } } + suspend fun deleteSymbol(symbol: Symbol) { + symbolDao.deleteSymbolById(symbol.id) + } + suspend fun createSymbolBackup( symbolText: String, categoryId: Int, @@ -184,4 +196,5 @@ class SymbolRepository @Inject constructor( "Unknown error", null ) } -} + +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt index 6e729e4a..7cc050ba 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/UserRepository.kt @@ -7,11 +7,15 @@ import com.example.speechbuddy.data.remote.UserRemoteSource import com.example.speechbuddy.data.remote.models.UserDtoMapper import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.domain.models.User +import com.example.speechbuddy.utils.Constants.Companion.GUEST_ID import com.example.speechbuddy.utils.Resource import com.example.speechbuddy.utils.ResponseCode import com.example.speechbuddy.utils.ResponseHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -27,7 +31,7 @@ class UserRepository @Inject constructor( ) { fun getMyInfo(): Flow> { - return userDao.getUserById(sessionManager.userId.value ?: -1).map { userEntity -> + return userDao.getUserById(sessionManager.userId.value!!).map { userEntity -> if (userEntity != null) Resource.success(userMapper.mapToDomainModel(userEntity)) else Resource.error("Unable to find user", null) } @@ -54,8 +58,17 @@ class UserRepository @Inject constructor( } } + fun setGuestMode() { + CoroutineScope(Dispatchers.IO).launch { + userIdPrefsManager.saveUserId(GUEST_ID) + } + } + suspend fun deleteUserInfo() { - userDao.deleteUserInfo() + CoroutineScope(Dispatchers.IO).launch { + userDao.deleteUserById(sessionManager.userId.value!!) + userIdPrefsManager.clearUserId() + } } private fun returnUnknownError(): Resource { 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 b0f35dfa..bb939c0d 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton - @Singleton class WeightTableRepository @Inject constructor( private val symbolDao: SymbolDao, @@ -34,7 +33,6 @@ class WeightTableRepository @Inject constructor( private val weightRowMapper = WeightRowMapper() private var allSymbols = getAllSymbols() - private fun getAllSymbols() = symbolDao.getSymbols().map { symbolEntities -> symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } @@ -47,8 +45,10 @@ class WeightTableRepository @Inject constructor( } suspend fun resetAllWeightRows() { - weightRowDao.deleteMySymbolsWeightRows() - weightRowDao.resetOriginalSymbolsWeightRows(List(500) { 0 }) + CoroutineScope(Dispatchers.IO).launch { + weightRowDao.deleteMySymbolsWeightRows() + weightRowDao.resetOriginalSymbolsWeightRows(List(500) { 0 }) + } } suspend fun replaceWeightTable(weightRowList: List) { @@ -114,6 +114,29 @@ class WeightTableRepository @Inject constructor( } } + fun updateWeightTableForDeletedSymbol(symbol: Symbol) { + CoroutineScope(Dispatchers.IO).launch { + val deletedSymbolIdx = weightRowDao.getCountOfRowsWithIdLessThan(symbol.id) + + val weightRows = mutableListOf() + weightRows.clear() + weightRows.addAll(fetchWeightRows()) + + // update existing weightRows + for (weightRow in weightRows) { + val newWeights = mutableListOf() + newWeights.addAll(weightRow.weights) + newWeights.removeAt(deletedSymbolIdx) + updateWeightRow(weightRow, newWeights) + } + + // delete weightRow for the deleted symbol + weightRowDao.deleteWeightRowById(symbol.id) + + allSymbols = getAllSymbols() + } + } + fun provideSuggestion(symbol: Symbol): Flow> = flow { val allSymbolList = allSymbols.first() val oneWeightRow = getWeightRowById(symbol.id).first() diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/MySymbolSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/MySymbolSettingsUiState.kt new file mode 100644 index 00000000..7b32fdc2 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/MySymbolSettingsUiState.kt @@ -0,0 +1,10 @@ +package com.example.speechbuddy.ui.models + +data class MySymbolSettingsUiState( + val mySymbolSettingsDisplayMode: MySymbolSettingsDisplayMode = MySymbolSettingsDisplayMode.SYMBOL +) + +enum class MySymbolSettingsDisplayMode { + SYMBOL, + FAVORITE +} \ No newline at end of file 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 2b2a3ec9..72245e87 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 @@ -31,5 +31,7 @@ class Constants { const val MAXIMUM_SYMBOL_TEXT_LENGTH = 20 const val DEFAULT_SYMBOL_COUNT = 500 + + const val GUEST_ID = -1 } } \ No newline at end of file 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 9b4f0206..407af03c 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 @@ -4,7 +4,6 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.speechbuddy.data.local.AuthTokenPrefsManager import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.repository.AuthRepository import com.example.speechbuddy.repository.SettingsRepository @@ -31,8 +30,7 @@ class AccountSettingsViewModel @Inject internal constructor( private val weightTableRepository: WeightTableRepository, private val symbolRepository: SymbolRepository, private val userRepository: UserRepository, - private val sessionManager: SessionManager, - private val authTokenPrefsManager: AuthTokenPrefsManager + private val sessionManager: SessionManager ) : ViewModel() { private val _uiState = MutableStateFlow(AccountSettingsUiState()) @@ -78,11 +76,10 @@ class AccountSettingsViewModel @Inject internal constructor( ResponseCode.SUCCESS.value -> { settingsRepository.resetSettings() weightTableRepository.resetAllWeightRows() - symbolRepository.clearAllMySymbols() - symbolRepository.resetFavoriteSymbols() + symbolRepository.resetSymbolsAndFavorites() userRepository.deleteUserInfo() sessionManager.deleteToken() - authTokenPrefsManager.clearAuthToken() + // authToken is already cleared by the repository hideAlert() } @@ -102,11 +99,10 @@ class AccountSettingsViewModel @Inject internal constructor( ResponseCode.SUCCESS.value -> { settingsRepository.resetSettings() weightTableRepository.resetAllWeightRows() - symbolRepository.clearAllMySymbols() - symbolRepository.resetFavoriteSymbols() + symbolRepository.resetSymbolsAndFavorites() userRepository.deleteUserInfo() sessionManager.deleteToken() - authTokenPrefsManager.clearAuthToken() + // authToken is already cleared by the repository hideAlert() } @@ -120,6 +116,10 @@ class AccountSettingsViewModel @Inject internal constructor( fun exitGuestMode() { viewModelScope.launch { + settingsRepository.resetSettings() + weightTableRepository.resetAllWeightRows() + symbolRepository.resetSymbolsAndFavorites() + userRepository.deleteUserInfo() sessionManager.exitGuestMode() } } @@ -129,21 +129,23 @@ class AccountSettingsViewModel @Inject internal constructor( settingsRepository.displayBackup().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> {} - - ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + 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() } + ResponseCode.NO_INTERNET_CONNECTION.value -> { + handleNoInternetConnection() + } } } @@ -157,7 +159,9 @@ class AccountSettingsViewModel @Inject internal constructor( when (result.code()) { ResponseCode.SUCCESS.value -> {} - ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + ResponseCode.NO_INTERNET_CONNECTION.value -> { + handleNoInternetConnection() + } } } } @@ -168,9 +172,13 @@ class AccountSettingsViewModel @Inject internal constructor( viewModelScope.launch { settingsRepository.weightTableBackup().collect { result -> when (result.code()) { - ResponseCode.SUCCESS.value -> { handleSuccess() } + ResponseCode.SUCCESS.value -> { + handleSuccess() + } - ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + ResponseCode.NO_INTERNET_CONNECTION.value -> { + handleNoInternetConnection() + } } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt index e24898e7..cd4b72a8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt @@ -355,6 +355,7 @@ class LoginViewModel @Inject internal constructor( fun enterGuestMode() { viewModelScope.launch { + userRepository.setGuestMode() sessionManager.enterGuestMode() } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/MySymbolSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/MySymbolSettingsViewModel.kt new file mode 100644 index 00000000..43f49740 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/MySymbolSettingsViewModel.kt @@ -0,0 +1,113 @@ +package com.example.speechbuddy.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.speechbuddy.domain.models.Symbol +import com.example.speechbuddy.repository.SymbolRepository +import com.example.speechbuddy.repository.WeightTableRepository +import com.example.speechbuddy.ui.models.MySymbolSettingsDisplayMode +import com.example.speechbuddy.ui.models.MySymbolSettingsUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +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 MySymbolSettingsViewModel @Inject internal constructor( + private val weightTableRepository: WeightTableRepository, + private val symbolRepository: SymbolRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(MySymbolSettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _symbols = MutableLiveData>() + val symbols: LiveData> get() = _symbols + + private val _checkedSymbols = mutableStateListOf() + val checkedSymbols: List get() = _checkedSymbols + + var queryInput by mutableStateOf("") + private set + + private var getSymbolsJob: Job? = null + + init { + getSymbols() + } + + fun setQuery(input: String) { + queryInput = input + getSymbols(input) + } + + fun toggleSymbolChecked(symbol: Symbol) { + if (_checkedSymbols.contains(symbol)) { + _checkedSymbols.remove(symbol) + } else { + _checkedSymbols.add(symbol) + } + } + + fun updateFavorite(symbol: Symbol, value: Boolean) { + viewModelScope.launch { + symbolRepository.updateFavorite(symbol, value) + } + } + + fun selectDisplayMode(displayMode: MySymbolSettingsDisplayMode) { + _uiState.update { currentState -> + currentState.copy( + mySymbolSettingsDisplayMode = displayMode + ) + } + getSymbols() + } + + fun deleteCheckedSymbols() { + viewModelScope.launch { + val checkedSymbols = _checkedSymbols.toList() + for (symbol in checkedSymbols) { + symbolRepository.deleteSymbol(symbol) + weightTableRepository.updateWeightTableForDeletedSymbol(symbol) + } + _checkedSymbols.clear() + } + } + + private fun getSymbols(query: String? = null) { + getSymbolsJob?.cancel() + getSymbolsJob = viewModelScope.launch { + when (_uiState.value.mySymbolSettingsDisplayMode) { + MySymbolSettingsDisplayMode.SYMBOL -> { + if (query != null) { + symbolRepository.getUserSymbols(query).collect { symbols -> + _symbols.postValue(symbols) + } + } else { + symbolRepository.getUserSymbols(queryInput).collect { symbols -> + _symbols.postValue(symbols) + } + } + } + + MySymbolSettingsDisplayMode.FAVORITE -> { + symbolRepository.getFavoriteSymbols(queryInput).collect { symbols -> + _symbols.postValue(symbols) + } + } + } + } + } + +} diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/SymbolSelectionViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/SymbolSelectionViewModel.kt index e08e032f..0cf4afc3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/SymbolSelectionViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/SymbolSelectionViewModel.kt @@ -45,6 +45,7 @@ class SymbolSelectionViewModel @Inject internal constructor( val entries: LiveData> get() = _entries private var getEntriesJob: Job? = null + private var needsToBeRecalled: Boolean = false init { repository.checkImages() @@ -73,6 +74,7 @@ class SymbolSelectionViewModel @Inject internal constructor( displayMode = displayMode ) } + queryInput = "" getEntries() } @@ -87,11 +89,10 @@ class SymbolSelectionViewModel @Inject internal constructor( fun clear(symbolItem: SymbolItem) { selectedSymbols = selectedSymbols.minus(symbolItem) - // when clearing one left selected symbol - if (selectedSymbols.isNotEmpty()) { - val lastSelectedSymbol = selectedSymbols.last() - provideSuggestion(lastSelectedSymbol.symbol) - } + if (selectedSymbols.isEmpty()) + getEntries() + else + provideSuggestion(selectedSymbols.last().symbol) } fun clearAll() { @@ -114,12 +115,23 @@ class SymbolSelectionViewModel @Inject internal constructor( viewModelScope.launch { repository.updateFavorite(symbol, value) } + if (needsToBeRecalled) { + /** + * selectedSymbols should always be NOT EMPTY in this case, + * thus executing provideSuggestion() as expected. + */ + if (selectedSymbols.isEmpty()) + getEntries() + else + provideSuggestion(selectedSymbols.last().symbol) + } } fun selectCategory(category: Category) { if (category != selectedCategory) { selectedCategory = category getEntriesJob?.cancel() + needsToBeRecalled = false getEntriesJob = viewModelScope.launch { repository.getSymbolsByCategory(category).collect { symbols -> _entries.postValue(listOf(category) + symbols) @@ -132,21 +144,22 @@ class SymbolSelectionViewModel @Inject internal constructor( } private fun provideSuggestion(symbol: Symbol) { - // became independent from selectSymbol function - // change it so that providing suggestion is available from any screen -// if (uiState.value.displayMode == DisplayMode.SYMBOL) { - + /** + * became independent from selectSymbol function + * change it so that providing suggestion is available from any screen + */ getEntriesJob?.cancel() + needsToBeRecalled = true getEntriesJob = viewModelScope.launch { weightTableRepository.provideSuggestion(symbol).collect { symbols -> _entries.postValue(symbols) } } -// } } private fun getEntries(query: String? = null) { getEntriesJob?.cancel() + needsToBeRecalled = false getEntriesJob = viewModelScope.launch { when (_uiState.value.displayMode) { DisplayMode.ALL -> { @@ -161,7 +174,7 @@ class SymbolSelectionViewModel @Inject internal constructor( * retrieve both symbols and categories from the repository */ DisplayMode.SYMBOL -> { - if (query != null) // called from setQuery() + if (!query.isNullOrEmpty()) // called from setQuery() repository.getEntries(query).collect { entries -> _entries.postValue(entries) } @@ -172,7 +185,7 @@ class SymbolSelectionViewModel @Inject internal constructor( } DisplayMode.CATEGORY -> { - if (query != null) + if (!query.isNullOrEmpty()) repository.getEntries(query).collect { entries -> _entries.postValue(entries) } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/worker/SeedDatabaseWorker.kt b/frontend/app/src/main/java/com/example/speechbuddy/worker/SeedDatabaseWorker.kt index 2c03ba82..c5d521d8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/worker/SeedDatabaseWorker.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/worker/SeedDatabaseWorker.kt @@ -24,7 +24,7 @@ class SeedDatabaseWorker( override suspend fun doWork(): Result = withContext(Dispatchers.IO) { try { val database = AppDatabase.getInstance(applicationContext) - + val weightRows = mutableListOf() applicationContext.assets.open("weight_table.txt").use { inputStream -> @@ -42,13 +42,10 @@ class SeedDatabaseWorker( } } - database.weightRowDao().upsertAll(weightRows) - - - + Result.success() - + applicationContext.assets.open(SYMBOL_DATA_FILENAME).use { inputStream -> JsonReader(inputStream.reader()).use { jsonReader -> @@ -73,8 +70,6 @@ class SeedDatabaseWorker( Result.success() } } - - } catch (ex: Exception) { Log.e("SeedDatabaseWorker", "Error seeding database", ex) Result.failure() diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index a898b4c3..4c2c0611 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -128,6 +128,8 @@ 상징 목록 관리 + 삭제 + 내가 만든 상징 버전 정보