diff --git a/app/build.gradle b/app/build.gradle index e031aa6d..377a4c5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -208,6 +208,7 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' implementation "androidx.activity:activity-compose:$rootProject.composeActivityVersion" implementation "com.google.accompanist:accompanist-systemuicontroller:$rootProject.accompanist_version" + implementation "com.google.accompanist:accompanist-permissions:$rootProject.accompanist_version" //Coil implementation("io.coil-kt:coil-compose:$rootProject.coilVersion") @@ -217,4 +218,7 @@ dependencies { //for live data to state implementation("androidx.compose.runtime:runtime-livedata:$rootProject.composeLiveData") + + //for language change + implementation "com.github.YarikSOffice:lingver:$rootProject.languageLibrary" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e185c999..a11998c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,14 +2,11 @@ - - - + + + securityUtil.encryptToken(_lang) + } ?: "" + } + } + + fun getAppLanguage(): Flow { + return userDataStore.data.map { + val language = it[APP_LANGUAGE]?.let { _lang -> + securityUtil.decryptToken(_lang) + } + language ?: "en" + } + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt b/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt new file mode 100644 index 00000000..cea9c71c --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.di.models + +import android.content.Context + +data class AppContext( + val context: Context +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt b/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt index 655c2b49..ac70b00a 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt @@ -73,6 +73,7 @@ import org.aossie.agoraandroid.domain.useCases.profile.GetUserDataUseCase import org.aossie.agoraandroid.domain.useCases.profile.ProfileUseCases import org.aossie.agoraandroid.domain.useCases.profile.ToggleTwoFactorAuthUseCase import org.aossie.agoraandroid.domain.useCases.profile.UpdateUserUseCase +import org.aossie.agoraandroid.ui.di.models.AppContext import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.InternetManager import org.aossie.agoraandroid.utilities.SecurityUtil @@ -501,4 +502,10 @@ class AppModule { ): CastVoteActivityUseCases { return CastVoteActivityUseCases(castVoteUseCase, verifyVotersUseCase) } + + @Singleton + @Provides + fun provideAppContext(context: Context): AppContext { + return AppContext(context) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt index 1c309fa5..e78dc5e3 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt @@ -1,21 +1,27 @@ package org.aossie.agoraandroid.ui.fragments.home +import android.content.Context +import android.content.Intent import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.yariksoffice.lingver.Lingver import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.data.db.PreferenceProvider import org.aossie.agoraandroid.domain.useCases.homeFragment.HomeFragmentUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.utilities.LocaleUtil import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import java.text.SimpleDateFormat import java.util.Calendar @@ -30,10 +36,9 @@ const val ACTIVE_ELECTION_COUNT = "activeElectionsCount" class HomeViewModel @Inject constructor( - private val homeViewModelUseCases: HomeFragmentUseCases + private val homeViewModelUseCases: HomeFragmentUseCases, + private val prefs: PreferenceProvider ) : ViewModel() { - private val _getLogoutStateFLow: MutableStateFlow?> = MutableStateFlow(null) - val getLogoutStateFlow: StateFlow?> = _getLogoutStateFLow var sessionExpiredListener: SessionExpiredListener? = null private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) private val currentDate: Date = Calendar.getInstance() @@ -46,6 +51,15 @@ constructor( private val _countMediatorLiveData = MediatorLiveData>() val countMediatorLiveData = _countMediatorLiveData + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() + + val appLanguage = prefs.getAppLanguage() + val getSupportedLanguages = LocaleUtil.getSupportedLanguages() + init { _countMediatorLiveData.value = mutableMapOf( TOTAL_ELECTION_COUNT to 0, @@ -108,23 +122,44 @@ constructor( } fun doLogout() { - _getLogoutStateFLow.value = ResponseUI.loading() + showLoading("Logging you out...") viewModelScope.launch { try { homeViewModelUseCases.logOut() - _getLogoutStateFLow.value = ResponseUI.success() + hideSnackBar() + hideLoading() + _uiEvents.emit(UiEvents.UserLoggedOut) } catch (e: ApiException) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } + fun changeLanguage(newLanguage: Pair, context: Context) { + viewModelScope.launch { + prefs.updateAppLanguage(newLanguage.second) + Lingver.getInstance().setLocale(context, newLanguage.second) + delay(500) + restartApp(context) + } + } + + private fun restartApp(context: Context) { + val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) + intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + sealed class UiEvents{ + object UserLoggedOut:UiEvents() + } + private fun showLoading(message: Any) { _progressAndErrorState.value = progressAndErrorState.value.copy( loading = Pair(message,true) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt index dea2aae1..5c632ddd 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt @@ -1,65 +1,35 @@ package org.aossie.agoraandroid.ui.fragments.profile -import android.Manifest -import android.app.Activity -import android.content.ContentResolver -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat -import androidx.core.net.toUri -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.login.LoginManager -import com.squareup.picasso.NetworkPolicy.OFFLINE +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R -import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.DialogChangeAvatarBinding -import org.aossie.agoraandroid.databinding.FragmentProfileBinding -import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.auth.login.LoginViewModel import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel -import org.aossie.agoraandroid.utilities.GetBitmapFromUri -import org.aossie.agoraandroid.utilities.HideKeyboard.hideKeyboardInFrag -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.canAuthenticateBiometric -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.isUrl -import org.aossie.agoraandroid.utilities.loadImage -import org.aossie.agoraandroid.utilities.loadImageFromMemoryNoCache -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toByteArray -import org.aossie.agoraandroid.utilities.toggleIsEnable -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException +import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel.UiEvents.UserLoggedOut +import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel.UiEvents.PasswordChanged +import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel.UiEvents.TwoFactorAuthToggled +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject -const val CAMERA_PERMISSION_REQUEST_CODE = 1 -const val STORAGE_PERMISSION_REQUEST_CODE = 2 -const val CAMERA_INTENT_REQUEST_CODE = 3 -const val STORAGE_INTENT_REQUEST_CODE = 4 - class ProfileFragment @Inject constructor( @@ -67,240 +37,60 @@ constructor( private val prefs: PreferenceProvider ) : BaseFragment(viewModelFactory) { - private var mAvatar = MutableLiveData() - - private var encodedImage: String? = null - private val viewModel: ProfileViewModel by viewModels { viewModelFactory } private val loginViewModel: LoginViewModel by viewModels { viewModelFactory } - private val homeViewModel: HomeViewModel by viewModels { viewModelFactory } - - private lateinit var mUser: UserModel - private lateinit var binding: FragmentProfileBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentProfileBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - homeViewModel.sessionExpiredListener = this loginViewModel.sessionExpiredListener = this + viewModel.sessionExpiredListener = this - binding.firstNameTiet.doAfterTextChanged { - if (it.isNullOrEmpty()) binding.firstNameTil.error = getString(string.first_name_empty) - else binding.firstNameTil.error = null - } - binding.lastNameTiet.doAfterTextChanged { - if (it.isNullOrEmpty()) binding.lastNameTil.error = getString(string.last_name_empty) - else binding.lastNameTil.error = null - } - binding.newPasswordTiet.doAfterTextChanged { - when { - it.isNullOrEmpty() -> - binding.newPasswordTil.error = - getString(string.password_empty_warn) - else -> binding.newPasswordTil.error = null - } - checkNewPasswordAndConfirmPassword(it) - } - binding.confirmPasswordTiet.doAfterTextChanged { - when { - it.isNullOrEmpty() -> - binding.confirmPasswordTil.error = - getString(string.password_empty_warn) - it.toString() != binding.newPasswordTiet.text.toString() -> - binding.confirmPasswordTil.error = - getString(string.password_not_match_warn) - else -> binding.confirmPasswordTil.error = null - } - } - setObserver() - - binding.updateProfileBtn.setOnClickListener { - binding.progressBar.show() - toggleIsEnable() - if (binding.firstNameTil.error == null && binding.lastNameTil.error == null) { - hideKeyboardInFrag(this@ProfileFragment) - val updatedUser = mUser - updatedUser.let { - it.firstName = binding.firstNameTiet.text.toString() - it.lastName = binding.lastNameTiet.text.toString() - } - viewModel.updateUser( - updatedUser - ) - } else { - binding.progressBar.hide() - toggleIsEnable() - } - } - - binding.swBiometric.setOnCheckedChangeListener { buttonView, isChecked -> - lifecycleScope.launch { - prefs.enableBiometric(isChecked) - } - } - if (!requireContext().canAuthenticateBiometric()) binding.swBiometric.visibility = View.GONE - - binding.switchWidget.setOnClickListener { - if (binding.switchWidget.isChecked) { - AlertDialog.Builder(requireContext()) - .setTitle("Please Confirm") - .setMessage("Are you sure you want to enable two factor authentication") - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { dialog, _ -> - binding.progressBar.show() - toggleIsEnable() - viewModel.toggleTwoFactorAuth() - dialog.cancel() - } - .setNegativeButton(android.R.string.cancel) { dialog, _ -> - binding.switchWidget.isChecked = false - dialog.cancel() - } - .create() - .show() - } else { - AlertDialog.Builder(requireContext()) - .setTitle("Please Confirm") - .setMessage("Are you sure you want to disable two factor authentication") - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { dialog, _ -> - binding.progressBar.show() - toggleIsEnable() - viewModel.toggleTwoFactorAuth() - dialog.cancel() - } - .setNegativeButton(android.R.string.cancel) { dialog, _ -> - binding.switchWidget.isChecked = true - dialog.cancel() - } - .create() - .show() - } - } - - binding.fabEditProfilePic.setOnClickListener { - showChangeProfileDialog() - } - - binding.changePasswordBtn.setOnClickListener { - val newPass = binding.newPasswordTiet.text.toString() - val conPass = binding.confirmPasswordTiet.text.toString() - when { - newPass.isEmpty() -> - binding.newPasswordTil.error = getString(string.password_empty_warn) - conPass.isEmpty() -> - binding.confirmPasswordTil.error = getString(string.password_empty_warn) - newPass != conPass -> - binding.confirmPasswordTil.error = getString(string.password_not_match_warn) - else -> updateUIAndChangePassword() - } - } - } - - private fun updateUIAndChangePassword() { - binding.progressBar.show() - toggleIsEnable() - hideKeyboardInFrag(this@ProfileFragment) - viewModel.changePassword(binding.newPasswordTiet.text.toString()) - } - - private fun decodeBitmap(encodedBitmap: String): Bitmap { - val decodedString = Base64.decode(encodedBitmap, Base64.NO_WRAP) - return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) - } - - private fun cacheAndSaveImage(url: String) { - binding.ivProfilePic.loadImage(url, OFFLINE) { - binding.ivProfilePic.loadImage(url) - } - } + composeView.setContent { - private fun setObserver() { - mAvatar.observe( - viewLifecycleOwner, - Observer { - binding.ivProfilePic.loadImageFromMemoryNoCache(it) - } - ) + val progressErrorState1 = viewModel.progressAndErrorState + val progressErrorState2 = homeViewModel.progressAndErrorState - lifecycleScope.launch { - viewModel.user.collect { - if (it != null) { - updateUI(it) - } - } - } + val progressErrorState by merge(progressErrorState1,progressErrorState2).collectAsState(initial = ScreensState()) - viewModel.passwordRequestCode.observe( - viewLifecycleOwner, - Observer { - handlePassword(it) - } - ) + val userData by viewModel.userModelState.collectAsState() + val profileDataState by viewModel.profileDataState.collectAsState() - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> onLoadingStarted() - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() + LaunchedEffect(key1 = viewModel) { + viewModel.uiEvents.collectLatest { + when(it) { + PasswordChanged -> { + loginViewModel.logInRequest( + userData?.username!!, profileDataState.confirmPassword, userData.trustedDevice + ) } - ResponseUI.Status.ERROR -> { - onError(it.message) + TwoFactorAuthToggled -> { + homeViewModel.doLogout() } - else -> {} } } } - } - - viewModel.userUpdateResponse.observe( - viewLifecycleOwner, - Observer { - handleUser(it) - } - ) - - viewModel.toggleTwoFactorAuthResponse.observe( - viewLifecycleOwner, - Observer { - handleTwoFactorAuthentication(it) - homeViewModel.doLogout() - } - ) - - viewModel.changeAvatarResponse.observe( - viewLifecycleOwner, - Observer { - handleChangeAvatar(it) - } - ) - lifecycleScope.launch { - homeViewModel.getLogoutStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.ERROR -> onError(it.message) - - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() + LaunchedEffect(key1 = viewModel) { + homeViewModel.uiEvents.collectLatest { + when(it) { + UserLoggedOut -> { lifecycleScope.launch { if (prefs.getIsFacebookUser().first()) { LoginManager.getInstance() @@ -308,294 +98,24 @@ constructor( } } homeViewModel.deleteUserData() - Navigation.findNavController(binding.root) - .navigate( + delay(2000) + findNavController().navigate( ProfileFragmentDirections.actionProfileFragmentToWelcomeFragment() ) } - ResponseUI.Status.LOADING -> onLoadingStarted() - else -> {} } } } - } - } - - private fun updateUI( - it: UserModel, - ) { - binding.userNameTv.text = it.username - binding.emailIdTv.text = it.email - binding.firstNameTiet.setText(it.firstName) - binding.lastNameTiet.setText(it.lastName) - binding.switchWidget.isChecked = it.twoFactorAuthentication ?: false - lifecycleScope.launch { - binding.swBiometric.isChecked = prefs.isBiometricEnabled().first() - } - mUser = it - if (it.avatarURL != null) { - if (it.avatarURL.isUrl()) - cacheAndSaveImage(it.avatarURL) - else { - val bitmap = decodeBitmap(it.avatarURL) - setAvatarFile(bitmap.toByteArray()) - } - } - } - - private fun onLoadingStarted() { - binding.progressBar.show() - toggleIsEnable() - } - - private fun onError(message: String?) { - binding.progressBar.hide() - notify(message) - toggleIsEnable() - } - - private fun showChangeProfileDialog() { - val dialogView = DialogChangeAvatarBinding.inflate(LayoutInflater.from(context)) - - val dialog = AlertDialog.Builder(requireContext()) - .setView(dialogView.root) - .create() - - dialogView.deleteProfile.setOnClickListener { - dialog.cancel() - deletePic() - } - - dialogView.cameraView.setOnClickListener { - dialog.cancel() - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - ) { - openCamera() - } else { - askCameraPermission() - } - } - - dialogView.galleryView.setOnClickListener { - dialog.cancel() - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED - ) { - openGallery() - } else { - askReadStoragePermission() - } - } - - dialog.show() - } - - private fun deletePic() { - val imageUri = Uri.parse( - ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + resources.getResourcePackageName(R.drawable.ic_user1) + - '/' + resources.getResourceTypeName(R.drawable.ic_user1) + '/' + resources.getResourceEntryName( - R.drawable.ic_user1 - ) - ) - try { - val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(requireContext(), imageUri) - encodedImage = encodeJpegImage(bitmap!!) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } catch (e: FileNotFoundException) { - notify(getString(string.file_not_found)) - } - } - - private fun askReadStoragePermission() { - requestPermissions( - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_REQUEST_CODE - ) - } - - private fun askCameraPermission() { - requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE) - } - - private fun handleChangeAvatar(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.profile_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handleUser(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.user_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handleTwoFactorAuthentication(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.authentication_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handlePassword(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.password_updated)) - loginViewModel.logInRequest( - mUser.username!!, binding.newPasswordTiet.text.toString(), mUser.trustedDevice - ) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun checkNewPasswordAndConfirmPassword(s: Editable?) { - if (s.toString() == binding.confirmPasswordTiet.text.toString() - .trim() - ) { - binding.confirmPasswordTil.error = null - } else { - if (!binding.confirmPasswordTiet.text.isNullOrEmpty()) { - binding.confirmPasswordTil.error = - getString(string.password_not_match_warn) - } - } - } - - private fun onStarted() { - binding.progressBar.show() - toggleIsEnable() - } - - private fun onFailure(message: String?) { - binding.progressBar.hide() - notify(message) - toggleIsEnable() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - intentData: Intent? - ) { - super.onActivityResult(requestCode, resultCode, intentData) - if (resultCode != Activity.RESULT_OK) return - - if (requestCode == STORAGE_INTENT_REQUEST_CODE && intentData?.data != null) { - val imageUri = intentData.data ?: return - try { - val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(requireContext(), imageUri) - encodedImage = encodeJpegImage(bitmap!!) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } catch (e: FileNotFoundException) { - notify(getString(string.file_not_found)) - } - } else if (requestCode == CAMERA_INTENT_REQUEST_CODE) { - val bitmap = intentData?.extras?.get("data") - if (bitmap is Bitmap) { - encodedImage = encodePngImage(bitmap) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } - } - } - - private fun openGallery() { - val galleryIntent = Intent() - galleryIntent.let { - it.type = "image/*" - it.action = Intent.ACTION_GET_CONTENT - } - startActivityForResult(galleryIntent, STORAGE_INTENT_REQUEST_CODE) - } - - private fun openCamera() { - val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - startActivityForResult(cameraIntent, CAMERA_INTENT_REQUEST_CODE) - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - openGallery() - } else { - notify(getString(string.permission_denied)) - } - } else if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - openCamera() - } else { - notify(getString(string.permission_denied)) - } - } - } - - private fun encodeJpegImage(bitmap: Bitmap): String { - val bytes = bitmap.toByteArray(Bitmap.CompressFormat.JPEG) - setAvatarFile(bytes) - return Base64.encodeToString(bytes, Base64.NO_WRAP) - } - - private fun encodePngImage(bitmap: Bitmap): String { - val bytes = bitmap.toByteArray(Bitmap.CompressFormat.PNG) - setAvatarFile(bytes) - return Base64.encodeToString(bytes, Base64.NO_WRAP) - } - - private fun setAvatarFile(bytes: ByteArray) { - try { - val avatar = File(context?.cacheDir, "avatar") - if (avatar.exists()) { - avatar.delete() + AgoraTheme { + ProfileScreen( + prefs = prefs, + screenState = progressErrorState, + userData = userData, + profileDataState = profileDataState, + ){ + viewModel.onEvent(it) + } } - val fos = FileOutputStream(avatar) - fos.write(bytes) - fos.flush() - fos.close() - mAvatar.value = avatar - } catch (e: IOException) { - e.printStackTrace() - notify(getString(string.error_loading_image)) } } - - private fun toggleIsEnable() { - binding.updateProfileBtn.toggleIsEnable() - binding.changePasswordBtn.toggleIsEnable() - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt index f1f76c42..597dd08f 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt @@ -1,64 +1,215 @@ package org.aossie.agoraandroid.ui.fragments.profile -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Base64 +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.network.responses.AuthToken import org.aossie.agoraandroid.domain.model.UpdateUserDtoModel import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.useCases.profile.ProfileUseCases +import org.aossie.agoraandroid.ui.di.models.AppContext import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenDataState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.ChangePasswordClick +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.DeletePic +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredConfirmPassword +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.ToggleTwoFactor +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.UpdateImage +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.UpdateProfileClick import org.aossie.agoraandroid.utilities.ApiException +import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.utilities.GetBitmapFromUri import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException +import org.aossie.agoraandroid.utilities.toByteArray import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException import javax.inject.Inject class ProfileViewModel @Inject constructor( - private val profileUseCases: ProfileUseCases + private val profileUseCases: ProfileUseCases, + private val appContext: AppContext ) : ViewModel() { val user = profileUseCases.getUser() - private lateinit var sessionExpiredListener: SessionExpiredListener - private val _passwordRequestCode = MutableLiveData>() + var sessionExpiredListener: SessionExpiredListener? = null - val passwordRequestCode: LiveData> - get() = _passwordRequestCode + private val _userModelState = MutableStateFlow (UserModel()) + val userModelState = _userModelState.asStateFlow() - private val _userUpdateResponse = MutableLiveData>() + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() - val userUpdateResponse: LiveData> - get() = _userUpdateResponse + private val _profileDataState = MutableStateFlow (ProfileScreenDataState()) + val profileDataState = _profileDataState.asStateFlow() - private val _toggleTwoFactorAuthResponse = MutableLiveData>() + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() - val toggleTwoFactorAuthResponse: LiveData> - get() = _toggleTwoFactorAuthResponse + init { + viewModelScope.launch { + user.collectLatest { + _userModelState.value = it + if(profileDataState.value.avatar==null){ + val bitmap = decodeBitmap(it.avatarURL!!) + setAvatar(bitmap) + } + _profileDataState.value = profileDataState.value.copy( + firstName = it.firstName?:"", + lastName = it.lastName?:"", + ) + } + } + } + + fun onEvent(event: ProfileScreenEvent) { + when(event) { + ChangePasswordClick -> { + val pass1 = profileDataState.value.newPassword + val pass2 = profileDataState.value.confirmPassword - private val _changeAvatarResponse = MutableLiveData>() + if(pass1.isEmpty() || pass2.isEmpty()) { + showMessage(string.password_empty_warn) + }else if(pass1 != pass2) { + showMessage(string.password_not_match_warn) + }else { + changePassword(pass2) + } + } + is EnteredConfirmPassword -> { + _profileDataState.value = profileDataState.value.copy( + confirmPassword = event.confirmPassword + ) + } + is EnteredFirstName -> { + _profileDataState.value = profileDataState.value.copy( + firstName = event.firstName + ) + } + is EnteredLastName -> { + _profileDataState.value = profileDataState.value.copy( + lastName = event.lastName + ) + } + is EnteredPassword -> { + _profileDataState.value = profileDataState.value.copy( + newPassword = event.password + ) + } + is ToggleTwoFactor -> { + showLoading("${if (event.checked) "Enabling" else "Disabling"} two factor authentication...") + toggleTwoFactorAuth() + } + UpdateProfileClick -> { + val firstName = profileDataState.value.firstName.trim() + val lastName = profileDataState.value.lastName.trim() - val changeAvatarResponse: LiveData> - get() = _changeAvatarResponse + if(firstName.isNullOrEmpty()) { + showMessage(string.first_name_empty) + }else if(lastName.isNullOrEmpty()) { + showMessage(string.last_name_empty) + }else { + val updatedUser = userModelState.value + updatedUser.let { + it.firstName = firstName + it.lastName = lastName + } + updateUser( + updatedUser + ) + } + } + is DeletePic -> { + deletePic() + } + is UpdateImage -> { + updateImage(event.uri) + } + } + } - fun changePassword(password: String) { + private fun updateImage(uri: Uri) = viewModelScope.launch { + showLoading("Updating image...") + try { + val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(appContext.context, uri) + val encodedImage = encodeJpegImage(bitmap!!) + val url = encodedImage!!.toUri() + changeAvatar( + url.toString(), + userModelState.value + ) + } catch (e: FileNotFoundException) { + showMessage(string.file_not_found) + } + } + private fun encodeJpegImage(bitmap: Bitmap): String { + val bytes = bitmap.toByteArray(Bitmap.CompressFormat.JPEG) + return Base64.encodeToString(bytes, Base64.NO_WRAP) + } + + private fun deletePic() = viewModelScope.launch { + showLoading("Deleting avatar..") + val imageUri = Uri.parse( + ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + appContext.context.resources.getResourcePackageName(R.drawable.ic_user1) + + '/' + appContext.context.resources.getResourceTypeName(R.drawable.ic_user1) + '/' + appContext.context.resources.getResourceEntryName( + R.drawable.ic_user1 + ) + ) + try { + val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(appContext.context, imageUri) + val encodedImage = encodeJpegImage(bitmap!!) + val url = encodedImage!!.toUri() + changeAvatar( + url.toString(), + userModelState.value + ) + } catch (e: FileNotFoundException) { + showMessage(string.file_not_found) + } + } + + fun changePassword(password: String) { + showLoading("Changing password...") viewModelScope.launch { try { profileUseCases.changePassword(password) - _passwordRequestCode.value = ResponseUI.success() + hideLoading() + showMessage(R.string.password_updated) + _uiEvents.emit(UiEvents.PasswordChanged) } catch (e: ApiException) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -67,11 +218,9 @@ constructor( url: String, user: UserModel ) { - viewModelScope.launch { try { profileUseCases.changeAvatar(url) - val authResponse = profileUseCases.getUserData() Timber.d(authResponse.toString()) authResponse.let { @@ -82,16 +231,18 @@ constructor( user.trustedDevice ) profileUseCases.saveUser(mUser) + showMessage(string.profile_updated) + val bitmap = decodeBitmap(mUser.avatarURL!!) + setAvatar(bitmap) } - _changeAvatarResponse.value = ResponseUI.success() } catch (e: ApiException) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -100,15 +251,16 @@ constructor( viewModelScope.launch { try { profileUseCases.toggleTwoFactorAuth() - _toggleTwoFactorAuthResponse.value = ResponseUI.success() + showMessage(R.string.authentication_updated) + _uiEvents.emit(UiEvents.TwoFactorAuthToggled) } catch (e: ApiException) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -116,6 +268,7 @@ constructor( fun updateUser( user: UserModel ) { + showLoading("Updating profile...") viewModelScope.launch { try { val updateUserDtoModel = UpdateUserDtoModel( @@ -130,16 +283,83 @@ constructor( ) profileUseCases.updateUser(updateUserDtoModel) profileUseCases.saveUser(user) - _userUpdateResponse.value = ResponseUI.success() + showMessage(R.string.user_updated) } catch (e: ApiException) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) + } + } + } + + private fun decodeBitmap(encodedBitmap: String): Bitmap { + val decodedString = Base64.decode(encodedBitmap, Base64.NO_WRAP) + return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) + } + + private fun setAvatar(bitmap: Bitmap) = viewModelScope.launch { + val bytes = bitmap.toByteArray(Bitmap.CompressFormat.PNG) + try { + val avatarFolder = File(appContext.context.cacheDir, "avatars") + if (!avatarFolder.exists()) { + avatarFolder.mkdir() + } else { + avatarFolder.listFiles()?.forEach { file -> + file.delete() + } + } + val avatar = File(avatarFolder, "avatar_${System.currentTimeMillis()}.png") + if (avatar.exists()) { + avatar.delete() } + val fos = FileOutputStream(avatar) + fos.write(bytes) + fos.flush() + fos.close() + _profileDataState.value = profileDataState.value.copy( + avatar = avatar + ) + } catch (e: IOException) { + e.printStackTrace() + showMessage(string.error_loading_image) + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() } } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } + + sealed class UiEvents{ + object PasswordChanged:UiEvents() + object TwoFactorAuthToggled:UiEvents() + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt index 941ebdc6..14684a71 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt @@ -7,32 +7,39 @@ import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.login.LoginManager -import com.squareup.picasso.NetworkPolicy.OFFLINE +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentSettingsBinding import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel +import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel.UiEvents.UserLoggedOut import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide +import org.aossie.agoraandroid.ui.screens.settings.SettingScreen +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.ChangeAppLanguage +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnAboutUsClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnAccountSettingClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnContactUsClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnLogoutClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnShareWithOthersClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import org.aossie.agoraandroid.utilities.isUrl -import org.aossie.agoraandroid.utilities.loadImage -import org.aossie.agoraandroid.utilities.loadImageFromMemoryNoCache -import org.aossie.agoraandroid.utilities.show import org.aossie.agoraandroid.utilities.toByteArray -import org.aossie.agoraandroid.utilities.toggleIsEnable import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -54,7 +61,7 @@ constructor( private var mAvatar = MutableLiveData() - private lateinit var binding: FragmentSettingsBinding + private lateinit var composeView: ComposeView private lateinit var mUser: UserModel @@ -67,49 +74,28 @@ constructor( container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - lifecycleScope.launch { - val user = viewModel.user - user.collect { - if (it != null) { - binding.tvEmailId.text = it.email - binding.tvName.text = (it.firstName ?: "") + " " + (it.lastName ?: "") - mUser = it - if (it.avatarURL != null) { - if (it.avatarURL.isUrl()) - cacheAndSaveImage(it.avatarURL) - else { - val bitmap = decodeBitmap(it.avatarURL) - setAvatar(bitmap) - } - } - } - } - } + homeViewModel.sessionExpiredListener = this - mAvatar.observe( - viewLifecycleOwner, - Observer { - binding.imageView.loadImageFromMemoryNoCache(it) - } - ) - - lifecycleScope.launch { - homeViewModel.getLogoutStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) - binding.tvLogout.toggleIsEnable() - } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() + composeView.setContent { + + val context = LocalContext.current + val userState by viewModel.user.collectAsState(UserModel()) + val appLanguageState by homeViewModel.appLanguage.collectAsState("") + val supportedLang = homeViewModel.getSupportedLanguages + val avatar by mAvatar.observeAsState() + val progressErrorState by homeViewModel.progressAndErrorState.collectAsState() + + LaunchedEffect(key1 = Unit) { + homeViewModel.uiEvents.collectLatest { event -> + when(event){ + UserLoggedOut -> { lifecycleScope.launch { if (prefs.getIsFacebookUser().first()) { LoginManager.getInstance() @@ -118,56 +104,58 @@ constructor( } homeViewModel.deleteUserData() notify("Logged Out") - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToWelcomeFragment() - ) + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToWelcomeFragment() + ) } - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - binding.tvLogout.toggleIsEnable() + } + } + + viewModel.user.collect { + if (it != null) { + mUser = it + if (it.avatarURL != null) { + if (!it.avatarURL.isUrl()) { + val bitmap = decodeBitmap(it.avatarURL) + setAvatar(bitmap) + } } - else -> {} } } } - } - homeViewModel.sessionExpiredListener = this - - binding.tvAccountSettings.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(SettingsFragmentDirections.actionSettingsFragmentToProfileFragment()) - } - binding.tvShare.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToShareWithOthersFragment() - ) - } - - binding.tvAbout.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToAboutFragment() - ) - } - - binding.tvContactUs.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToContactUsFragment() - ) - } - - binding.tvLogout.setOnClickListener { - homeViewModel.doLogout() - } - } - - private fun cacheAndSaveImage(url: String) { - binding.imageView.loadImage(url, OFFLINE) { - binding.imageView.loadImage(url) + AgoraTheme { + SettingScreen(userState,avatar,appLanguageState,supportedLang,progressErrorState){ event-> + when(event){ + OnAboutUsClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToAboutFragment() + ) + } + OnAccountSettingClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToProfileFragment() + ) + } + OnContactUsClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToContactUsFragment() + ) + } + OnLogoutClick -> { + homeViewModel.doLogout() + } + OnShareWithOthersClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToShareWithOthersFragment() + ) + } + is ChangeAppLanguage -> { + homeViewModel.changeLanguage(event.language,context) + } + } + } + } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt new file mode 100644 index 00000000..abcf4f24 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt @@ -0,0 +1,281 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.core.content.FileProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.aossie.agoraandroid.BuildConfig +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.data.db.PreferenceProvider +import org.aossie.agoraandroid.domain.model.UserModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PermissionsDialog +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryLabelTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryPasswordField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.profile.component.ConfirmationDialog +import org.aossie.agoraandroid.ui.screens.profile.component.IconTextSwitchButton +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Camera +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Delete +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Gallery +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileItem +import org.aossie.agoraandroid.utilities.canAuthenticateBiometric +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Objects + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun ProfileScreen( + prefs: PreferenceProvider, + screenState: ScreensState, + userData: UserModel?, + profileDataState: ProfileScreenDataState, + onEvent: (ProfileScreenEvent) -> Unit +) { + + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val twoFactorAuth = remember { + mutableStateOf(false) + } + + val bioMetricState = remember { + mutableStateOf(false) + } + + val showTwoFactorDialog = remember { + mutableStateOf(false) + } + val storagePermissionState = rememberMultiplePermissionsState( + listOf(android.Manifest.permission.READ_EXTERNAL_STORAGE,android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + ) + val cameraPermissionState = rememberMultiplePermissionsState( + listOf( android.Manifest.permission.CAMERA) + ) + val permissionDialog = remember { mutableStateOf(Pair("",false) to storagePermissionState) } + val storagePermissionText = stringResource(id = string.storage_permission_required) + val cameraPermissionText = stringResource(id = string.camera_permission_required) + + val getImageFromGallery = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + uri?.let { + onEvent(ProfileScreenEvent.UpdateImage(it)) + } + } + + val file = context.createImageFile() + val uri = FileProvider.getUriForFile( + Objects.requireNonNull(context), + BuildConfig.APPLICATION_ID + ".provider", file + ) + + val cameraLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { + onEvent(ProfileScreenEvent.UpdateImage(uri)) + } + + LaunchedEffect(key1 = prefs) { + bioMetricState.value = prefs.isBiometricEnabled().first() + } + + LaunchedEffect(key1 = userData) { + userData?.let { + twoFactorAuth.value = it.twoFactorAuthentication?:false + } + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)){ + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + + item { + ProfileItem(userModel = userData, avatar = profileDataState.avatar) { + when(it) { + Delete -> { + onEvent(ProfileScreenEvent.DeletePic) + } + Camera -> { + if(cameraPermissionState.allPermissionsGranted){ + cameraLauncher.launch(uri) + }else{ + if (cameraPermissionState.shouldShowRationale) { + permissionDialog.value = Pair(cameraPermissionText, true) to cameraPermissionState + } else { + permissionDialog.value = Pair(cameraPermissionText, true) to cameraPermissionState + } + } + } + Gallery -> { + if(storagePermissionState.allPermissionsGranted){ + getImageFromGallery.launch("image/*") + }else{ + if (storagePermissionState.shouldShowRationale) { + permissionDialog.value = Pair(storagePermissionText, true) to storagePermissionState + } else { + permissionDialog.value = Pair(storagePermissionText, true) to storagePermissionState + } + } + } + } + } + } + item { + Spacer(modifier = Modifier.height(10.dp)) + } + item { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box(modifier = Modifier.weight(0.45f)) { + PrimaryLabelTextField( + label = stringResource(id = string.first_name), + value = profileDataState.firstName, + onValueChange = { + onEvent(ProfileScreenEvent.EnteredFirstName(it)) + } + ) + } + Box(modifier = Modifier.weight(0.45f)) { + PrimaryLabelTextField( + label = stringResource(id = string.last_name), + value = profileDataState.lastName, + onValueChange = { + onEvent(ProfileScreenEvent.EnteredLastName(it)) + } + ) + } + } + } + item { + PrimaryButton(text = stringResource(id = string.update_profile)) { + onEvent(ProfileScreenEvent.UpdateProfileClick) + } + } + item { + PrimaryPasswordField( + label = stringResource(id = string.new_password), + password = profileDataState.newPassword, + onPasswordChange = { + onEvent(ProfileScreenEvent.EnteredPassword(it)) + } + ) + } + item { + PrimaryPasswordField( + label = stringResource(id = string.confirm_new_password), + password = profileDataState.confirmPassword, + onPasswordChange = { + onEvent(ProfileScreenEvent.EnteredConfirmPassword(it)) + } + ) + } + item { + PrimaryButton(text = stringResource(id = string.change_password)) { + onEvent(ProfileScreenEvent.ChangePasswordClick) + } + } + item { + IconTextSwitchButton( + text = stringResource(id = string.toggle_two_factor_authentication), + iconStart = drawable.ic_two_factor_auth, + checked = twoFactorAuth.value, + onCheckedChange = { + twoFactorAuth.value = it + showTwoFactorDialog.value = true + } + ) + } + if(context.canAuthenticateBiometric()){ + item { + IconTextSwitchButton( + text = stringResource(id = string.toggle_biometric_authentication), + iconStart = drawable.ic_fingerprint_24, + checked = bioMetricState.value, + onCheckedChange = { + bioMetricState.value = it + coroutineScope.launch { + prefs.enableBiometric(it) + } + } + ) + } + } + } + + if (permissionDialog.value.first.second) { + PermissionsDialog( + title = "Permission !", + description = permissionDialog.value.first.first, + onDialogConfirm = { + permissionDialog.value.second.launchMultiplePermissionRequest() + permissionDialog.value = Pair("",false) to storagePermissionState + }, + onDialogDismiss = { + permissionDialog.value = Pair("",false) to storagePermissionState + } + ) + } + + ConfirmationDialog( + showDialog = showTwoFactorDialog.value, + enableTwoFactor = twoFactorAuth.value, + onConfirm = { + showTwoFactorDialog.value = false + onEvent(ProfileScreenEvent.ToggleTwoFactor(twoFactorAuth.value)) + }) { + showTwoFactorDialog.value = false + twoFactorAuth.value = !twoFactorAuth.value + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} + +fun Context.createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val image = File.createTempFile( + imageFileName, + ".jpg", + externalCacheDir + ) + return image +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt new file mode 100644 index 00000000..9b3e44d6 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import java.io.File + +data class ProfileScreenDataState( + val firstName:String = "", + val lastName:String = "", + val newPassword:String = "", + val confirmPassword:String = "", + val avatar:File? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt new file mode 100644 index 00000000..98dcf841 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt @@ -0,0 +1,16 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import android.net.Uri + +sealed class ProfileScreenEvent{ + data class EnteredFirstName(val firstName:String): ProfileScreenEvent() + data class EnteredLastName(val lastName:String): ProfileScreenEvent() + data class EnteredPassword(val password:String): ProfileScreenEvent() + data class EnteredConfirmPassword(val confirmPassword:String): ProfileScreenEvent() + object UpdateProfileClick: ProfileScreenEvent() + object ChangePasswordClick: ProfileScreenEvent() + data class ToggleTwoFactor(val checked: Boolean): ProfileScreenEvent() + data class UpdateImage(val uri: Uri,): ProfileScreenEvent() + object DeletePic: ProfileScreenEvent() + +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt new file mode 100644 index 00000000..a4666ba2 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt @@ -0,0 +1,52 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ConfirmationDialog( + showDialog: Boolean, + enableTwoFactor:Boolean, + onConfirm: () -> Unit, + onCancel: () -> Unit +) { + if (showDialog) { + AlertDialog( + onDismissRequest = onCancel, + title = { + Text("Please Confirm") + }, + text = { + if(enableTwoFactor){ + Text("Are you sure you want to enable two-factor authentication?") + }else{ + Text("Are you sure you want to disable two factor authentication?") + } + }, + confirmButton = { + Button( + onClick = { + onConfirm() + } + ) { + Text("OK") + } + }, + dismissButton = { + Button( + onClick = { + onCancel() + } + ) { + Text("Cancel") + } + }, + modifier = Modifier.padding(16.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt new file mode 100644 index 00000000..c0cc8a57 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt @@ -0,0 +1,57 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +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.painterResource +import androidx.compose.ui.unit.dp + +@Composable +fun IconTextSwitchButton( + text: String, + iconStart: Int, + checked:Boolean, + onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box( + modifier = Modifier + .size(44.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(10.dp) + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconStart), + contentDescription = "", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = text, + style = MaterialTheme.typography.titleMedium + ) + } + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt new file mode 100644 index 00000000..aa8e168b --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt @@ -0,0 +1,150 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.PhotoCamera +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest.Builder +import kotlinx.coroutines.Dispatchers +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.UserModel +import java.io.File + +enum class ProfileButtonOptions { + Delete, Camera, Gallery +} + +@Composable +fun ProfileItem(userModel: UserModel?, avatar: File?, changeAvatarClick: (ProfileButtonOptions) -> Unit) { + + val context = LocalContext.current + + val dropDownMenuState = remember { mutableStateOf(false) } + val menuItems = listOf( + stringResource(id = string.delete) to painterResource(id = drawable.ic_delete_24) to ProfileButtonOptions.Delete, + stringResource(id = string.camera) to painterResource(id = drawable.ic_camera) to ProfileButtonOptions.Camera, + stringResource(id = string.gallery) to painterResource(id = drawable.ic_gallery) to ProfileButtonOptions.Gallery + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.size(140.dp) + ) { + Card( + modifier = Modifier + .size(120.dp) + .align(Alignment.TopCenter), + ) { + if (userModel != null) { + AsyncImage( + model = Builder(LocalContext.current) + .data(avatar?: drawable.ic_user_new) + .dispatcher(Dispatchers.IO) + .error(drawable.ic_user_new) + .crossfade(true) + .build(), + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = "" + ) + } else { + CircularProgressIndicator() + } + } + Box( + modifier = Modifier + .size(60.dp) + .background( + color = MaterialTheme.colorScheme.background, + shape = CircleShape, + ) + .align(Alignment.BottomEnd) + ) { + Box( + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + ) + .align(Alignment.Center) + .clickable { dropDownMenuState.value = true }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Rounded.PhotoCamera, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + DropdownMenu( + modifier = Modifier.align(Alignment.BottomEnd), + expanded = dropDownMenuState.value, + onDismissRequest = { dropDownMenuState.value = false } + ) { + menuItems.forEach { item -> + DropdownMenuItem( + text = { + Text(text = item.first.first) + }, + leadingIcon = { + Icon( + painter = item.first.second, + contentDescription = "" + ) + }, + onClick = { + changeAvatarClick(item.second) + dropDownMenuState.value = false + } + ) + } + } + } + } + } + userModel?.let { + Text( + text = (userModel.username ?: "") , + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = userModel.email?: "", + style = MaterialTheme.typography.titleMedium + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt new file mode 100644 index 00000000..a80603f9 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt @@ -0,0 +1,219 @@ +package org.aossie.agoraandroid.ui.screens.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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import coil.compose.AsyncImage +import coil.request.ImageRequest +import kotlinx.coroutines.Dispatchers +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.domain.model.UserModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.settings.component.LanguageUpdateDialog +import java.io.File + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingScreen( + userModel: UserModel, + avatar: File?, + appLangState: String, + supportedLang: List>, + screenState: ScreensState, + screenEvent: (SettingsScreenEvent) -> Unit +) { + + val languageDialogState = remember { mutableStateOf(false) } + val selectedLang = supportedLang.find { + it.second == appLangState + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 25.dp), + ) { + item { + ProfileItem(userModel,avatar) + } + item { + Spacer(modifier = Modifier.height(15.dp)) + Divider( + modifier = Modifier.padding(horizontal = 25.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + ) + Spacer(modifier = Modifier.height(15.dp)) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.language), + arrowText = selectedLang?.first ?: "English", + iconStart = R.drawable.ic_translate, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + languageDialogState.value = true + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.account_settings), + iconStart = R.drawable.ic_user_new, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnAccountSettingClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.about_us), + iconStart = R.drawable.ic_info, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnAboutUsClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.share_with_others), + iconStart = R.drawable.ic_share, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnShareWithOthersClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.contact_us), + iconStart = R.drawable.ic_contact_us, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnContactUsClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.logout), + iconStart = R.drawable.ic_logout, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnLogoutClick) + } + ) + } + } + + if(languageDialogState.value){ + LanguageUpdateDialog( + languages = supportedLang, + onDismissRequest = { + languageDialogState.value = false + }, + onConfirmRequest = { + screenEvent(SettingsScreenEvent.ChangeAppLanguage(it)) + }, + selectedLang = selectedLang!!, + ) + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} + +@Composable +fun ProfileItem(userModel: UserModel, avatar: File?) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 25.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Card( + modifier = Modifier.size(100.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 10.dp + ) + ) { + AsyncImage( + model = ImageRequest + .Builder(LocalContext.current) + .data(avatar?.toUri() ?: userModel.avatarURL) + .dispatcher(Dispatchers.IO) + .error(R.drawable.ic_user) + .crossfade(true) + .build(), + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = "") + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = (userModel.firstName ?: "") + " " + (userModel.lastName ?: ""), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = userModel.email?: "", + style = MaterialTheme.typography.titleMedium + ) + } + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt new file mode 100644 index 00000000..48643f83 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt @@ -0,0 +1,10 @@ +package org.aossie.agoraandroid.ui.screens.settings + +sealed class SettingsScreenEvent{ + data class ChangeAppLanguage(val language: Pair):SettingsScreenEvent() + object OnAccountSettingClick:SettingsScreenEvent() + object OnAboutUsClick:SettingsScreenEvent() + object OnShareWithOthersClick:SettingsScreenEvent() + object OnContactUsClick:SettingsScreenEvent() + object OnLogoutClick:SettingsScreenEvent() +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt new file mode 100644 index 00000000..d237cd06 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt @@ -0,0 +1,38 @@ +package org.aossie.agoraandroid.ui.screens.settings.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun LanguageRadioButtonItem( + selected: Pair, + onCheckedChange: (Pair) -> Unit, + language: Pair +){ + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Checkbox(checked = selected.second==language.second, onCheckedChange = { + if(it){ + onCheckedChange(language) + }else{ + onCheckedChange(language) + } + }) + Text(text = language.first, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt new file mode 100644 index 00000000..bf2f590e --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt @@ -0,0 +1,91 @@ +package org.aossie.agoraandroid.ui.screens.settings.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.aossie.agoraandroid.R + +@Composable +fun LanguageUpdateDialog( + languages: List>, + onDismissRequest:() -> Unit, + onConfirmRequest:(Pair) -> Unit, + selectedLang: Pair, +) { + val selected = remember { + mutableStateOf(selectedLang) + } + Dialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(15.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.language), + style = MaterialTheme.typography.headlineMedium + ) + LazyColumn( + modifier = Modifier + .heightIn(max = 400.dp) + .fillMaxWidth() + ) { + itemsIndexed(languages) { index, item -> + LanguageRadioButtonItem( + language = item, + selected = selected.value, + onCheckedChange = { + selected.value = it + } + ) + } + } + Row(modifier = Modifier.align(Alignment.End)) { + TextButton( + onClick = onDismissRequest, + ) { + Text("Cancel") + } + TextButton( + onClick = { + if(selected.value != selectedLang){ + onConfirmRequest(selected.value!!) + }else{ + onDismissRequest.invoke() + } + }, + ) { + Text("Update") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt index 6ef77a7e..03739b6b 100644 --- a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt @@ -38,6 +38,7 @@ object AppConstants { const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7Padding" const val SALT = "QWlGNHNhMTJTQWZ2bGhpV3U=" const val IV = "bVQzNFNhRkQ1Njc4UUFaWA==" + const val DEFAULT_LANG = "en" enum class Status { PENDING, ACTIVE, FINISHED } diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt new file mode 100644 index 00000000..bc35d284 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.utilities + +object LocaleUtil { + + fun getSupportedLanguages(): List> { + return listOf( + Pair("English","en"), + Pair("Hindi","hi"), + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_art.xml b/app/src/main/res/drawable/ic_art.xml deleted file mode 100644 index 002b5c7d..00000000 --- a/app/src/main/res/drawable/ic_art.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml index bc65ddfb..dff8eb4b 100644 --- a/app/src/main/res/drawable/ic_camera.xml +++ b/app/src/main/res/drawable/ic_camera.xml @@ -1,15 +1,10 @@ + android:width="30dp" + android:height="30dp" + android:viewportWidth="18" + android:viewportHeight="18"> - - + android:pathData="M7.333,15.75H10.667C13.007,15.75 14.178,15.75 15.019,15.199C15.382,14.961 15.694,14.654 15.938,14.296C16.5,13.471 16.5,12.321 16.5,10.023C16.5,7.724 16.5,6.575 15.938,5.75C15.694,5.392 15.382,5.085 15.019,4.847C14.479,4.492 13.802,4.366 12.767,4.321C12.272,4.321 11.847,3.953 11.75,3.477C11.676,3.128 11.484,2.816 11.206,2.592C10.929,2.368 10.582,2.248 10.226,2.25H7.774C7.034,2.25 6.395,2.764 6.25,3.477C6.153,3.953 5.728,4.321 5.234,4.321C4.199,4.366 3.522,4.493 2.981,4.847C2.619,5.085 2.307,5.392 2.063,5.75C1.5,6.575 1.5,7.724 1.5,10.023C1.5,12.321 1.5,13.47 2.062,14.296C2.305,14.653 2.617,14.96 2.981,15.199C3.822,15.75 4.993,15.75 7.333,15.75ZM9,6.955C7.274,6.955 5.875,8.328 5.875,10.022C5.875,11.717 7.274,13.09 9,13.09C10.726,13.09 12.125,11.717 12.125,10.023C12.125,8.328 10.726,6.955 9,6.955ZM9,8.182C7.965,8.182 7.125,9.006 7.125,10.023C7.125,11.039 7.965,11.863 9,11.863C10.035,11.863 10.875,11.039 10.875,10.023C10.875,9.006 10.035,8.182 9,8.182ZM12.542,7.568C12.542,7.229 12.821,6.955 13.167,6.955H14C14.344,6.955 14.625,7.229 14.625,7.568C14.623,7.732 14.557,7.889 14.44,8.004C14.322,8.119 14.164,8.183 14,8.182H13.167C13.086,8.183 13.005,8.167 12.929,8.137C12.854,8.106 12.785,8.061 12.727,8.005C12.669,7.948 12.623,7.88 12.591,7.805C12.559,7.73 12.542,7.65 12.542,7.568Z" + android:fillColor="#000000" + android:fillType="evenOdd"/> diff --git a/app/src/main/res/drawable/ic_contact_us.xml b/app/src/main/res/drawable/ic_contact_us.xml index 821bb788..8877941b 100644 --- a/app/src/main/res/drawable/ic_contact_us.xml +++ b/app/src/main/res/drawable/ic_contact_us.xml @@ -1,390 +1,9 @@ + android:width="25dp" + android:height="24dp" + android:viewportWidth="25" + android:viewportHeight="24"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:pathData="M8.42,7.54C7.62,7.2 7.28,6.21 7.76,5.49C8.73,4.05 10.35,3 12.49,3C14.84,3 16.45,4.07 17.27,5.41C17.97,6.56 18.38,8.71 17.3,10.31C16.1,12.08 14.95,12.62 14.33,13.76C14.18,14.03 14.09,14.25 14.03,14.7C13.94,15.43 13.34,16 12.6,16C11.73,16 11.02,15.25 11.12,14.38C11.18,13.87 11.3,13.34 11.58,12.84C12.35,11.45 13.83,10.63 14.69,9.4C15.6,8.11 15.09,5.7 12.51,5.7C11.34,5.7 10.58,6.31 10.11,7.04C9.76,7.61 9.03,7.79 8.42,7.54ZM14.5,20C14.5,21.1 13.6,22 12.5,22C11.4,22 10.5,21.1 10.5,20C10.5,18.9 11.4,18 12.5,18C13.6,18 14.5,18.9 14.5,20Z" + android:fillColor="#40484C"/> diff --git a/app/src/main/res/drawable/ic_fingerprint_24.xml b/app/src/main/res/drawable/ic_fingerprint_24.xml new file mode 100644 index 00000000..c2c24155 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml new file mode 100644 index 00000000..bb083559 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..3ff53120 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 00000000..f368b13d --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index ae0eabbd..7a62e69e 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,580 +1,13 @@ + android:width="25dp" + android:height="24dp" + android:viewportWidth="25" + android:viewportHeight="24"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml new file mode 100644 index 00000000..8fb6e968 --- /dev/null +++ b/app/src/main/res/drawable/ic_translate.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_two_factor_auth.xml b/app/src/main/res/drawable/ic_two_factor_auth.xml new file mode 100644 index 00000000..c6f99820 --- /dev/null +++ b/app/src/main/res/drawable/ic_two_factor_auth.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_new.xml b/app/src/main/res/drawable/ic_user_new.xml new file mode 100644 index 00000000..47e44bf4 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_new.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/dialog_change_avatar.xml b/app/src/main/res/layout/dialog_change_avatar.xml deleted file mode 100644 index 43d75e18..00000000 --- a/app/src/main/res/layout/dialog_change_avatar.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml deleted file mode 100644 index 6c450ff8..00000000 --- a/app/src/main/res/layout/fragment_profile.xml +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index 168d30b2..00000000 --- a/app/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 541e4357..eee6bd8e 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -133,8 +133,7 @@ + android:label="Settings"> + अब वोट करें + फेसबुक के साथ जारी रखें + लॉग इन करें + साइन अप करें + अब जल्द ही + उपयोगकर्ता नाम + पासवर्ड + लॉग इन करें + उपयोगकर्ता लॉगिन + उपयोगकर्ता साइन अप करें + पहला नाम + उपनाम + मेल पता + क्या आपके पास पहले से एक खाता मौजूद है \? + दाखिल करना + पासवर्ड भूल गए \? + प्रोफ़ाइल + डैशबोर्ड + बातचीत करना + एक बग रिपोर्ट करो + दूसरों के साथ साझा करें + अगोरा के बारे में + मेरे निमंत्रण + संपर्क करें + एगोरा वोट एक मतदान मंच है जहां उपयोगकर्ता चुनाव बना सकते हैं और दोस्तों को वोट डालने के लिए आमंत्रित कर सकते हैं। + यह वोटिंग एल्गोरिदम की एक विस्तृत श्रृंखला का समर्थन करता है, जिनमें से कुछ प्रमुख हैं, जैसे कि बहुमत, समतावादी, ऑस्ट्रेलियाई एसटीवी। + हमारे Gitter चैनलों पर बेझिझक हमसे संपर्क करें और Gitlab पर हमारे साथ काम करें + AOSSIE की ग्रिड + AOSSIE का GITLAB + बेझिझक कोई मुद्दा उठाएं ताकि हमारी टीम उसमें यथाशीघ्र सुधार कर सके + पासवर्ड बदलें + नया पासवर्ड + नए पासवर्ड की पुष्टि करें + चुनाव बनाएं + सक्रिय\nचुनाव + लंबित \nचुनाव + कुल\nचुनाव + ख़त्म\nचुनाव + मतदाताओं को आमंत्रित करें\? + वास्तविक समय परिणाम प्राप्त करें\? + मतपत्र कितने गुप्त होते हैं\? + मतदाताओं की सूची कौन देख सकता है\? + केवल मैं + चुनाव तक पहुंच रखने वाला हर कोई + कुछ और विकल्प + अगला + उम्मीदवार जोड़ें + उम्मीदवार का नाम + उम्मीदवार का नाम दर्ज करें + चुनाव विवरण + चुनाव का नाम + चुनाव विधि + आरंभ करने की तिथि + अंतिम तिथि + खजूर बीनने वाला + वोटिंग एल्गोरिथम चुनें + बाल्डविन + संपूर्ण मतपत्र + लॉग आउट + testemail + 0 + उपयोगकर्ता ईमेल आईडी + लिंक भेजें + सत्यापित करना + AOSSIE द्वारा ❤ के साथ विकसित किया गया + पासवर्ड रीसेट लिंक प्राप्त करने के लिए कृपया उपयोगकर्ता नाम दर्ज करें + मरीज़ की देखभाल करना, मरीज़ की देखभाल करना ज़रूरी है, लेकिन यह ऐसे समय में होगा जब बहुत काम और दर्द होगा। छोटी से छोटी बात पर आने के लिए कोई भी किसी भी तरह का काम नहीं कर सकता + उम्मीदवार + उम्मीदवार का नाम + शुरू करना: + शुरू करने की तिथि - शुरू होने की तिथि - रवाना होने की तिथि + दर्जा: + चुनाव की स्थिति + अंत: + समाप्त होने की तारीख + छवि बटन + विवरण + सक्रिय चुनाव + चुनाव ख़त्म + लंबित चुनाव + कुल चुनाव + ओपन सोर्स को और अधिक खूबसूरत जगह बनाने के लिए दूसरों के साथ साझा करें + चुनाव हटाएं + मतदाताओं को आमंत्रित करें + मतदाता + मतदान + परिणाम + मतपत्र + वोट मतपत्र + मतदाता का नाम + मतदाता ईमेल + मतदाता का ईमेल + मतदाता का नाम + मतदाता जोड़ें + वोटर को हटाने के लिए स्वाइप करें + उम्मीदवार को हटाने के लिए स्वाइप करें + मतदाताओं का विवरण दर्ज करें + विजेता का नाम + कृपया अपना उत्तर यहां लिखें + सुरक्षा प्रश्न + स्वागत है, %1$s! + लिंक भेजा गया, कृपया अपने ईमेल जांचें + अमान्य उपयोगकर्ता नाम + कुछ गलत हो गया। कृपया बाद में पुन: प्रयास करें + कनेक्शन सफलतापूर्वक स्थापित हो गया + पूरी तरह से गुप्त और कभी किसी को नहीं दिखाया गया + केवल मुझे दिखाई देता है + चुनाव तक पहुंच रखने वाले सभी लोगों के लिए दृश्यमान + ओकलाहोमा + रेंजवोटिंग + क्रमबद्ध जोड़े + संतुष्टि अनुमोदन मतदान + स्मिथसेट + क्रमशः संप्रोद्ध स्वीकृति वोटिंग + स्वीकृति + छाँट के साथ विस्तारित चुनाव + कोपलैंड + अनआवृत सेट + बोर्डा + मिनिमैक्स कोंडोर्सेट + नैंसन + केमेनी-यंग + यादृच्छिक चुनाव + आकस्मिक + तत्काल चुनाव 2 राउंड + यहाँ दिखाने के लिए कुछ नहीं है + + + //string-array for security questions + + आपकी मां का पहला नाम क्या है? + आपके पहले पालतू जानवर का नाम क्या है? + आपका उपनाम क्या है? + आपने किस प्राथमिक स्कूल में अध्ययन किया है? + आपकी गृहनगरी कहाँ है? + + + + पूरी तरह से गुप्त और कभी किसी को नहीं दिखाया गया + केवल मुझे दिखाई देता है + चुनाव तक पहुंच रखने वाले सभी लोगों के लिए दृश्यमान + + + + Oklahoma + RangeVoting + RankedPairs + Satisfaction Approval Voting + SmithSet + Sequential Proportional Approval Voting + Approval + Exhaustive ballot with dropoff + Copeland + UncoveredSet + Borda + MinimaxCondorcet + Nanson + Kemeny-Young + RandomBallot + Contingent + InstantRunoff2Round + + + //profile fragment labels + + ईमेल : + उपयोगकर्ता नाम : + पहला नाम : + अंतिम नाम : + प्रोफ़ाइल अपडेट करें + पासवर्ड खाली नहीं हो सकता + पासवर्ड मेल नहीं खा रहे हैं + पासवर्ड सफलतापूर्वक बदला गया + आपका सत्र समाप्त हो गया था। कृपया फिर से लॉगिन करें! + + एक ही ईमेल आईडी वाले मतदाता को पहले से ही जोड़ दिया गया है + उपयोगकर्ता सफलतापूर्वक अपडेट किया गया! + पासवर्ड सफलतापूर्वक अपडेट किया गया! + प्रोफ़ाइल फ़ोटो सफलतापूर्वक अपडेट की गई! + दोहरी प्रमाणीकरण अपडेट किया गया, कृपया फिर से लॉगिन करें! + पहला नाम खाली नहीं हो सकता + अंतिम नाम खाली नहीं हो सकता + ईमेल बदला नहीं जा सकता + उपयोगकर्ता नाम बदला नहीं जा सकता + + + हैलो खाली फ्रेगमेंट + शीर्षक + अधिक + चुनाव + + अवैध अनुरोध + अप्रमाणित + अवैध क्रेडेंशियल्स + सक्रिय + शुरू होगा : + शुक्रवार जनवरी 24 12:00:04 + समाप्त होगा : + लोरेम इप्सम डोलर सिट अमेट, कॉन्सेक्टेटर एडिपिसिंग एलिट उत अलिक्वाम + इस चुनाव के लिए कोई मतदाता नहीं है + खाली बैलट + खाली चुनाव + शुरू करें + एगोरा + बस वही रहने वाले मत बनिए,\nमौजूदा रहिए + चुनावों को अनुसूचित करें\nऔर मतदाताओं को आमंत्रित करें + मतदान करें और\nउम्मीदवार का चयन करें + परिणामों को दर्शाना और\nघोषित करना + गोपनीय सुरक्षा प्रश्न + स्वागत है, + ——————— या ——————— + मतदाता सूची दृश्यता + रीयल टाइम + आमंत्रित करें + जोड़ें + agora@example.com + जॉन डो + हमारे बारे में + खाता सेटिंग्स + सेटिंग्स + + \u0020 + चलिए बॉल चलाएं + हमारे Gitter चैनल पर हमसे संपर्क करें और हमारे साथ Gitlab पर काम करें + Gitlab + Gitter + होम + शेयर करें + गोपनीयता नीति + नियम और शर्तें + कैलेंडर के लिए ड्रॉप डाउन + जुलाई + सूची + दोहरी प्रमाणीकरण + कृपया अपने पंजीकृत ईमेल पते पर भेजा गया एक टाइम पासवर्ड दर्ज करें + OTP फिर से भेजें + एक समय पासवर्ड + क्या आप इस डिवाइस पर भरोसा करते हैं? + दोहरी प्रमाणीकरण टॉगल करें + बायोमेट्रिक प्रमाणीकरण सक्षम करें + [{ "include": "https://agora-frontend.herokuapp.com/.well-known/assetlinks.json" }] + उम्मीदवार + कृपया उम्मीदवार का चयन करें + मत दें + चयनित उम्मीदवार + कैमरा + कैमरा विकल्प + गैलरी विकल्प + गैलरी + ऐसा लगता है कि आपके पास कोई ब्राउज़र स्थापित नहीं है। + मतदाता हटा दिया गया + मत सफलतापूर्वक दिया गया! + यहां कुछ दिखाने के लिए कुछ नहीं है + परिणाम प्राप्त करने में असमर्थ + कृपया अपनी नेटवर्क कनेक्शन की जांच करें + अन्य + चुनाव अभी शुरू नहीं हुआ है + चुनाव समाप्त हो गया है + सक्रिय चुनावों को हटाया नहीं जा सकता + चुनाव परिणाम प्रतिशत में + चुनाव परिणाम में वोटों की संख्या + विजेता है + लॉगिन रद्द किया गया + OTP आपके पंजीकृत ईमेल पते पर भेजा गया है + आयात करें + निर्यात करें + फ़ाइल लिखने में त्रुटि + फ़ाइल नहीं मिली! + फ़ाइल उपलब्ध नहीं है! + फ़ाइल पढ़ने में त्रुटि + अनुमति अस्वीकृत + कोई शीट नहीं मिली + एक्सेल फ़ाइल नहीं + मान्य नाम और ईमेल पता दर्ज करें + कृपया मतदाता का ईमेल दर्ज करें + कृपया मान्य ईमेल पता दर्ज करें + कृपया मतदाता का नाम दर्ज करें + खोजें... + अमान्य URL + कृपया पहले एक उम्मीदवार का चयन करें + मत पुष्टि करें? + क्या आप वाकई इस चुनाव के लिए मतदान करना चाहते हैं? + पुष्टि करें + रद्द करें + एक सक्रियण लिंक पंजीकृत ईमेल आईडी पर भेजा गया है आपके खाते की पुष्टि करने के लिए + परिणाम साझा करें + वोट + एक सक्रियण लिंक पंजीकृत ईमेल आईडी पर भेजा गया है आपके खाते की पुष्टि करने के लिए + कृपया OTP दर्ज करें + कृपया आगे बढ़ने के लिए चेकबॉक्स पर टैप करें + कृपया कम से कम एक उम्मीदवार जोड़ें + कृपया सभी विवरण दर्ज करें + "समाप्ति तिथि आरंभ तिथि और समय के बाद होनी चाहिए" + आरंभ तिथि मौजूदा तिथि और समय के बाद होनी चाहिए + छवि लोड करते समय त्रुटि + मतदान आमंत्रण! + "आपको एक चुनाव के लिए मतदान के लिए आमंत्रित किया गया है" + चुनाव दृश्यता + मतदाता सूची दृश्यता + चुनाव बनाएं और मतदान के लिए उपयोगकर्ता को आमंत्रित करें + अपने चुनाव के लिए एक नाम दर्ज करें + अपने चुनाव के लिए एक विधि चुनें + अपने चुनाव के लिए एक विवरण दर्ज करें + अपने चुनाव के लिए सभी उम्मीदवार दर्ज करें + उम्मीदवारों का आयात करें + उम्मीदवारों का आयात करें\nएक्सेल शीट से + चुनाव को कौन देख सकता है चुनें + मतदाता सूची को सार्वजनिक बनाने के लिए चुनें + अपने चुनाव के लिए प्रारंभ तिथि चुनें + अपने चुनाव के लिए समाप्ति तिथि चुनें + रीयल टाइम + आमंत्रित करें + यह चुनाव की लाइव\nस्थिति दिखाता है + हटाएं + सक्रिय होने पर चुनाव को हटाया नहीं जा सकता + चुनाव समाप्त होने पर मतदाताओं को आमंत्रित नहीं किया जा सकता + चुनाव के लिए मतदाताओं को देखें + चुनाव के लिए मतपत्र देखें + चुनाव का परिणाम देखें + छोड़ें + कुछ गड़बड़ हो गई है। कृपया पुनः प्रयास करें। + प्रमाणीकरण विफल हुआ। + जीव-रहित प्रमाणीकरण + उपयोगकर्ता को ऐप का उपयोग करने से पहले प्रमाणित किया जाना चाहिए + बार चार्ट + पाई चार्ट + प्रारंभ समय वर्तमान समय से छोटा है + भाषा + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad3ba28f..2018b91a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,5 +336,8 @@ Facebook Login... Login error View details + Language + Storage permission required for this feature to be available. Please grant the permission. + Camera permission required for this feature to be available. Please grant the permission. diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index efdd4beb..62748bb2 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2c17aecf..25be4524 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ buildscript { accompanist_version = '0.31.2-alpha' coilVersion = '2.4.0' composeLiveData = '1.4.3' + languageLibrary = '1.3.0' } repositories {