From c5d3a004d7ae094e9ea2bce22e62b1ad4a95de5e Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Sun, 12 Nov 2023 17:17:06 +0900 Subject: [PATCH 01/19] :sparkles: Implement setting initial page feature --- .../com/example/speechbuddy/HomeActivity.kt | 15 +++- .../speechbuddy/compose/SpeechBuddyHome.kt | 18 +++-- .../data/local/SettingsPrefsManager.kt | 71 +++++++++++++++++++ .../example/speechbuddy/di/DatabaseModule.kt | 7 ++ .../domain/models/SettingsPreferences.kt | 11 +++ .../repository/SettingsRepository.kt | 48 +++++++++++++ .../ui/models/DisplaySettingsUiState.kt | 3 +- .../example/speechbuddy/utils/Constants.kt | 5 ++ .../viewmodel/BackupSettingsViewModel.kt | 8 ++- .../viewmodel/DisplaySettingsViewModel.kt | 43 ++++++++++- 10 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt diff --git a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt index 926f9569..1278e776 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt @@ -3,14 +3,19 @@ package com.example.speechbuddy import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.core.view.WindowCompat import com.example.speechbuddy.compose.SpeechBuddyHome import com.example.speechbuddy.ui.SpeechBuddyTheme +import com.example.speechbuddy.viewmodel.DisplaySettingsViewModel +import com.example.speechbuddy.viewmodel.LoginViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class HomeActivity : BaseActivity() { + private val displaySettingsViewModel: DisplaySettingsViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -19,7 +24,7 @@ class HomeActivity : BaseActivity() { setContent { SpeechBuddyTheme { - SpeechBuddyHome() + SpeechBuddyHome(getInitialPage()) } } @@ -38,4 +43,12 @@ class HomeActivity : BaseActivity() { finish() } + private fun getDarkMode(): Boolean { + return displaySettingsViewModel.getDarkMode() + } + + private fun getInitialPage(): Boolean { + return displaySettingsViewModel.getInitialPage() + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt index 5899a1f1..5a865d79 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt @@ -38,7 +38,9 @@ data class BottomNavItem( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SpeechBuddyHome() { +fun SpeechBuddyHome( + initialPage: Boolean +) { val navController = rememberNavController() val navItems = listOf( BottomNavItem( @@ -77,7 +79,8 @@ fun SpeechBuddyHome() { ) { paddingValues -> SpeechBuddyHomeNavHost( navController = navController, - bottomPaddingValues = paddingValues + bottomPaddingValues = paddingValues, + initialPage = initialPage ) } } @@ -120,9 +123,16 @@ private fun BottomNavigationBar( @Composable private fun SpeechBuddyHomeNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + initialPage: Boolean ) { - NavHost(navController = navController, startDestination = "symbol_selection") { + val startDestination = if (initialPage) { + "symbol_selection" + } else { + "text_to_speech" + } + + NavHost(navController = navController, startDestination = startDestination) { composable("symbol_selection") { SymbolSelectionScreen( bottomPaddingValues = bottomPaddingValues diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt new file mode 100644 index 00000000..0adb9bbe --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt @@ -0,0 +1,71 @@ +package com.example.speechbuddy.data.local + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.createDataStore +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.AUTO_BACKUP +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.DARK_MODE +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.INITIAL_PAGE +import com.example.speechbuddy.domain.models.SettingsPreferences +import com.example.speechbuddy.utils.Constants +import com.example.speechbuddy.utils.Constants.Companion.SETTINGS_PREFS +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException +import javax.inject.Inject + +class SettingsPrefsManager @Inject constructor(context: Context) { + + private val dataStore = context.createDataStore(name = SETTINGS_PREFS) + + val settingsPreferencesFlow: Flow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { preferences -> + SettingsPreferences( + autoBackup = preferences[AUTO_BACKUP] ?: true, + darkMode = preferences[DARK_MODE] ?: false, + initialPage = preferences[INITIAL_PAGE] ?: true + ) + } + + suspend fun saveAutoBackup(value: Boolean) { + dataStore.edit { preferences -> + preferences[AUTO_BACKUP] = value + } + } + + suspend fun saveDarkMode(value: Boolean) { + dataStore.edit { preferences -> + preferences[DARK_MODE] = value + } + } + + suspend fun saveInitialPage(value: Boolean) { + dataStore.edit { preferences -> + preferences[INITIAL_PAGE] = value + } + } + + suspend fun resetSettings() { + dataStore.edit { preferences -> + preferences[AUTO_BACKUP] = true + preferences[DARK_MODE] = false + preferences[INITIAL_PAGE] = true + } + } + + private object PreferencesKeys { + val AUTO_BACKUP = booleanPreferencesKey(Constants.AUTO_BACKUP_PREF) + val DARK_MODE = booleanPreferencesKey(Constants.DARK_MODE_PREF) + val INITIAL_PAGE = booleanPreferencesKey(Constants.INITIAL_PAGE_PREF) + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt index f8888a86..c6ac189e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt @@ -4,6 +4,7 @@ import android.content.Context import com.example.speechbuddy.data.local.AppDatabase import com.example.speechbuddy.data.local.AuthTokenPrefsManager import com.example.speechbuddy.data.local.CategoryDao +import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.local.SymbolDao import dagger.Module import dagger.Provides @@ -38,4 +39,10 @@ class DatabaseModule { return AuthTokenPrefsManager(context) } + @Singleton + @Provides + fun provideSettingsPrefsManager(@ApplicationContext context: Context): SettingsPrefsManager { + return SettingsPrefsManager(context) + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt new file mode 100644 index 00000000..a99d91fb --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt @@ -0,0 +1,11 @@ +package com.example.speechbuddy.domain.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SettingsPreferences( + val darkMode: Boolean = false, + val autoBackup: Boolean = true, + val initialPage: Boolean = true +) : Parcelable \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt new file mode 100644 index 00000000..8a1b244b --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt @@ -0,0 +1,48 @@ +package com.example.speechbuddy.repository + +import com.example.speechbuddy.data.local.SettingsPrefsManager +import com.example.speechbuddy.domain.models.AuthToken +import com.example.speechbuddy.ui.models.InitialPage +import com.example.speechbuddy.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class SettingsRepository @Inject constructor( + private val settingsPrefManager: SettingsPrefsManager +) { + suspend fun setDarkMode(value: Boolean) { + settingsPrefManager.saveDarkMode(value) + } + + suspend fun setInitialPage(page: InitialPage) { + if (page == InitialPage.SYMBOL_SELECTION) { + settingsPrefManager.saveInitialPage(true) + } else { + settingsPrefManager.saveInitialPage(false) + } + } + + suspend fun setAutoBackup(value: Boolean) { + settingsPrefManager.saveAutoBackup(value) + } + + fun getDarkMode(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.darkMode) + } + } + + fun getInitialPage(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.initialPage) + } + } + + fun getAutoBackup(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.autoBackup) + } + } + +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt index db1d4277..196363ee 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/DisplaySettingsUiState.kt @@ -1,8 +1,7 @@ package com.example.speechbuddy.ui.models data class DisplaySettingsUiState( - /* TODO */ - val isDarkModeEnabled: Boolean = true, + val isDarkModeEnabled: Boolean = false, val initialPage: InitialPage = InitialPage.SYMBOL_SELECTION ) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt b/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt index 65e4a7cd..f612eff5 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt @@ -15,6 +15,11 @@ class Constants { const val ACCESS_TOKEN_PREF: String = "com.example.speechbuddy.ACCESS_TOKEN_PREF" const val REFRESH_TOKEN_PREF: String = "com.example.speechbuddy.REFRESH_TOKEN_PREF" + const val SETTINGS_PREFS: String = "com.example.speechbuddy.SETTINGS_PREFS" + const val AUTO_BACKUP_PREF: String = "com.example.speechbuddy.AUTO_BACKUP_PREF" + const val DARK_MODE_PREF: String = "com.example.speechbuddy.DARK_MODE_PREF" + const val INITIAL_PAGE_PREF: String = "com.example.speechbuddy.INITIAL_PAGE_PREF" + const val MINIMUM_PASSWORD_LENGTH = 8 const val MAXIMUM_NICKNAME_LENGTH = 15 const val CODE_LENGTH = 6 diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 6a76ad0b..9b78b6dd 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -2,7 +2,9 @@ package com.example.speechbuddy.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import com.example.speechbuddy.repository.AuthRepository +import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.ui.models.BackupSettingsUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +16,7 @@ import javax.inject.Inject @HiltViewModel class BackupSettingsViewModel @Inject internal constructor( - private val repository: AuthRepository + private val repository: SettingsRepository ) : ViewModel() { private val _uiState = MutableStateFlow(BackupSettingsUiState()) @@ -26,6 +28,10 @@ class BackupSettingsViewModel @Inject internal constructor( isAutoBackupEnabled = value ) } + viewModelScope.launch { + repository.setAutoBackup(value) + } + // TODO: Implement automated backup } fun backup() { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt index 4236ddf8..b3581066 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.ui.models.DisplaySettingsUiState import com.example.speechbuddy.ui.models.InitialPage import dagger.hilt.android.lifecycle.HiltViewModel @@ -8,12 +10,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { +class DisplaySettingsViewModel @Inject internal constructor( + private val repository: SettingsRepository +) : ViewModel() { - private val _uiState = MutableStateFlow(DisplaySettingsUiState()) + private val initialPageBoolean = getInitialPage() + private val initialInitialPage = if (initialPageBoolean) { + InitialPage.SYMBOL_SELECTION + } else { + InitialPage.TEXT_TO_SPEECH + } + + private val _uiState = MutableStateFlow(DisplaySettingsUiState( + getDarkMode(), initialInitialPage)) val uiState: StateFlow = _uiState.asStateFlow() fun setDarkMode(value: Boolean) { @@ -22,6 +35,9 @@ class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { isDarkModeEnabled = value ) } + viewModelScope.launch { + repository.setDarkMode(value) + } } fun setInitialPage(page: InitialPage) { @@ -30,5 +46,28 @@ class DisplaySettingsViewModel @Inject internal constructor() : ViewModel() { initialPage = page ) } + viewModelScope.launch { + repository.setInitialPage(page) + } + } + + fun getDarkMode(): Boolean { + var darkMode: Boolean = false + viewModelScope.launch { + repository.getDarkMode().collect { + darkMode = it.data?: false + } + } + return darkMode + } + + fun getInitialPage(): Boolean { + var initialPage: Boolean = true + viewModelScope.launch { + repository.getInitialPage().collect { + initialPage = it.data?: true + } + } + return initialPage } } \ No newline at end of file From e5d0d511c4202ccef819c01b575329411e044f1b Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Sun, 12 Nov 2023 21:13:00 +0900 Subject: [PATCH 02/19] :sparkles: Implement dark mode feature --- .../com/example/speechbuddy/BaseActivity.kt | 4 +++ .../com/example/speechbuddy/HomeActivity.kt | 32 ++++++++++++++++--- .../speechbuddy/compose/SpeechBuddyHome.kt | 25 +++++++++------ .../compose/settings/SettingsScreen.kt | 13 +++++--- .../data/local/SettingsPrefsManager.kt | 7 ++++ .../repository/SettingsRepository.kt | 19 ++++++++++- 6 files changed, 82 insertions(+), 18 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt index efcdf664..35fc7dfc 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/BaseActivity.kt @@ -2,6 +2,7 @@ package com.example.speechbuddy import androidx.appcompat.app.AppCompatActivity import com.example.speechbuddy.domain.SessionManager +import com.example.speechbuddy.repository.SettingsRepository import javax.inject.Inject abstract class BaseActivity : AppCompatActivity() { @@ -9,4 +10,7 @@ abstract class BaseActivity : AppCompatActivity() { @Inject lateinit var sessionManager: SessionManager + @Inject + lateinit var settingsRepository: SettingsRepository + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt index 1278e776..77d3e7cc 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt @@ -1,34 +1,51 @@ package com.example.speechbuddy +import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.core.view.WindowCompat +import androidx.lifecycle.Observer import com.example.speechbuddy.compose.SpeechBuddyHome import com.example.speechbuddy.ui.SpeechBuddyTheme import com.example.speechbuddy.viewmodel.DisplaySettingsViewModel -import com.example.speechbuddy.viewmodel.LoginViewModel import dagger.hilt.android.AndroidEntryPoint + @AndroidEntryPoint class HomeActivity : BaseActivity() { private val displaySettingsViewModel: DisplaySettingsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) // Displaying edge-to-edge WindowCompat.setDecorFitsSystemWindows(window, false) + val isBeingReloadedForDarkModeChange = intent.getBooleanExtra("isBeingReloadedForDarkModeChange", false) + setContent { - SpeechBuddyTheme { - SpeechBuddyHome(getInitialPage()) + SpeechBuddyTheme( + darkTheme = getDarkMode() + ) { + SpeechBuddyHome(getInitialPage(), isBeingReloadedForDarkModeChange) } } subscribeObservers() + + val previousDarkMode = getDarkMode() + + val darkModeObserver = Observer { darkMode -> + if (darkMode != previousDarkMode) { + recreateHomeActivity() + } + } + + settingsRepository.darkModeLiveData.observeForever(darkModeObserver) } private fun subscribeObservers() { @@ -43,6 +60,13 @@ class HomeActivity : BaseActivity() { finish() } + private fun recreateHomeActivity() { + val intent = Intent(this, HomeActivity::class.java) + intent.putExtra("isBeingReloadedForDarkModeChange", true) + startActivity(intent) + finish() + } + private fun getDarkMode(): Boolean { return displaySettingsViewModel.getDarkMode() } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt index 5a865d79..6051fe3e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt @@ -39,7 +39,8 @@ data class BottomNavItem( @OptIn(ExperimentalMaterial3Api::class) @Composable fun SpeechBuddyHome( - initialPage: Boolean + initialPage: Boolean, + isBeingReloadedForDarkModeChange: Boolean ) { val navController = rememberNavController() val navItems = listOf( @@ -80,7 +81,8 @@ fun SpeechBuddyHome( SpeechBuddyHomeNavHost( navController = navController, bottomPaddingValues = paddingValues, - initialPage = initialPage + initialPage = initialPage, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } } @@ -124,13 +126,17 @@ private fun BottomNavigationBar( private fun SpeechBuddyHomeNavHost( navController: NavHostController, bottomPaddingValues: PaddingValues, - initialPage: Boolean + initialPage: Boolean, + isBeingReloadedForDarkModeChange: Boolean ) { - val startDestination = if (initialPage) { - "symbol_selection" - } else { - "text_to_speech" - } + val startDestination = if (isBeingReloadedForDarkModeChange) { + "settings" + } else if (initialPage) { + "symbol_selection" + } else { + "text_to_speech" + } + NavHost(navController = navController, startDestination = startDestination) { composable("symbol_selection") { @@ -150,7 +156,8 @@ private fun SpeechBuddyHomeNavHost( } composable("settings") { SettingsScreen( - bottomPaddingValues = bottomPaddingValues + bottomPaddingValues = bottomPaddingValues, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt index b889da76..afe36d01 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt @@ -1,5 +1,6 @@ package com.example.speechbuddy.compose.settings +import android.util.Log import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.navigation.NavHostController @@ -9,23 +10,27 @@ import androidx.navigation.compose.rememberNavController @Composable fun SettingsScreen( - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + isBeingReloadedForDarkModeChange: Boolean ) { val navController = rememberNavController() SettingsScreenNavHost( navController = navController, - bottomPaddingValues = bottomPaddingValues + bottomPaddingValues = bottomPaddingValues, + isBeingReloadedForDarkModeChange = isBeingReloadedForDarkModeChange ) } @Composable private fun SettingsScreenNavHost( navController: NavHostController, - bottomPaddingValues: PaddingValues + bottomPaddingValues: PaddingValues, + isBeingReloadedForDarkModeChange: Boolean ) { + val startDestination = if (isBeingReloadedForDarkModeChange) "display" else "main" val navigateToMain = { navController.navigate("main") } - NavHost(navController = navController, startDestination = "main") { + NavHost(navController = navController, startDestination = startDestination) { composable("main") { MainSettings( navController = navController, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt index 0adb9bbe..0db33489 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt @@ -1,21 +1,28 @@ package com.example.speechbuddy.data.local import android.content.Context +import android.util.Log import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.createDataStore +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.AUTO_BACKUP import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.DARK_MODE import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.INITIAL_PAGE import com.example.speechbuddy.domain.models.SettingsPreferences import com.example.speechbuddy.utils.Constants import com.example.speechbuddy.utils.Constants.Companion.SETTINGS_PREFS +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject +import javax.inject.Singleton class SettingsPrefsManager @Inject constructor(context: Context) { 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 8a1b244b..a743ee96 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,17 +1,34 @@ package com.example.speechbuddy.repository +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager -import com.example.speechbuddy.domain.models.AuthToken import com.example.speechbuddy.ui.models.InitialPage import com.example.speechbuddy.utils.Resource +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 +@Singleton class SettingsRepository @Inject constructor( private val settingsPrefManager: SettingsPrefsManager ) { + + private val _darkModeLiveData = MutableLiveData() + val darkModeLiveData: LiveData + get() = _darkModeLiveData + suspend fun setDarkMode(value: Boolean) { + CoroutineScope(Dispatchers.Main).launch { + if (_darkModeLiveData.value != value) { + _darkModeLiveData.value = value + } + } + settingsPrefManager.saveDarkMode(value) } From 4bf44d5c05b30c092d6438c1bcf6123a8c2fe1c1 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Fri, 17 Nov 2023 16:33:51 +0900 Subject: [PATCH 03/19] :sparkles: Implement display settings backup feature --- .../compose/settings/BackupSettings.kt | 39 ++++++++ .../data/remote/SettingsRemoteSource.kt | 24 +++++ .../data/remote/models/SettingsBackupDto.kt | 10 ++ .../example/speechbuddy/di/NetworkModule.kt | 7 ++ .../repository/SettingsRepository.kt | 33 ++++++- .../speechbuddy/service/BackupService.kt | 23 +++++ .../ui/models/BackupSettingsUiState.kt | 10 +- .../viewmodel/BackupSettingsViewModel.kt | 94 ++++++++++++++++++- .../viewmodel/DisplaySettingsViewModel.kt | 1 + frontend/app/src/main/res/values/strings.xml | 1 + 10 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt index f0bbd1a2..87834523 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt @@ -1,5 +1,6 @@ package com.example.speechbuddy.compose.settings +import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -8,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -15,7 +18,9 @@ import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -23,6 +28,7 @@ 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 @OptIn(ExperimentalMaterial3Api::class) @@ -34,6 +40,7 @@ fun BackupSettings( viewModel: BackupSettingsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + val loading by viewModel.loading.observeAsState() Surface( modifier = modifier.fillMaxSize() @@ -89,4 +96,36 @@ fun BackupSettings( } } } + + uiState.alert.let { alert -> + when (alert) { + BackupSettingsAlert.SUCCESS -> { + viewModel.toastDisplayed() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.backup_success), + Toast.LENGTH_SHORT + ).show() + } + + BackupSettingsAlert.CONNECTION -> { + viewModel.toastDisplayed() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.connection_error), + Toast.LENGTH_SHORT + ).show() + } + + else -> {} + } + } + + if (loading == true) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + ) + } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt new file mode 100644 index 00000000..4e1964c4 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt @@ -0,0 +1,24 @@ +package com.example.speechbuddy.data.remote + +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.service.BackupService +import com.example.speechbuddy.utils.ResponseHandler +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.Flow +import retrofit2.Response +import javax.inject.Inject + +class SettingsRemoteSource @Inject constructor( + private val backupService: BackupService, + private val responseHandler: ResponseHandler +){ + suspend fun getSettings(authHeader: String): Flow> = + flow { + try { + val result = backupService.getSettings(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt new file mode 100644 index 00000000..989e3701 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SettingsBackupDto.kt @@ -0,0 +1,10 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SettingsBackupDto( + @Json(name = "display_mode") val displayMode: Int? = null, + @Json(name = "default_menu") val defaultMenu: Int? = null +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt index d299b186..bdec861f 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt @@ -8,6 +8,7 @@ import com.example.speechbuddy.data.remote.models.AccessTokenDtoMapper import com.example.speechbuddy.data.remote.models.AuthTokenDtoMapper import com.example.speechbuddy.data.remote.models.UserDtoMapper import com.example.speechbuddy.service.AuthService +import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.service.UserService import com.example.speechbuddy.utils.Constants import com.example.speechbuddy.utils.ResponseHandler @@ -58,6 +59,12 @@ class NetworkModule { return retrofit.create(AuthService::class.java) } + @Singleton + @Provides + fun provideBackupService(retrofit: Retrofit): BackupService { + return retrofit.create(BackupService::class.java) + } + @Singleton @Provides fun provideUserDtoMapper(): UserDtoMapper { 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 a743ee96..73e7e9e5 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,21 +1,32 @@ package com.example.speechbuddy.repository +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.data.remote.requests.AuthRefreshRequest +import com.example.speechbuddy.domain.SessionManager +import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.ui.models.InitialPage import com.example.speechbuddy.utils.Resource +import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import retrofit2.Response import javax.inject.Inject import javax.inject.Singleton @Singleton class SettingsRepository @Inject constructor( - private val settingsPrefManager: SettingsPrefsManager + private val settingsPrefManager: SettingsPrefsManager, + private val backupService: BackupService, + private val responseHandler: ResponseHandler, + private val sessionManager: SessionManager ) { private val _darkModeLiveData = MutableLiveData() @@ -28,7 +39,6 @@ class SettingsRepository @Inject constructor( _darkModeLiveData.value = value } } - settingsPrefManager.saveDarkMode(value) } @@ -62,4 +72,23 @@ class SettingsRepository @Inject constructor( } } + suspend fun displayBackup(settingsBackupDto: SettingsBackupDto): Flow> = + flow { + try { + val result = backupService.displayBackup(getAuthHeader(), settingsBackupDto) + emit(result) + Log.d("repository", result.code().toString()) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + /* TODO: favorite symbol, symbol list, weight table backup */ + /* TODO: setting 받아오기 */ + + private fun getAuthHeader(): String { + val accessToken = sessionManager.cachedToken.value?.accessToken + return "Bearer $accessToken" + } + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt new file mode 100644 index 00000000..5baa196d --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -0,0 +1,23 @@ +package com.example.speechbuddy.service + +import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface BackupService { + + @POST("/setting/backup/") + suspend fun displayBackup( + @Header("Authorization") header: String, + @Body settingsBackupDto: SettingsBackupDto + ): Response + + @GET("/setting/backup/") + suspend fun getSettings( + @Header("Authorization") header: String + ): Response + +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt index a83efc44..deda0da9 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt @@ -3,5 +3,11 @@ package com.example.speechbuddy.ui.models data class BackupSettingsUiState( /* TODO */ val lastBackupDate: String = "2023.09.09", - val isAutoBackupEnabled: Boolean = true -) \ No newline at end of file + val isAutoBackupEnabled: Boolean = true, + val alert: BackupSettingsAlert? = null +) + +enum class BackupSettingsAlert { + SUCCESS, + CONNECTION +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 9b78b6dd..0446f204 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -1,17 +1,24 @@ package com.example.speechbuddy.viewmodel +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.speechbuddy.data.remote.models.SettingsBackupDto import com.example.speechbuddy.repository.AuthRepository import com.example.speechbuddy.repository.SettingsRepository +import com.example.speechbuddy.ui.models.BackupSettingsAlert import com.example.speechbuddy.ui.models.BackupSettingsUiState +import com.example.speechbuddy.utils.ResponseCode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import javax.inject.Inject @HiltViewModel @@ -22,6 +29,9 @@ class BackupSettingsViewModel @Inject internal constructor( private val _uiState = MutableStateFlow(BackupSettingsUiState()) val uiState: StateFlow = _uiState.asStateFlow() + private val _loading = MutableLiveData(false) + val loading: LiveData get() = _loading + fun setAutoBackup(value: Boolean) { _uiState.update { currentState -> currentState.copy( @@ -34,9 +44,89 @@ class BackupSettingsViewModel @Inject internal constructor( // TODO: Implement automated backup } - fun backup() { + fun toastDisplayed() { + _uiState.update { currentState -> + currentState.copy( + alert = null + ) + } + } + + private fun getDarkMode(): Int { + var darkMode: Boolean = false + viewModelScope.launch { + repository.getDarkMode().collect { + darkMode = it.data?: false + } + } + return if (!darkMode) { 0 } else { 1 } + } + + private fun getInitialPage(): Int { + var initialPage: Boolean = true viewModelScope.launch { - //repository.backup() + repository.getInitialPage().collect { + initialPage = it.data?: true + } + } + return if (initialPage) { 1 } else { 0 } + } + + private fun displayBackup() { + + viewModelScope.launch { + repository.displayBackup( + SettingsBackupDto( + getDarkMode(), + getInitialPage() + ) + ).collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> { + /* TODO: 백업 완성됐을 때 로딩 조절 */ + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.SUCCESS + ) + } + } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.CONNECTION + ) + } + } + } + } } } + + private fun favoriteBackup() { + viewModelScope.launch { + //repository.favoriteBackup() + } + } + + private fun symbolListBackup() { + viewModelScope.launch { + //repository.symbolListBackup() + } + } + + private fun weightTableBackup() { + viewModelScope.launch { + //repository.weightTableBackup() + } + } + + fun backup() { + _loading.value = true + displayBackup() + } + + } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt index b3581066..d3514ecb 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt @@ -1,5 +1,6 @@ package com.example.speechbuddy.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.speechbuddy.repository.SettingsRepository diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index 74055fbc..ad5a91f4 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ 마지막 백업 날짜 자동 백업 활성화 지금 백업하기 + 백업 성공 디스플레이 From dabbe6ede2194ca0618d68c504c635aa4b89654d Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Fri, 17 Nov 2023 19:15:23 +0900 Subject: [PATCH 04/19] :art: Add backup functions --- .../repository/SettingsRepository.kt | 45 +++++++++++++++++-- .../speechbuddy/service/BackupService.kt | 14 +++++- .../viewmodel/BackupSettingsViewModel.kt | 37 ++++++++++++--- 3 files changed, 85 insertions(+), 11 deletions(-) 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 73e7e9e5..2792da81 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 @@ -7,6 +7,8 @@ import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.remote.models.SettingsBackupDto import com.example.speechbuddy.data.remote.requests.AuthRefreshRequest import com.example.speechbuddy.domain.SessionManager +import com.example.speechbuddy.domain.models.Entry +import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.ui.models.InitialPage import com.example.speechbuddy.utils.Resource @@ -14,6 +16,7 @@ import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -26,7 +29,8 @@ class SettingsRepository @Inject constructor( private val settingsPrefManager: SettingsPrefsManager, private val backupService: BackupService, private val responseHandler: ResponseHandler, - private val sessionManager: SessionManager + private val sessionManager: SessionManager, + private val symbolRepository: SymbolRepository ) { private val _darkModeLiveData = MutableLiveData() @@ -77,14 +81,47 @@ class SettingsRepository @Inject constructor( try { val result = backupService.displayBackup(getAuthHeader(), settingsBackupDto) emit(result) - Log.d("repository", result.code().toString()) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) } } - /* TODO: favorite symbol, symbol list, weight table backup */ - /* TODO: setting 받아오기 */ + suspend fun symbolListBackup(): Flow> = + flow { + try { + /* + var symbollist: List = emptyList() + var params: String = "" + symbolRepository.getSymbols("").collect {symbols -> + symbollist = symbols + } + val len = symbollist.size + for (i in 1 until len) + params = params + symbollist[i].id + "," + */ + + /* TODO: symbol list 받아와서 params에 넣기 */ + + val result = backupService.symbolListBackup("", getAuthHeader()) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun favoriteSymbolBackup(): Flow> = + flow { + try { + /* TODO: favorite symbol list 받아와서 params에 넣기 */ + val result = backupService.favoriteSymbolBackup("", getAuthHeader()) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + /* TODO: weight table backup */ + /* TODO: setting 관련 get */ private fun getAuthHeader(): String { val accessToken = sessionManager.cachedToken.value?.accessToken diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt index 5baa196d..1120f8a4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -6,6 +6,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Query interface BackupService { @@ -15,9 +16,20 @@ interface BackupService { @Body settingsBackupDto: SettingsBackupDto ): Response + @POST("/symbol/enable/") + suspend fun symbolListBackup( + @Query("id") id: String, + @Header("Authorization") header: String + ): Response + + @POST("/symbol/favorite/backup/") + suspend fun favoriteSymbolBackup( + @Query("id") id: String, + @Header("Authorization") header: String + ): Response + @GET("/setting/backup/") suspend fun getSettings( @Header("Authorization") header: String ): Response - } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 0446f204..f2279329 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -73,7 +73,6 @@ class BackupSettingsViewModel @Inject internal constructor( } private fun displayBackup() { - viewModelScope.launch { repository.displayBackup( SettingsBackupDto( @@ -83,7 +82,7 @@ class BackupSettingsViewModel @Inject internal constructor( ).collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { - /* TODO: 백업 완성됐을 때 로딩 조절 */ + /* TODO: 백업 완성됐을 때 지우기 */ _loading.value = false _uiState.update { currentState -> currentState.copy ( @@ -105,15 +104,39 @@ class BackupSettingsViewModel @Inject internal constructor( } } - private fun favoriteBackup() { + private fun symbolListBackup() { + /* viewModelScope.launch { - //repository.favoriteBackup() + repository.symbolListBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> { + /* TODO: 백업 완성됐을 때 로딩 조절 */ + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.SUCCESS + ) + } + } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.CONNECTION + ) + } + } + } + + } } + */ } - private fun symbolListBackup() { + private fun favoriteBackup() { viewModelScope.launch { - //repository.symbolListBackup() + //repository.favoriteBackup() } } @@ -126,6 +149,8 @@ class BackupSettingsViewModel @Inject internal constructor( fun backup() { _loading.value = true displayBackup() + //symbolListBackup() + //favoriteBackup() } From 7408218cbfbff1ee89af4f8ce92ad2dfb5d0e316 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Mon, 20 Nov 2023 14:14:24 +0900 Subject: [PATCH 05/19] :sparkles: Implement user symbol list backup, favorite symbol list backup --- .../speechbuddy/data/local/SymbolDao.kt | 6 +++ .../data/remote/SettingsRemoteSource.kt | 4 +- .../repository/SettingsRepository.kt | 41 +++++++++++-------- .../repository/SymbolRepository.kt | 13 +++++- .../speechbuddy/service/BackupService.kt | 6 +-- .../viewmodel/BackupSettingsViewModel.kt | 40 ++++++++++-------- 6 files changed, 70 insertions(+), 40 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt index eae93a07..09f33543 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt @@ -32,6 +32,12 @@ interface SymbolDao { @Query("SELECT * FROM symbols WHERE categoryId = :categoryId") fun getSymbolsByCategoryId(categoryId: Int): Flow> + @Query("SELECT id FROM symbols WHERE id > 500") + fun getUserSymbolsId(): Flow> + + @Query("SELECT id FROM symbols WHERE isFavorite = 1") + fun getFavoriteSymbolsId(): Flow> + @Update suspend fun updateSymbol(symbolEntity: SymbolEntity) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt index 4e1964c4..436ba4e4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt @@ -12,10 +12,10 @@ class SettingsRemoteSource @Inject constructor( private val backupService: BackupService, private val responseHandler: ResponseHandler ){ - suspend fun getSettings(authHeader: String): Flow> = + suspend fun getDisplaySettings(authHeader: String): Flow> = flow { try { - val result = backupService.getSettings(authHeader) + val result = backupService.getDisplaySettings(authHeader) emit(result) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) 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 2792da81..236ea6a6 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 @@ -89,21 +89,18 @@ class SettingsRepository @Inject constructor( suspend fun symbolListBackup(): Flow> = flow { try { - /* - var symbollist: List = emptyList() - var params: String = "" - symbolRepository.getSymbols("").collect {symbols -> - symbollist = symbols + if (symbolRepository.getUserSymbolsIdString().isEmpty()) { + val result = backupService.symbolListBackup( + header = getAuthHeader() + ) + emit(result) + } else { + val result = backupService.symbolListBackup( + symbolRepository.getUserSymbolsIdString(), + getAuthHeader() + ) + emit(result) } - val len = symbollist.size - for (i in 1 until len) - params = params + symbollist[i].id + "," - */ - - /* TODO: symbol list 받아와서 params에 넣기 */ - - val result = backupService.symbolListBackup("", getAuthHeader()) - emit(result) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) } @@ -112,9 +109,19 @@ class SettingsRepository @Inject constructor( suspend fun favoriteSymbolBackup(): Flow> = flow { try { - /* TODO: favorite symbol list 받아와서 params에 넣기 */ - val result = backupService.favoriteSymbolBackup("", getAuthHeader()) - emit(result) + Log.d("symbol", symbolRepository.getFavoriteSymbolsIdString()) + if (symbolRepository.getFavoriteSymbolsIdString().isEmpty()) { + val result = backupService.favoriteSymbolBackup( + header = getAuthHeader() + ) + emit(result) + } else { + val result = backupService.favoriteSymbolBackup( + symbolRepository.getFavoriteSymbolsIdString(), + getAuthHeader() + ) + emit(result) + } } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt index 5f0b4a47..cc03e921 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt @@ -6,7 +6,6 @@ import com.example.speechbuddy.data.local.models.CategoryMapper import com.example.speechbuddy.data.local.models.SymbolEntity import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.remote.MySymbolRemoteSource -import com.example.speechbuddy.utils.ResponseHandler import com.example.speechbuddy.data.remote.models.MySymbolDtoMapper import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.domain.models.Category @@ -15,8 +14,10 @@ import com.example.speechbuddy.domain.models.MySymbol import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.utils.Resource import com.example.speechbuddy.utils.ResponseCode +import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import okhttp3.MultipartBody import javax.inject.Inject @@ -89,6 +90,16 @@ class SymbolRepository @Inject constructor( symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } + suspend fun getUserSymbolsIdString(): String { + val idList = symbolDao.getUserSymbolsId().firstOrNull() ?: emptyList() + return if (idList.isEmpty()) "" else idList.joinToString(separator = ",") + } + + suspend fun getFavoriteSymbolsIdString(): String { + val idList = symbolDao.getFavoriteSymbolsId().firstOrNull() ?: emptyList() + return if (idList.isEmpty()) "" else idList.joinToString(separator = ",") + } + suspend fun updateFavorite(symbol: Symbol, value: Boolean) { val symbolEntity = SymbolEntity( id = symbol.id, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt index 1120f8a4..d191447e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -18,18 +18,18 @@ interface BackupService { @POST("/symbol/enable/") suspend fun symbolListBackup( - @Query("id") id: String, + @Query("id") id: String? = null, @Header("Authorization") header: String ): Response @POST("/symbol/favorite/backup/") suspend fun favoriteSymbolBackup( - @Query("id") id: String, + @Query("id") id: String? = null, @Header("Authorization") header: String ): Response @GET("/setting/backup/") - suspend fun getSettings( + suspend fun getDisplaySettings( @Header("Authorization") header: String ): Response } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index f2279329..7ab5b4c5 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -81,15 +81,27 @@ class BackupSettingsViewModel @Inject internal constructor( ) ).collect { result -> when (result.code()) { - ResponseCode.SUCCESS.value -> { - /* TODO: 백업 완성됐을 때 지우기 */ + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { _loading.value = false _uiState.update { currentState -> currentState.copy ( - alert = BackupSettingsAlert.SUCCESS + alert = BackupSettingsAlert.CONNECTION ) } } + } + } + } + } + + private fun symbolListBackup() { + + viewModelScope.launch { + repository.symbolListBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} ResponseCode.NO_INTERNET_CONNECTION.value -> { _loading.value = false @@ -100,17 +112,18 @@ class BackupSettingsViewModel @Inject internal constructor( } } } + } } + } - private fun symbolListBackup() { - /* + private fun favoriteSymbolBackup() { viewModelScope.launch { - repository.symbolListBackup().collect { result -> + repository.favoriteSymbolBackup().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { - /* TODO: 백업 완성됐을 때 로딩 조절 */ + /* TODO: 백업 완성됐을 때 여기는 지우고 weight table에 복붙 */ _loading.value = false _uiState.update { currentState -> currentState.copy ( @@ -128,16 +141,8 @@ class BackupSettingsViewModel @Inject internal constructor( } } } - } } - */ - } - - private fun favoriteBackup() { - viewModelScope.launch { - //repository.favoriteBackup() - } } private fun weightTableBackup() { @@ -149,8 +154,9 @@ class BackupSettingsViewModel @Inject internal constructor( fun backup() { _loading.value = true displayBackup() - //symbolListBackup() - //favoriteBackup() + symbolListBackup() + favoriteSymbolBackup() + //weightTableBackup() } From 099f73d6cb737e49b37fbe80998e123fba5f7317 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Mon, 20 Nov 2023 14:21:51 +0900 Subject: [PATCH 06/19] :sparkles: Implement getting last backup date feature --- .../example/speechbuddy/ui/models/BackupSettingsUiState.kt | 3 +-- .../example/speechbuddy/viewmodel/BackupSettingsViewModel.kt | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt index deda0da9..c4eb300d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/BackupSettingsUiState.kt @@ -1,8 +1,7 @@ package com.example.speechbuddy.ui.models data class BackupSettingsUiState( - /* TODO */ - val lastBackupDate: String = "2023.09.09", + val lastBackupDate: String = "", val isAutoBackupEnabled: Boolean = true, val alert: BackupSettingsAlert? = null ) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 7ab5b4c5..113170bd 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -127,7 +128,8 @@ class BackupSettingsViewModel @Inject internal constructor( _loading.value = false _uiState.update { currentState -> currentState.copy ( - alert = BackupSettingsAlert.SUCCESS + alert = BackupSettingsAlert.SUCCESS, + lastBackupDate = LocalDate.now().toString() ) } } From 1702d0093b53db909d63ddabec34d10502c46cd9 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Mon, 20 Nov 2023 14:47:49 +0900 Subject: [PATCH 07/19] :sparkles: Implement get symbol list, get favorites list in BackupService and SettingsRemoteSource --- .../data/remote/SettingsRemoteSource.kt | 22 +++++++++++++++++++ .../data/remote/models/FavoritesListDto.kt | 14 ++++++++++++ .../data/remote/models/SymbolListDto.kt | 18 +++++++++++++++ .../speechbuddy/service/BackupService.kt | 12 ++++++++++ 4 files changed, 66 insertions(+) create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt index 436ba4e4..a1072734 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/SettingsRemoteSource.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.data.remote +import com.example.speechbuddy.data.remote.models.FavoritesListDto import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.data.remote.models.SymbolListDto import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.flow.flow @@ -21,4 +23,24 @@ class SettingsRemoteSource @Inject constructor( emit(responseHandler.getConnectionErrorResponse()) } } + + suspend fun getSymbolList(authHeader: String): Flow> = + flow { + try { + val result = backupService.getSymbolList(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + + suspend fun getFavoritesList(authHeader: String): Flow> = + flow { + try { + val result = backupService.getFavoriteSymbolList(authHeader) + emit(result) + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt new file mode 100644 index 00000000..d9e36aa6 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/FavoritesListDto.kt @@ -0,0 +1,14 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SymbolIdDto( + @Json(name = "id") val id: Int +) + +@JsonClass(generateAdapter = true) +data class FavoritesListDto( + @Json(name = "results") val results: List +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt new file mode 100644 index 00000000..caf251af --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/models/SymbolListDto.kt @@ -0,0 +1,18 @@ +package com.example.speechbuddy.data.remote.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SymbolDto( + @Json(name = "id") val id: Int, + @Json(name = "text") val text: String, + @Json(name = "category") val category: Int, + @Json(name = "image") val image: String, + @Json(name = "created_at") val createdAt: String +) + +@JsonClass(generateAdapter = true) +data class SymbolListDto( + @Json(name = "my_symbols") val mySymbols: List +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt index d191447e..f83430f8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.service +import com.example.speechbuddy.data.remote.models.FavoritesListDto import com.example.speechbuddy.data.remote.models.SettingsBackupDto +import com.example.speechbuddy.data.remote.models.SymbolListDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET @@ -32,4 +34,14 @@ interface BackupService { suspend fun getDisplaySettings( @Header("Authorization") header: String ): Response + + @GET("/symbol/") + suspend fun getSymbolList( + @Header("Authorization") header: String + ): Response + + @GET("/symbol/favorite/backup/") + suspend fun getFavoriteSymbolList( + @Header("Authorization") header: String + ): Response } \ No newline at end of file From 8753cbed92cdb8701a00e9070cdc90a0330fa91a Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 15:23:30 +0900 Subject: [PATCH 08/19] :sparkles: Implement copyright page --- .../compose/settings/AccountSettings.kt | 13 ++- .../compose/settings/CopyrightScreen.kt | 79 +++++++++++++++++++ .../compose/settings/MainSettings.kt | 3 + .../compose/settings/SettingsScreen.kt | 7 ++ .../data/remote/requests/BackupRequest.kt | 14 ++++ .../repository/SettingsRepository.kt | 20 ++++- .../speechbuddy/service/BackupService.kt | 7 ++ .../ui/models/AccountSettingsUiState.kt | 1 + .../viewmodel/BackupSettingsViewModel.kt | 32 ++++++-- frontend/app/src/main/res/values/strings.xml | 8 +- 10 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt index 1eca7308..a3ecb062 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 @@ -92,7 +92,7 @@ fun AccountSettings( ) { ButtonUi( text = stringResource(id = R.string.logout), - onClick = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) } + onClick = { viewModel.showAlert(AccountSettingsAlert.BACKUP) } ) ButtonUi( @@ -108,6 +108,17 @@ fun AccountSettings( uiState.alert?.let { alert -> when (alert) { + AccountSettingsAlert.BACKUP -> { + AlertDialogUi( + title = stringResource(id = R.string.logout), + text = stringResource(id = R.string.logout_warning), + dismissButtonText = stringResource(id = R.string.logout), + confirmButtonText = stringResource(id = R.string.backup), + onDismiss = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) }, + onConfirm = { /* TODO: backup */ } + ) + } + AccountSettingsAlert.LOGOUT -> { AlertDialogUi( title = stringResource(id = R.string.logout), diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt new file mode 100644 index 00000000..40951bd9 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/CopyrightScreen.kt @@ -0,0 +1,79 @@ +package com.example.speechbuddy.compose.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.speechbuddy.R +import com.example.speechbuddy.compose.utils.TitleUi +import com.example.speechbuddy.compose.utils.TopAppBarUi +import com.example.speechbuddy.ui.SpeechBuddyTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Copyright( + modifier: Modifier = Modifier, + onBackClick: () -> Unit, + bottomPaddingValues: PaddingValues +) { + Surface( + modifier = modifier.fillMaxSize() + ) { + Scaffold( + topBar = { + TopAppBarUi( + title = stringResource(id = R.string.settings), + onBackClick = onBackClick, + isBackClickEnabled = true + ) + } + ) { topPaddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = topPaddingValues.calculateTopPadding(), + bottom = bottomPaddingValues.calculateBottomPadding() + ) + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + TitleUi(title = stringResource(id = R.string.copyright_info)) + + Spacer(modifier = modifier.height(20.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(R.string.copyright), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} + +@Preview +@Composable +fun CopyrightPreview() { + SpeechBuddyTheme { + Copyright(onBackClick = { /*TODO*/ }, bottomPaddingValues = PaddingValues()) + } +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt index 2ad4cfb2..2c170644 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/MainSettings.kt @@ -80,6 +80,9 @@ fun MainSettings( SettingsTextButton(text = stringResource(id = R.string.developers_info), onClick = { navController.navigate("developers") }) + + SettingsTextButton(text = stringResource(id = R.string.copyright_info), + onClick = { navController.navigate("copyright") }) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt index d91d4aa2..a454b9a0 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 @@ -79,5 +79,12 @@ private fun SettingsScreenNavHost( bottomPaddingValues = bottomPaddingValues ) } + + composable("copyright") { + Copyright( + onBackClick = navigateToMain, + bottomPaddingValues = bottomPaddingValues + ) + } } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt new file mode 100644 index 00000000..409dbe94 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/remote/requests/BackupRequest.kt @@ -0,0 +1,14 @@ +package com.example.speechbuddy.data.remote.requests + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BackupWeightTableRequest( + val weight_table: List +) + +@JsonClass(generateAdapter = true) +data class WeightTableEntity( + val id: Int, + val weight: String +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt index 236ea6a6..d0ac6e85 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.remote.models.SettingsBackupDto import com.example.speechbuddy.data.remote.requests.AuthRefreshRequest +import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.domain.models.Entry import com.example.speechbuddy.domain.models.Symbol @@ -30,7 +31,8 @@ class SettingsRepository @Inject constructor( private val backupService: BackupService, private val responseHandler: ResponseHandler, private val sessionManager: SessionManager, - private val symbolRepository: SymbolRepository + private val symbolRepository: SymbolRepository, + private val weightTableRepository: WeightTableRepository ) { private val _darkModeLiveData = MutableLiveData() @@ -109,7 +111,6 @@ class SettingsRepository @Inject constructor( suspend fun favoriteSymbolBackup(): Flow> = flow { try { - Log.d("symbol", symbolRepository.getFavoriteSymbolsIdString()) if (symbolRepository.getFavoriteSymbolsIdString().isEmpty()) { val result = backupService.favoriteSymbolBackup( header = getAuthHeader() @@ -127,6 +128,21 @@ class SettingsRepository @Inject constructor( } } + suspend fun weightTableBackup(): Flow> = + flow { + try { + /* + val result = backupService.weightTableBackup( + getAuthHeader(), + BackupWeightTableRequest(weightTableRepository.) + ) + emit(result) + */ + } catch (e: Exception) { + emit(responseHandler.getConnectionErrorResponse()) + } + } + /* TODO: weight table backup */ /* TODO: setting 관련 get */ diff --git a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt index f83430f8..c81a0381 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/service/BackupService.kt @@ -3,6 +3,7 @@ package com.example.speechbuddy.service import com.example.speechbuddy.data.remote.models.FavoritesListDto import com.example.speechbuddy.data.remote.models.SettingsBackupDto import com.example.speechbuddy.data.remote.models.SymbolListDto +import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET @@ -30,6 +31,12 @@ interface BackupService { @Header("Authorization") header: String ): Response + @POST("/weight/backup/") + suspend fun weightTableBackup( + @Header("Authorization") header: String, + @Body backupWeightTableRequest: BackupWeightTableRequest + ): Response + @GET("/setting/backup/") suspend fun getDisplaySettings( @Header("Authorization") header: String diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt index 49e88498..cbadd76e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt @@ -8,6 +8,7 @@ data class AccountSettingsUiState( ) enum class AccountSettingsAlert { + BACKUP, LOGOUT, WITHDRAW, WITHDRAW_PROCEED, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 113170bd..ae32c8ae 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.viewmodel +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -122,9 +124,28 @@ class BackupSettingsViewModel @Inject internal constructor( private fun favoriteSymbolBackup() { viewModelScope.launch { repository.favoriteSymbolBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.CONNECTION + ) + } + } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun weightTableBackup() { + viewModelScope.launch { + repository.weightTableBackup().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { - /* TODO: 백업 완성됐을 때 여기는 지우고 weight table에 복붙 */ _loading.value = false _uiState.update { currentState -> currentState.copy ( @@ -147,18 +168,13 @@ class BackupSettingsViewModel @Inject internal constructor( } } - private fun weightTableBackup() { - viewModelScope.launch { - //repository.weightTableBackup() - } - } - + @RequiresApi(Build.VERSION_CODES.O) fun backup() { _loading.value = true displayBackup() symbolListBackup() favoriteSymbolBackup() - //weightTableBackup() + weightTableBackup() } diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index f7a186ac..65d6871d 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -97,11 +97,13 @@ 계정 로그아웃 정말로 로그아웃하시겠습니까? + 로그아웃하기 전 백업하시겠습니까? 변경사항을 백업하지 않으면 이용 기록이 손실될 수 있습니다. 회원탈퇴 회원탈퇴를 하면 지금까지의 이용 기록이 모두 사라지며 복구할 수 없습니다. SpeechBuddy를 다시 이용하시려면 게스트 모드를 이용하거나 회원가입을 새로 해야 합니다. 정말로 탈퇴하시겠습니까? 정말로 탈퇴하시겠습니까? 이 동작은 취소할 수 없습니다. 인터넷 연결 없음 - 인터넷 연결을 확인하세요 + 인터넷 연결을 확인하세요업 + 백업 현재 게스트 모드를 사용 중입니다. @@ -132,4 +134,8 @@ 개발자 정보 + + + 저작권 정보 + 본 서비스의 그림상징은 \'한국형 보완대체의사소통용 기본상징 체계집\'의 일부로 박은혜, 김영태(이화여자대학교), 홍기형(성신여자대학교)에게 저작권이 있습니다.\n자료의 무단 사용 및 2차 가공, 배포, 상업적 용도로 사용하는 것을 금합니다. \ No newline at end of file From f86ca7aae356090f05fd11ebbaf0d1d3d60e7428 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 15:24:44 +0900 Subject: [PATCH 09/19] :fire: Remove copyright preview --- .../speechbuddy/compose/settings/CopyrightScreen.kt | 8 -------- 1 file changed, 8 deletions(-) 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 40951bd9..c2b8ca20 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 @@ -68,12 +68,4 @@ fun Copyright( } } } -} - -@Preview -@Composable -fun CopyrightPreview() { - SpeechBuddyTheme { - Copyright(onBackClick = { /*TODO*/ }, bottomPaddingValues = PaddingValues()) - } } \ No newline at end of file From b0908fbdb02e81a7e33d5741efe547c6923a8c41 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 17:45:53 +0900 Subject: [PATCH 10/19] :sparkles: Implement manual backup in settings --- .../example/speechbuddy/di/DatabaseModule.kt | 7 +++++++ .../repository/SettingsRepository.kt | 7 +------ .../repository/WeightTableRepository.kt | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt index 3c436bbf..3daecef0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/DatabaseModule.kt @@ -11,6 +11,7 @@ import com.example.speechbuddy.data.local.models.CategoryMapper import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.local.models.UserMapper import com.example.speechbuddy.data.local.WeightRowDao +import com.example.speechbuddy.domain.utils.Converters import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -70,6 +71,12 @@ class DatabaseModule { return CategoryMapper() } + @Singleton + @Provides + fun provideConverters(): Converters { + return Converters() + } + @Singleton @Provides fun provideAuthTokenPrefsManager(@ApplicationContext context: Context): AuthTokenPrefsManager { 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 d0ac6e85..d3d4d1c5 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 @@ -131,21 +131,16 @@ class SettingsRepository @Inject constructor( suspend fun weightTableBackup(): Flow> = flow { try { - /* val result = backupService.weightTableBackup( getAuthHeader(), - BackupWeightTableRequest(weightTableRepository.) + weightTableRepository.getBackupWeightTableRequest() ) emit(result) - */ } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) } } - /* TODO: weight table backup */ - /* TODO: setting 관련 get */ - private fun getAuthHeader(): String { val accessToken = sessionManager.cachedToken.value?.accessToken return "Bearer $accessToken" 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 dbef0add..ed198a55 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt @@ -5,13 +5,17 @@ import com.example.speechbuddy.data.local.WeightRowDao import com.example.speechbuddy.data.local.models.SymbolMapper import com.example.speechbuddy.data.local.models.WeightRowEntity import com.example.speechbuddy.data.local.models.WeightRowMapper +import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest +import com.example.speechbuddy.data.remote.requests.WeightTableEntity import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.domain.models.WeightRow +import com.example.speechbuddy.domain.utils.Converters import com.example.speechbuddy.ui.models.SymbolItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -23,7 +27,8 @@ import javax.inject.Singleton @Singleton class WeightTableRepository @Inject constructor( private val symbolDao: SymbolDao, - private val weightRowDao: WeightRowDao + private val weightRowDao: WeightRowDao, + private val converters: Converters ) { private val symbolMapper = SymbolMapper() private val weightRowMapper = WeightRowMapper() @@ -41,6 +46,17 @@ class WeightTableRepository @Inject constructor( } } + suspend fun getBackupWeightTableRequest(): BackupWeightTableRequest { + val weightRowList = getAllWeightRows().firstOrNull() ?: emptyList() + val weightTableEntities = weightRowList.map { weightRow -> + WeightTableEntity( + id = weightRow.id, + weight = converters.fromList(weightRow.weights) + ) + } + return BackupWeightTableRequest(weight_table = weightTableEntities) + } + private fun getWeightRowById(rowId: Int) = weightRowDao.getWeightRowById(rowId).map { weightRowEntities -> weightRowEntities.map { weightRowEntity -> From 8b7b4c257dfec8413abed15285c9d4940f304dc8 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 18:58:49 +0900 Subject: [PATCH 11/19] :art: Refactor displayBackup --- .../repository/SettingsRepository.kt | 16 +++++++++-- .../viewmodel/BackupSettingsViewModel.kt | 27 +------------------ 2 files changed, 15 insertions(+), 28 deletions(-) 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 d3d4d1c5..54ade132 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 @@ -18,6 +18,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -78,10 +79,21 @@ class SettingsRepository @Inject constructor( } } - suspend fun displayBackup(settingsBackupDto: SettingsBackupDto): Flow> = + suspend fun displayBackup(): Flow> = flow { try { - val result = backupService.displayBackup(getAuthHeader(), settingsBackupDto) + var darkMode: Int? = 0 + var initialPage: Int? = 1 + getDarkMode().first().data?.let { + darkMode = if (it) 1 else 0 + } + getInitialPage().first().data?.let{ + initialPage = if (it) 1 else 0 + } + val result = backupService.displayBackup( + getAuthHeader(), + SettingsBackupDto(darkMode, initialPage) + ) emit(result) } catch (e: Exception) { emit(responseHandler.getConnectionErrorResponse()) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index ae32c8ae..cb4ce503 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -55,34 +55,9 @@ class BackupSettingsViewModel @Inject internal constructor( } } - private fun getDarkMode(): Int { - var darkMode: Boolean = false - viewModelScope.launch { - repository.getDarkMode().collect { - darkMode = it.data?: false - } - } - return if (!darkMode) { 0 } else { 1 } - } - - private fun getInitialPage(): Int { - var initialPage: Boolean = true - viewModelScope.launch { - repository.getInitialPage().collect { - initialPage = it.data?: true - } - } - return if (initialPage) { 1 } else { 0 } - } - private fun displayBackup() { viewModelScope.launch { - repository.displayBackup( - SettingsBackupDto( - getDarkMode(), - getInitialPage() - ) - ).collect { result -> + repository.displayBackup().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> {} From 85f49dba8d16078763c29d1bfcd2105103a56fa2 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 19:33:21 +0900 Subject: [PATCH 12/19] :art: Refactor BackupSettingsViewModel --- .../viewmodel/BackupSettingsViewModel.kt | 75 +++++++------------ 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index cb4ce503..8030c44c 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -1,15 +1,11 @@ package com.example.speechbuddy.viewmodel import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.speechbuddy.data.remote.models.SettingsBackupDto -import com.example.speechbuddy.repository.AuthRepository import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.ui.models.BackupSettingsAlert import com.example.speechbuddy.ui.models.BackupSettingsUiState @@ -20,13 +16,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.time.LocalDate import javax.inject.Inject @HiltViewModel class BackupSettingsViewModel @Inject internal constructor( - private val repository: SettingsRepository + private val repository: SettingsRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(BackupSettingsUiState()) @@ -61,14 +56,7 @@ class BackupSettingsViewModel @Inject internal constructor( when (result.code()) { ResponseCode.SUCCESS.value -> {} - ResponseCode.NO_INTERNET_CONNECTION.value -> { - _loading.value = false - _uiState.update { currentState -> - currentState.copy ( - alert = BackupSettingsAlert.CONNECTION - ) - } - } + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } } } } @@ -81,14 +69,7 @@ class BackupSettingsViewModel @Inject internal constructor( when (result.code()) { ResponseCode.SUCCESS.value -> {} - ResponseCode.NO_INTERNET_CONNECTION.value -> { - _loading.value = false - _uiState.update { currentState -> - currentState.copy ( - alert = BackupSettingsAlert.CONNECTION - ) - } - } + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } } } @@ -102,14 +83,7 @@ class BackupSettingsViewModel @Inject internal constructor( when (result.code()) { ResponseCode.SUCCESS.value -> {} - ResponseCode.NO_INTERNET_CONNECTION.value -> { - _loading.value = false - _uiState.update { currentState -> - currentState.copy ( - alert = BackupSettingsAlert.CONNECTION - ) - } - } + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } } } } @@ -120,24 +94,9 @@ class BackupSettingsViewModel @Inject internal constructor( viewModelScope.launch { repository.weightTableBackup().collect { result -> when (result.code()) { - ResponseCode.SUCCESS.value -> { - _loading.value = false - _uiState.update { currentState -> - currentState.copy ( - alert = BackupSettingsAlert.SUCCESS, - lastBackupDate = LocalDate.now().toString() - ) - } - } - - ResponseCode.NO_INTERNET_CONNECTION.value -> { - _loading.value = false - _uiState.update { currentState -> - currentState.copy ( - alert = BackupSettingsAlert.CONNECTION - ) - } - } + ResponseCode.SUCCESS.value -> { handleSuccess() } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } } } } @@ -152,5 +111,25 @@ class BackupSettingsViewModel @Inject internal constructor( weightTableBackup() } + private fun handleNoInternetConnection() { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.CONNECTION + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun handleSuccess() { + _loading.value = false + _uiState.update { currentState -> + currentState.copy ( + alert = BackupSettingsAlert.SUCCESS, + lastBackupDate = LocalDate.now().toString() + ) + } + } + } \ No newline at end of file From a2e9930d5347ad997c2fabf06dca95ac1be22f54 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Tue, 21 Nov 2023 20:18:01 +0900 Subject: [PATCH 13/19] :sparkles: Implement backup before logout --- .../compose/settings/AccountSettings.kt | 43 +++++++-- .../ui/models/AccountSettingsUiState.kt | 2 + .../viewmodel/AccountSettingsViewModel.kt | 91 +++++++++++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) 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 a3ecb062..af14d093 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt @@ -1,5 +1,8 @@ package com.example.speechbuddy.compose.settings +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -7,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -14,6 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -26,6 +32,7 @@ import com.example.speechbuddy.compose.utils.TopAppBarUi import com.example.speechbuddy.ui.models.AccountSettingsAlert import com.example.speechbuddy.viewmodel.AccountSettingsViewModel +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountSettings( @@ -111,14 +118,32 @@ fun AccountSettings( AccountSettingsAlert.BACKUP -> { AlertDialogUi( title = stringResource(id = R.string.logout), - text = stringResource(id = R.string.logout_warning), + text = stringResource(id = R.string.logout_backup), dismissButtonText = stringResource(id = R.string.logout), confirmButtonText = stringResource(id = R.string.backup), onDismiss = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) }, - onConfirm = { /* TODO: backup */ } + onConfirm = { viewModel.backup() } ) } + AccountSettingsAlert.LOADING -> { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + ) + } + + AccountSettingsAlert.BACKUP_SUCCESS -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.backup_success), + Toast.LENGTH_SHORT + ).show() + viewModel.showAlert(AccountSettingsAlert.LOGOUT) + } + + AccountSettingsAlert.LOGOUT -> { AlertDialogUi( title = stringResource(id = R.string.logout), @@ -155,14 +180,12 @@ fun AccountSettings( } AccountSettingsAlert.CONNECTION -> { - AlertDialogUi( - title = stringResource(id = R.string.no_connection), - text = stringResource(id = R.string.no_connection_warning), - dismissButtonText = stringResource(id = R.string.cancel), - confirmButtonText = stringResource(id = R.string.confirm), - onDismiss = { viewModel.hideAlert() }, - onConfirm = { viewModel.hideAlert() } - ) + viewModel.hideAlert() + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.connection_error), + Toast.LENGTH_SHORT + ).show() } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt index cbadd76e..475faae3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/AccountSettingsUiState.kt @@ -9,6 +9,8 @@ data class AccountSettingsUiState( enum class AccountSettingsAlert { BACKUP, + LOADING, + BACKUP_SUCCESS, LOGOUT, WITHDRAW, WITHDRAW_PROCEED, diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt index 4eca506c..5cc2b96b 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt @@ -1,9 +1,14 @@ package com.example.speechbuddy.viewmodel +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.speechbuddy.domain.SessionManager import com.example.speechbuddy.repository.AuthRepository +import com.example.speechbuddy.repository.SettingsRepository import com.example.speechbuddy.repository.UserRepository import com.example.speechbuddy.ui.models.AccountSettingsAlert import com.example.speechbuddy.ui.models.AccountSettingsUiState @@ -20,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class AccountSettingsViewModel @Inject internal constructor( private val authRepository: AuthRepository, + private val settingsRepository: SettingsRepository, private val userRepository: UserRepository, private val sessionManager: SessionManager ) : ViewModel() { @@ -58,12 +64,14 @@ class AccountSettingsViewModel @Inject internal constructor( } fun logout() { + showAlert(AccountSettingsAlert.LOADING) viewModelScope.launch { authRepository.logout().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { /* TODO: 디바이스에 저장돼 있는 유저 정보 초기화(토큰 말고) */ sessionManager.logout() + hideAlert() } ResponseCode.NO_INTERNET_CONNECTION.value -> { @@ -76,11 +84,13 @@ class AccountSettingsViewModel @Inject internal constructor( fun withdraw() { viewModelScope.launch { + showAlert(AccountSettingsAlert.LOADING) authRepository.withdraw().collect { result -> when (result.code()) { ResponseCode.SUCCESS.value -> { /* TODO: 디바이스에 저장돼 있는 유저 정보 초기화(토큰 말고) */ sessionManager.logout() + hideAlert() } ResponseCode.NO_INTERNET_CONNECTION.value -> { @@ -97,4 +107,85 @@ class AccountSettingsViewModel @Inject internal constructor( } } + private fun displayBackup() { + viewModelScope.launch { + settingsRepository.displayBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + private fun symbolListBackup() { + + viewModelScope.launch { + settingsRepository.symbolListBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + + } + } + + } + + private fun favoriteSymbolBackup() { + viewModelScope.launch { + settingsRepository.favoriteSymbolBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> {} + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun weightTableBackup() { + viewModelScope.launch { + settingsRepository.weightTableBackup().collect { result -> + when (result.code()) { + ResponseCode.SUCCESS.value -> { handleSuccess() } + + ResponseCode.NO_INTERNET_CONNECTION.value -> { handleNoInternetConnection() } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun backup() { + _uiState.update { + it.copy( + alert = AccountSettingsAlert.LOADING + ) + } + displayBackup() + symbolListBackup() + favoriteSymbolBackup() + weightTableBackup() + } + + private fun handleNoInternetConnection() { + _uiState.update { currentState -> + currentState.copy( + alert = AccountSettingsAlert.CONNECTION + ) + } + } + + private fun handleSuccess() { + _uiState.update { currentState -> + currentState.copy( + alert = AccountSettingsAlert.BACKUP_SUCCESS + ) + } + } + } \ No newline at end of file From 4f97b838615fdc39068ebb85c56b333d315dd732 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Wed, 22 Nov 2023 10:33:36 +0900 Subject: [PATCH 14/19] :bug: Remove isInternetAvailable --- .../src/main/java/com/example/speechbuddy/di/NetworkModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt index c5f297bc..d68dbbe8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/di/NetworkModule.kt @@ -108,7 +108,7 @@ class NetworkModule { class AuthInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - if (!isInternetAvailable(context)) throw ConnectException() + //if (!isInternetAvailable(context)) throw ConnectException() val builder = chain.request().newBuilder() try { return chain.proceed(builder.build()) From 9dc45f70a6e74cb351035b98182e49a31f2e1a79 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Wed, 22 Nov 2023 13:49:46 +0900 Subject: [PATCH 15/19] :bug: Fix dark mode bug --- .../java/com/example/speechbuddy/HomeActivity.kt | 5 ++++- .../example/speechbuddy/compose/SpeechBuddyHome.kt | 4 ++++ .../speechbuddy/compose/settings/SettingsScreen.kt | 13 +++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt index 0c341a0b..b844fec3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/HomeActivity.kt @@ -1,12 +1,13 @@ package com.example.speechbuddy -import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle import android.view.MotionEvent import android.view.inputmethod.InputMethodManager import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.core.view.WindowCompat import androidx.lifecycle.Observer import com.example.speechbuddy.compose.SpeechBuddyHome @@ -20,6 +21,7 @@ class HomeActivity : BaseActivity() { private val displaySettingsViewModel: DisplaySettingsViewModel by viewModels() + @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -47,6 +49,7 @@ class HomeActivity : BaseActivity() { } settingsRepository.darkModeLiveData.observeForever(darkModeObserver) + } private fun subscribeObservers() { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt index 67a6c6e9..f5eca6c2 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyHome.kt @@ -1,5 +1,7 @@ package com.example.speechbuddy.compose +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height @@ -36,6 +38,7 @@ data class BottomNavItem( val iconResId: Int ) +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable fun SpeechBuddyHome( @@ -122,6 +125,7 @@ private fun BottomNavigationBar( } } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun SpeechBuddyHomeNavHost( navController: NavHostController, 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 a454b9a0..0e3264e4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/SettingsScreen.kt @@ -1,13 +1,17 @@ package com.example.speechbuddy.compose.settings -import android.util.Log +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +@RequiresApi(Build.VERSION_CODES.O) @Composable fun SettingsScreen( bottomPaddingValues: PaddingValues, @@ -21,13 +25,18 @@ fun SettingsScreen( ) } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun SettingsScreenNavHost( navController: NavHostController, bottomPaddingValues: PaddingValues, isBeingReloadedForDarkModeChange: Boolean ) { - val startDestination = if (isBeingReloadedForDarkModeChange) "display" else "main" + val flag = remember{ mutableStateOf(false) } + val startDestination = if (isBeingReloadedForDarkModeChange && !flag.value) "display" else "main" + if (isBeingReloadedForDarkModeChange && !flag.value) { + flag.value = true + } val navigateToMain = { navController.navigate("main") } NavHost(navController = navController, startDestination = startDestination) { From a82bc6cbeeac9b3f9fbc344fb1e9b477a4b6ce25 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Wed, 22 Nov 2023 13:52:23 +0900 Subject: [PATCH 16/19] :bug: Adjust GuestSettings button position --- .../com/example/speechbuddy/compose/settings/GuestSettings.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt index 5f9adcd9..7ed4d248 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/GuestSettings.kt @@ -3,6 +3,7 @@ package com.example.speechbuddy.compose.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding @@ -60,6 +61,8 @@ fun GuestSettings( onClick = { viewModel.exitGuestMode() }, modifier = Modifier.offset(y = 240.dp) ) + + Spacer(modifier = Modifier.padding(70.dp)) } } } From 372e9dd51c8733b1a595ba57afc37d998bf5e183 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Wed, 22 Nov 2023 14:00:49 +0900 Subject: [PATCH 17/19] :bug: Fix Logout Backup Alertdialog onDismissRequest bug --- .../example/speechbuddy/compose/settings/AccountSettings.kt | 3 ++- .../example/speechbuddy/compose/settings/BackupSettings.kt | 3 +++ .../com/example/speechbuddy/compose/utils/AlertDialogUi.kt | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) 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 af14d093..59d5a0dc 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/AccountSettings.kt @@ -122,7 +122,8 @@ fun AccountSettings( dismissButtonText = stringResource(id = R.string.logout), confirmButtonText = stringResource(id = R.string.backup), onDismiss = { viewModel.showAlert(AccountSettingsAlert.LOGOUT) }, - onConfirm = { viewModel.backup() } + onConfirm = { viewModel.backup() }, + onDismissRequest = { viewModel.hideAlert() } ) } 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 87834523..c88eadc8 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/settings/BackupSettings.kt @@ -1,6 +1,8 @@ package com.example.speechbuddy.compose.settings +import android.os.Build import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -31,6 +33,7 @@ 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( diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt index 81b28a3b..64e9d0e4 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/utils/AlertDialogUi.kt @@ -16,10 +16,11 @@ fun AlertDialogUi( dismissButtonText: String, confirmButtonText: String, onDismiss: () -> Unit, - onConfirm: () -> Unit + onConfirm: () -> Unit, + onDismissRequest: () -> Unit = onDismiss ) { AlertDialog( - onDismissRequest = onDismiss, + onDismissRequest = onDismissRequest, confirmButton = { Button( onClick = onConfirm, From 773865c05eadc916d3b2bfa8ee2822a4a80c9f60 Mon Sep 17 00:00:00 2001 From: Minyeong Lee <13619@snu.ac.kr> Date: Wed, 22 Nov 2023 17:49:33 +0900 Subject: [PATCH 18/19] :sparkles: Implement storing last backup date logic --- .../data/local/SettingsPrefsManager.kt | 20 ++++++---- .../domain/models/SettingsPreferences.kt | 3 +- .../repository/SettingsRepository.kt | 16 +++++--- .../example/speechbuddy/utils/Constants.kt | 1 + .../viewmodel/AccountSettingsViewModel.kt | 7 +++- .../viewmodel/BackupSettingsViewModel.kt | 40 ++++++++++++++++++- .../viewmodel/DisplaySettingsViewModel.kt | 5 +-- 7 files changed, 70 insertions(+), 22 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt index 0db33489..7d72fb22 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SettingsPrefsManager.kt @@ -1,28 +1,23 @@ package com.example.speechbuddy.data.local import android.content.Context -import android.util.Log import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.createDataStore -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.AUTO_BACKUP import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.DARK_MODE import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.INITIAL_PAGE +import com.example.speechbuddy.data.local.SettingsPrefsManager.PreferencesKeys.LAST_BACKUP_DATE import com.example.speechbuddy.domain.models.SettingsPreferences import com.example.speechbuddy.utils.Constants import com.example.speechbuddy.utils.Constants.Companion.SETTINGS_PREFS -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject -import javax.inject.Singleton class SettingsPrefsManager @Inject constructor(context: Context) { @@ -40,7 +35,8 @@ class SettingsPrefsManager @Inject constructor(context: Context) { SettingsPreferences( autoBackup = preferences[AUTO_BACKUP] ?: true, darkMode = preferences[DARK_MODE] ?: false, - initialPage = preferences[INITIAL_PAGE] ?: true + initialPage = preferences[INITIAL_PAGE] ?: true, + lastBackupDate = preferences[LAST_BACKUP_DATE] ?: "" ) } @@ -62,11 +58,18 @@ class SettingsPrefsManager @Inject constructor(context: Context) { } } + suspend fun saveLastBackupDate(value: String) { + dataStore.edit { preferences -> + preferences[LAST_BACKUP_DATE] = value + } + } + suspend fun resetSettings() { dataStore.edit { preferences -> preferences[AUTO_BACKUP] = true preferences[DARK_MODE] = false preferences[INITIAL_PAGE] = true + preferences[LAST_BACKUP_DATE] = "" } } @@ -74,5 +77,6 @@ class SettingsPrefsManager @Inject constructor(context: Context) { val AUTO_BACKUP = booleanPreferencesKey(Constants.AUTO_BACKUP_PREF) val DARK_MODE = booleanPreferencesKey(Constants.DARK_MODE_PREF) val INITIAL_PAGE = booleanPreferencesKey(Constants.INITIAL_PAGE_PREF) + val LAST_BACKUP_DATE = stringPreferencesKey(Constants.LAST_BACKUP_DATE_PREF) } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt index a99d91fb..9bb55c7f 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/domain/models/SettingsPreferences.kt @@ -7,5 +7,6 @@ import kotlinx.parcelize.Parcelize data class SettingsPreferences( val darkMode: Boolean = false, val autoBackup: Boolean = true, - val initialPage: Boolean = true + val initialPage: Boolean = true, + val lastBackupDate: String = "" ) : Parcelable \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SettingsRepository.kt index 54ade132..0935e4db 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,15 +1,10 @@ package com.example.speechbuddy.repository -import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.example.speechbuddy.data.local.SettingsPrefsManager import com.example.speechbuddy.data.remote.models.SettingsBackupDto -import com.example.speechbuddy.data.remote.requests.AuthRefreshRequest -import com.example.speechbuddy.data.remote.requests.BackupWeightTableRequest import com.example.speechbuddy.domain.SessionManager -import com.example.speechbuddy.domain.models.Entry -import com.example.speechbuddy.domain.models.Symbol import com.example.speechbuddy.service.BackupService import com.example.speechbuddy.ui.models.InitialPage import com.example.speechbuddy.utils.Resource @@ -17,7 +12,6 @@ import com.example.speechbuddy.utils.ResponseHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -61,6 +55,10 @@ class SettingsRepository @Inject constructor( settingsPrefManager.saveAutoBackup(value) } + suspend fun setLastBackupDate(value: String) { + settingsPrefManager.saveLastBackupDate(value) + } + fun getDarkMode(): Flow> { return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> Resource.success(settingsPreferences.darkMode) @@ -79,6 +77,12 @@ class SettingsRepository @Inject constructor( } } + fun getLastBackupDate(): Flow> { + return settingsPrefManager.settingsPreferencesFlow.map { settingsPreferences -> + Resource.success(settingsPreferences.lastBackupDate) + } + } + suspend fun displayBackup(): Flow> = flow { try { 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 2f2484f8..a630bd0d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/utils/Constants.kt @@ -19,6 +19,7 @@ class Constants { const val AUTO_BACKUP_PREF: String = "com.example.speechbuddy.AUTO_BACKUP_PREF" const val DARK_MODE_PREF: String = "com.example.speechbuddy.DARK_MODE_PREF" const val INITIAL_PAGE_PREF: String = "com.example.speechbuddy.INITIAL_PAGE_PREF" + const val LAST_BACKUP_DATE_PREF: String = "com.example.speechbuddy.LAST_BACKUP_DATE_PREF" const val MINIMUM_PASSWORD_LENGTH = 8 const val MAXIMUM_NICKNAME_LENGTH = 15 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 5cc2b96b..1fb80024 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/AccountSettingsViewModel.kt @@ -2,8 +2,6 @@ package com.example.speechbuddy.viewmodel import android.os.Build import androidx.annotation.RequiresApi -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.speechbuddy.domain.SessionManager @@ -20,6 +18,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -180,7 +179,11 @@ class AccountSettingsViewModel @Inject internal constructor( } } + @RequiresApi(Build.VERSION_CODES.O) private fun handleSuccess() { + viewModelScope.launch { + settingsRepository.setLastBackupDate(LocalDate.now().toString()) + } _uiState.update { currentState -> currentState.copy( alert = AccountSettingsAlert.BACKUP_SUCCESS diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt index 8030c44c..0bfd52c0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/BackupSettingsViewModel.kt @@ -24,7 +24,12 @@ class BackupSettingsViewModel @Inject internal constructor( private val repository: SettingsRepository, ) : ViewModel() { - private val _uiState = MutableStateFlow(BackupSettingsUiState()) + private val _uiState = MutableStateFlow( + BackupSettingsUiState( + lastBackupDate = getLastBackupDate(), + isAutoBackupEnabled = getAutoBackup() + ) + ) val uiState: StateFlow = _uiState.asStateFlow() private val _loading = MutableLiveData(false) @@ -42,6 +47,37 @@ class BackupSettingsViewModel @Inject internal constructor( // TODO: Implement automated backup } + private fun setLastBackupDate(value: String) { + _uiState.update { currentState -> + currentState.copy( + lastBackupDate = value + ) + } + viewModelScope.launch { + repository.setLastBackupDate(value) + } + } + + private fun getAutoBackup(): Boolean { + var autoBackup = false + viewModelScope.launch { + repository.getAutoBackup().collect { + autoBackup = it.data?: false + } + } + return autoBackup + } + + private fun getLastBackupDate(): String { + var lastBackupDate = "" + viewModelScope.launch { + repository.getLastBackupDate().collect { + lastBackupDate = it.data?: "" + } + } + return lastBackupDate + } + fun toastDisplayed() { _uiState.update { currentState -> currentState.copy( @@ -123,10 +159,10 @@ class BackupSettingsViewModel @Inject internal constructor( @RequiresApi(Build.VERSION_CODES.O) private fun handleSuccess() { _loading.value = false + setLastBackupDate(LocalDate.now().toString()) _uiState.update { currentState -> currentState.copy ( alert = BackupSettingsAlert.SUCCESS, - lastBackupDate = LocalDate.now().toString() ) } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt index d3514ecb..d5191b36 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/DisplaySettingsViewModel.kt @@ -1,6 +1,5 @@ package com.example.speechbuddy.viewmodel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.speechbuddy.repository.SettingsRepository @@ -53,7 +52,7 @@ class DisplaySettingsViewModel @Inject internal constructor( } fun getDarkMode(): Boolean { - var darkMode: Boolean = false + var darkMode = false viewModelScope.launch { repository.getDarkMode().collect { darkMode = it.data?: false @@ -63,7 +62,7 @@ class DisplaySettingsViewModel @Inject internal constructor( } fun getInitialPage(): Boolean { - var initialPage: Boolean = true + var initialPage = true viewModelScope.launch { repository.getInitialPage().collect { initialPage = it.data?: true From 611f5eb97a78d4dcb3dbaeef3418e2ec60d7700f Mon Sep 17 00:00:00 2001 From: JH Date: Wed, 22 Nov 2023 20:02:06 +0900 Subject: [PATCH 19/19] :card_file_box: Implement clear weighttable, symbols table --- .../main/java/com/example/speechbuddy/data/local/SymbolDao.kt | 3 +++ .../java/com/example/speechbuddy/data/local/WeightRowDao.kt | 2 ++ .../com/example/speechbuddy/repository/SymbolRepository.kt | 4 ++++ .../example/speechbuddy/repository/WeightTableRepository.kt | 4 ++++ 4 files changed, 13 insertions(+) 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 09f33543..3b577707 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/SymbolDao.kt @@ -46,4 +46,7 @@ interface SymbolDao { @Upsert suspend fun upsertAll(symbolEntities: List) + + @Query("DELETE FROM symbols") + suspend fun deleteAllSymbols() } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt b/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt index 08877251..042be11d 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/data/local/WeightRowDao.kt @@ -21,5 +21,7 @@ interface WeightRowDao { @Upsert suspend fun upsertAll(weightRowEntities: List) + @Query("DELETE FROM weighttable") + suspend fun deleteAllWeightRows() } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt index cc03e921..6507f330 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/SymbolRepository.kt @@ -45,6 +45,10 @@ class SymbolRepository @Inject constructor( symbolEntities.map { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } + suspend fun deleteAllSymbols() { + symbolDao.deleteAllSymbols() + } + fun getLastSymbol() = symbolDao.getLastSymbol().map { symbolEntities -> symbolEntities.first().let { symbolEntity -> symbolMapper.mapToDomainModel(symbolEntity) } } 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 5124200c..93fc6417 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/repository/WeightTableRepository.kt @@ -46,6 +46,10 @@ class WeightTableRepository @Inject constructor( } } + suspend fun deleteAllWeightRows() { + weightRowDao.deleteAllWeightRows() + } + suspend fun getBackupWeightTableRequest(): BackupWeightTableRequest { val weightRowList = getAllWeightRows().firstOrNull() ?: emptyList() val weightTableEntities = weightRowList.map { weightRow ->