diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02dbaf48..44404ddc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,30 +16,21 @@ before_script: - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 - apt-get --quiet install curl --yes - export ANDROID_HOME="${PWD}/android-home" - # Create a new directory at specified location - install -d $ANDROID_HOME - # Here we are installing androidSDK tools from official source, - # (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well) - # after that unzipping those tools and - # then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build - wget --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip - # move to the archive at ANDROID_HOME - pushd $ANDROID_HOME - unzip -d cmdline-tools cmdline-tools.zip - pushd cmdline-tools - # since commandline tools version 7583922 the root folder is named "cmdline-tools" so we rename it if necessary - mv cmdline-tools tools || true - popd - popd - export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/tools/bin/ - # Nothing fancy here, just checking sdkManager version - sdkmanager --version - # use yes to accept all licenses - yes | sdkmanager --licenses || true - sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" - sdkmanager "platform-tools" - sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" - - touch local.properties && echo -e "fbApiKey=$FB_API_KEY\nfbApiScheme=$FB_API_SCHEME\nsecretKey=$SECRET_KEY\nserverKey=$SERVER_KEY" > local.properties + - touch local.properties && echo -e "fbApiKey=$FB_API_KEY\nfbClientSecret=$FB_CLIENT_SECRET\nfbApiScheme=$FB_API_SCHEME\nsecretKey=$SECRET_KEY\nserverKey=$SERVER_KEY" > local.properties - touch app/google-services.json && echo $FIREBASE_JSON > app/google-services.json - touch signing.properties && echo -e "storeFile=keystore.jks\nstorePassword=$KEYSTORE_PASSWORD\nkeyAlias=$KEY_ALIAS\nkeyPassword=$KEY_PASSWORD" > signing.properties - touch app/keystore.jks && echo $KEYSTORE | base64 --decode > app/keystore.jks @@ -49,7 +40,6 @@ before_script: - apt-get --quiet install ruby ruby-dev build-essential dh-autoreconf --yes - gem install bundler:1.17.2 - bundle install - # temporarily disable checking for EPIPE error and use yes to accept all licenses - set +o pipefail - set -o pipefail diff --git a/app/build.gradle b/app/build.gradle index e25d2007..377a4c5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def fbApiScheme = properties.getProperty('fbApiScheme') def fbApiKey = properties.getProperty('fbApiKey') +def fbClientSecret = properties.getProperty('fbClientSecret') def secretKey = properties.getProperty('secretKey') def serverKey = properties.getProperty('serverKey') @@ -30,6 +31,7 @@ android { compileSdkVersion rootProject.ext.compileSdk defaultConfig { resValue("string", "fbApiKey", fbApiKey) + resValue("string", "fbClientSecret", fbClientSecret) resValue("string", "fbApiScheme", fbApiScheme) resValue("string", "secretKey", secretKey) resValue("string", "serverKey", serverKey) @@ -104,7 +106,6 @@ dependencies { testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test:runner:$testRunnerVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion" - // Lifecycle components implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntime" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensions" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" @@ -121,7 +122,6 @@ dependencies { //Shimmer implementation "com.facebook.shimmer:shimmer:$facebookShimmerVersion" //Facebook Login - implementation "com.facebook.android:facebook-login:$facebookLoginVersion" implementation "com.facebook.android:facebook-android-sdk:$facebookAndroidSDK" //Pie Chart implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" @@ -208,10 +208,17 @@ 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") //extended Material Icons implementation "androidx.compose.material:material-icons-extended:1.4.3" + + //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 a8db1c5f..a11998c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,14 +2,11 @@ - - - + + - - @@ -80,6 +75,9 @@ + diff --git a/app/src/main/java/org/aossie/agoraandroid/AgoraApp.kt b/app/src/main/java/org/aossie/agoraandroid/AgoraApp.kt index 57b259f8..3f3a4b5b 100644 --- a/app/src/main/java/org/aossie/agoraandroid/AgoraApp.kt +++ b/app/src/main/java/org/aossie/agoraandroid/AgoraApp.kt @@ -2,8 +2,10 @@ package org.aossie.agoraandroid import android.app.Application import android.util.Log +import com.yariksoffice.lingver.Lingver import org.aossie.agoraandroid.ui.di.AppComponent import org.aossie.agoraandroid.ui.di.DaggerAppComponent +import org.aossie.agoraandroid.utilities.AppConstants import timber.log.Timber import timber.log.Timber.DebugTree import timber.log.Timber.Tree @@ -33,6 +35,7 @@ class AgoraApp : Application() { override fun onCreate() { super.onCreate() Timber.plant(if (BuildConfig.DEBUG) DebugTree() else CrashReportingTree()) + Lingver.init(this,AppConstants.DEFAULT_LANG) } private class CrashReportingTree : Tree() { diff --git a/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt b/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt index 662e9bbb..2794df7c 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt @@ -16,6 +16,7 @@ import org.aossie.agoraandroid.data.network.dto.ElectionDto import org.aossie.agoraandroid.data.network.dto.VotersDto import org.aossie.agoraandroid.data.network.dto.WinnerDto import org.aossie.agoraandroid.data.network.responses.Ballots +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.data.network.responses.ElectionResponse import org.aossie.agoraandroid.domain.repository.ElectionsRepository import org.aossie.agoraandroid.utilities.ApiException @@ -107,7 +108,7 @@ constructor( db.getElectionDao() .getPendingElections(currentDate) - override suspend fun fetchElections() { + override suspend fun fetchElections(): ElectionListResponse { val isNeeded = prefs.getUpdateNeeded().first() if (isNeeded) { try { @@ -117,12 +118,14 @@ constructor( elections.postValue(it) } Timber.d(response.toString()) + return response } catch (e: NoInternetException) { } catch (e: ApiException) { } catch (e: SessionExpirationException) { } catch (e: IOException) { } } + return ElectionListResponse(elections.value!!) } override fun getFinishedElections(currentDate: String): Flow> = diff --git a/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt b/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt index dfee67b4..aecfab10 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt @@ -79,7 +79,7 @@ class UserRepositoryImpl( .deleteAllElections() } - override suspend fun sendForgotPasswordLink(username: String?): String { + override suspend fun sendForgotPasswordLink(username: String?): List { return apiRequest { api.sendForgotPassword(username) } } diff --git a/app/src/main/java/org/aossie/agoraandroid/data/db/PreferenceProvider.kt b/app/src/main/java/org/aossie/agoraandroid/data/db/PreferenceProvider.kt index f76e08d6..9e974b9f 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/db/PreferenceProvider.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/db/PreferenceProvider.kt @@ -27,6 +27,7 @@ constructor( private val REFRESH_TOKEN = stringPreferencesKey("refreshToken") private val FACEBOOK_ACCESS_TOKEN = stringPreferencesKey("facebookAccessToken") private val ENABLE_BIOMETRIC = booleanPreferencesKey("isBiometricEnabled") + private val APP_LANGUAGE = stringPreferencesKey("appLanguage") } private val userDataStore = context.userDataStore @@ -166,4 +167,21 @@ constructor( it.clear() } } + + suspend fun updateAppLanguage(lang: String) { + userDataStore.edit { + it[APP_LANGUAGE] = lang?.let { _lang -> + 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/data/network/api/Api.kt b/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt index b09313bd..d1b82280 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt @@ -39,7 +39,7 @@ interface Api { suspend fun resendOTP(@Path("userName") userName: String?): Response @POST("auth/forgotPassword/send/{userName}") - suspend fun sendForgotPassword(@Path("userName") userName: String?): Response + suspend fun sendForgotPassword(@Path("userName") userName: String?): Response> @GET("election") suspend fun getAllElections(): Response diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt b/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt index f5dd597c..acee8d7a 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt @@ -7,10 +7,11 @@ import org.aossie.agoraandroid.data.network.dto.ElectionDto import org.aossie.agoraandroid.data.network.dto.VotersDto import org.aossie.agoraandroid.data.network.dto.WinnerDto import org.aossie.agoraandroid.data.network.responses.Ballots +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.data.network.responses.ElectionResponse interface ElectionsRepository { - suspend fun fetchAndSaveElections() + suspend fun fetchAndSaveElections(): ElectionListResponse fun getElections(): Flow> fun getFinishedElectionsCount(currentDate: String): LiveData fun getPendingElectionsCount(currentDate: String): LiveData @@ -18,7 +19,7 @@ interface ElectionsRepository { fun getActiveElectionsCount(currentDate: String): LiveData suspend fun saveElections(elections: List) fun getPendingElections(currentDate: String): Flow> - suspend fun fetchElections() + suspend fun fetchElections(): ElectionListResponse fun getFinishedElections(currentDate: String): Flow> fun getActiveElections(currentDate: String): Flow> fun getElectionById(id: String): Flow diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt b/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt index 8fb9e129..44a8eb33 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt @@ -19,7 +19,7 @@ interface UserRepository { suspend fun logout() fun getUser(): Flow suspend fun deleteUser() - suspend fun sendForgotPasswordLink(username: String?): String + suspend fun sendForgotPasswordLink(username: String?): List suspend fun updateUser(updateUserData: UpdateUserDto): List suspend fun changeAvatar(url: String): List suspend fun changePassword(password: String): List diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt index 251c52cf..090fc7f5 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt @@ -8,7 +8,7 @@ class SendForgotPasswordLinkUseCase @Inject constructor( ) { suspend operator fun invoke( username: String? - ): String { + ): List { return repository.sendForgotPasswordLink(username) } } diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt index a0357200..2d6485b5 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt @@ -1,12 +1,13 @@ package org.aossie.agoraandroid.domain.useCases.homeFragment +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.domain.repository.ElectionsRepository import javax.inject.Inject class FetchAndSaveElectionUseCase @Inject constructor( private val electionsRepository: ElectionsRepository ) { - suspend operator fun invoke() { + suspend operator fun invoke(): ElectionListResponse { return electionsRepository.fetchAndSaveElections() } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt b/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt index 50622c39..9c22d8e1 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt @@ -173,6 +173,10 @@ class MainActivity : AppCompatActivity() { -> { supportActionBar?.hide() } + R.id.twoFactorAuthFragment + -> { + supportActionBar?.hide() + } else -> { window.clearFlags(LayoutParams.FLAG_TRANSLUCENT_STATUS) supportActionBar?.show() 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/about/AboutFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt index 28ab47ea..10876bdf 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt @@ -4,23 +4,34 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import org.aossie.agoraandroid.databinding.FragmentAboutBinding +import org.aossie.agoraandroid.ui.screens.about.AboutScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme /** * A simple [Fragment] subclass. */ class AboutFragment : Fragment() { - private lateinit var binding: FragmentAboutBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - // Inflate the layout for this fragment - binding = FragmentAboutBinding.inflate(layoutInflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + AgoraTheme { + AboutScreen() + } + } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt index cb69727c..fd8f6218 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt @@ -4,19 +4,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R -import org.aossie.agoraandroid.databinding.FragmentForgotPasswordBinding +import androidx.navigation.fragment.findNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreen +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnBackIconClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -31,44 +33,44 @@ constructor( private val forgotPasswordViewModel: ForgotPasswordViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentForgotPasswordBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentForgotPasswordBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - binding.buttonSendLink.setOnClickListener { - val userName = binding.editTextUserName.editText?.text.toString().trim() - if (userName.isEmpty()) { - notify("Please Enter User Name") - } else { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - binding.progressBar.show() - forgotPasswordViewModel.sendResetLink(userName) - } - } + composeView.setContent { - lifecycleScope.launch { - forgotPasswordViewModel.getSendResetLinkStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> binding.progressBar.show() - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(R.string.link_sent_please_check_your_email)) + val userNameState by forgotPasswordViewModel.userNameState.collectAsState() + val progressErrorState by forgotPasswordViewModel.progressAndErrorState.collectAsState() + + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + AgoraTheme { + ForgotPasswordScreen(progressErrorState,userNameState) { event -> + when(event){ + OnBackIconClick -> { + findNavController().navigateUp() } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) + else -> { + forgotPasswordViewModel.onEvent(event) } - else -> {} } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt index 6c046235..c4c5ef25 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt @@ -2,14 +2,20 @@ package org.aossie.agoraandroid.ui.fragments.auth.forgotpassword import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.useCases.authentication.forgotPassword.SendForgotPasswordLinkUseCase +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnSendLinkClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnUserNameEntered +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.SnackBarActionClick +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.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import javax.inject.Inject class ForgotPasswordViewModel @@ -18,26 +24,78 @@ constructor( private val sendForgotPasswordLinkUseCase: SendForgotPasswordLinkUseCase ) : ViewModel() { - private val _getSendResetLinkStateFlow: MutableStateFlow?> = - MutableStateFlow(null) - val getSendResetLinkStateFlow: StateFlow?> = - _getSendResetLinkStateFlow + private val _userNameState = MutableStateFlow ("") + val userNameState = _userNameState.asStateFlow() - fun sendResetLink(userName: String?) = viewModelScope.launch { - _getSendResetLinkStateFlow.value = ResponseUI.loading() + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + + private fun sendResetLink(userName: String?) = viewModelScope.launch { + showLoading("Sending link...") try { sendForgotPasswordLinkUseCase(userName) - _getSendResetLinkStateFlow.value = ResponseUI.success() + hideLoading() + showMessage(R.string.link_sent_please_check_your_email) } catch (e: ApiException) { if (e.message == "412") { - _getSendResetLinkStateFlow.value = ResponseUI.error(AppConstants.INVALID_USERNAME_MESSAGE) + showMessage(AppConstants.INVALID_USERNAME_MESSAGE) } else { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } catch (e: NoInternetException) { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } + + fun onEvent(forgotPasswordScreenEvents: ForgotPasswordScreenEvents){ + when(forgotPasswordScreenEvents){ + OnSendLinkClick -> { + val userName = _userNameState.value.trim() + if (userName.isEmpty()) { + showMessage("Please Enter User Name") + } else { + sendResetLink(userName) + } + } + is OnUserNameEntered -> { + _userNameState.value = forgotPasswordScreenEvents.userName + } + SnackBarActionClick -> { + hideSnackBar() + } + else -> {} + } + } + + 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) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt index ba59d3ed..d28452cb 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt @@ -1,34 +1,41 @@ package org.aossie.agoraandroid.ui.fragments.auth.login -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.CallbackManager -import com.facebook.CallbackManager.Factory import com.facebook.FacebookCallback import com.facebook.FacebookException import com.facebook.login.LoginManager import com.facebook.login.LoginResult +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.aossie.agoraandroid.R import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentLoginBinding import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.disableView -import org.aossie.agoraandroid.utilities.enableView -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toggleIsEnable +import org.aossie.agoraandroid.ui.screens.auth.login.LoginScreen +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.ForgotPasswordClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginFacebookClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.OnTwoFactorAuthentication +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.UserLoggedIn +import org.aossie.agoraandroid.ui.theme.AgoraTheme +import org.aossie.agoraandroid.utilities.navigateSafely import timber.log.Timber import javax.inject.Inject @@ -41,7 +48,7 @@ constructor( private val viewModelFactory: ViewModelProvider.Factory, private val prefs: PreferenceProvider ) : BaseFragment(viewModelFactory) { - lateinit var binding: FragmentLoginBinding + private lateinit var composeView: ComposeView private val loginViewModel: LoginViewModel by viewModels { viewModelFactory @@ -53,56 +60,83 @@ constructor( container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentLoginBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } + @OptIn(ExperimentalMaterial3Api::class) override fun onFragmentInitiated() { + composeView.setContent { - initObjects() + val loginDataState by loginViewModel.loginDataState.collectAsState() + val progressErrorState by loginViewModel.progressAndErrorState.collectAsState() - initListeners() - } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() - private fun initObjects() { - loginViewModel.sessionExpiredListener = this - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - makeFieldsToggleEnable() + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + + LaunchedEffect(key1 = Unit) { + loginViewModel.uiEvents.collectLatest { event -> + when(event) { + UserLoggedIn -> { + findNavController().navigateSafely( + LoginFragmentDirections.actionLoginFragmentToHomeFragment() + ) + } + is OnTwoFactorAuthentication -> { + findNavController().navigateSafely( + LoginFragmentDirections.actionLoginFragmentToTwoFactorAuthFragment(event.crypto) + ) + } + } + } + } + + AgoraTheme { + LoginScreen(loginDataState,progressErrorState) { event -> + when(event){ + ForgotPasswordClick -> { + findNavController() + .navigateSafely(LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()) + } + BackArrowClick ->{ + findNavController().navigateUp() } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - makeFieldsToggleEnable() - it.message?.let { crypto -> - onTwoFactorAuthentication(crypto) - } ?: kotlin.run { - Navigation.findNavController(binding.root) - .navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment()) - } + LoginFacebookClick -> { + LoginManager + .getInstance() + .logInWithReadPermissions( + requireActivity(), + callbackManager!!, + listOf("email", "public_profile") + ) } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) - makeFieldsToggleEnable() - enableBtnFacebook() + else -> { + loginViewModel.onEvent(event) } - else -> {} } } } } + initObjects() + } - callbackManager = Factory.create() - + private fun initObjects() { + loginViewModel.sessionExpiredListener = this + callbackManager = CallbackManager.Factory.create() LoginManager.getInstance() .registerCallback( callbackManager, - object : FacebookCallback { - override fun onSuccess(loginResult: LoginResult?) { + object : FacebookCallback { + override fun onSuccess(loginResult: LoginResult) { Timber.d("Success") lifecycleScope.launch { prefs.setFacebookAccessToken(loginResult?.accessToken?.token) @@ -111,103 +145,14 @@ constructor( } override fun onCancel() { - enableBtnFacebook() - notify(resources.getString(R.string.login_cancelled)) + loginViewModel.showMessage(resources.getString(R.string.login_cancelled)) } override fun onError(exception: FacebookException) { - enableBtnFacebook() notify(exception.message.toString()) + loginViewModel.showMessage(exception.message.toString()) } } ) } - - private fun makeFieldsToggleEnable() { - binding.loginBtn.toggleIsEnable() - binding.username.toggleIsEnable() - binding.password.toggleIsEnable() - } - - private fun initListeners() { - binding.forgotPasswordTv.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()) - } - - binding.loginBtn.setOnClickListener { - val userName = binding.loginUserNameTil.editText - ?.text - .toString() - .trim { it <= ' ' } - val userPass = binding.loginPasswordTil.editText - ?.text - .toString() - .trim { it <= ' ' } - HideKeyboard.hideKeyboardInFrag(this) - loginViewModel.logInRequest(userName, userPass) - } - - binding.password.doAfterTextChanged { - updateLoginButton() - } - binding.username.doAfterTextChanged { - updateLoginButton() - } - - binding.btnFacebookLogin.setOnClickListener { - binding.btnFacebookLogin.disableView() - LoginManager.getInstance() - .logInWithReadPermissions( - activity, - listOf("email", "public_profile") - ) - } - } - - private fun updateLoginButton() { - val usernameInput: String = binding.username.text - .toString() - .trim() - val passwordInput: String = binding.password.text - .toString() - .trim() - binding.loginBtn.isEnabled = usernameInput.isNotEmpty() && passwordInput.isNotEmpty() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent? - ) { - Timber.d("Activity result") - callbackManager!!.onActivityResult(requestCode, resultCode, data) - } - - override fun onSessionExpired() { - // do nothing - } - - private fun enableBtnFacebook() { - binding.btnFacebookLogin.enableView() - } - - fun onTwoFactorAuthentication( - crypto: String - ) { - lifecycleScope.launch { - loginViewModel.getLoggedInUser() - .collect { - if (it != null) { - if (it.twoFactorAuthentication!!) { - notify(getString(R.string.otp_sent)) - val action = - LoginFragmentDirections.actionLoginFragmentToTwoFactorAuthFragment(crypto) - Navigation.findNavController(binding.root) - .navigate(action) - } - } - } - } - } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt index b18a262e..332eaf11 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt @@ -2,15 +2,25 @@ package org.aossie.agoraandroid.ui.fragments.auth.login 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.StateFlow +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.data.db.PreferenceProvider import org.aossie.agoraandroid.domain.model.AuthResponseModel import org.aossie.agoraandroid.domain.model.LoginDtoModel import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.useCases.authentication.login.LogInUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent +import org.aossie.agoraandroid.ui.screens.auth.models.LoginModel +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.NoInternetException @@ -27,6 +37,15 @@ constructor( private val logInUseCases: LogInUseCases ) : ViewModel() { + private val _loginDataState = MutableStateFlow (LoginModel()) + val loginDataState = _loginDataState.asStateFlow() + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() + var sessionExpiredListener: SessionExpiredListener? = null private val _getLoginStateFlow: MutableStateFlow?> = @@ -40,8 +59,11 @@ constructor( password: String, trustedDevice: String? = null ) { + showLoading(R.string.authenticating) _getLoginStateFlow.value = ResponseUI.loading() if (identifier.isEmpty() || password.isEmpty()) { + hideLoading() + showMessage(AppConstants.INVALID_CREDENTIALS_MESSAGE) _getLoginStateFlow.value = ResponseUI.error(AppConstants.INVALID_CREDENTIALS_MESSAGE) return } @@ -62,19 +84,36 @@ constructor( subscribeToFCM(mail) } Timber.d(user.toString()) + hideSnackBar() + hideLoading() if (!it.twoFactorAuthentication!!) { + _uiEvents.emit(LoginUiEvent.UserLoggedIn) _getLoginStateFlow.value = ResponseUI.success() } else { - _getLoginStateFlow.value = ResponseUI.success(user.crypto!!) + logInUseCases.getUser().collectLatest { + it?.let { + it.twoFactorAuthentication?.let { + if(it){ + _uiEvents.emit(LoginUiEvent.OnTwoFactorAuthentication(user.crypto!!)) + _getLoginStateFlow.value = ResponseUI.success(user.crypto!!) + }else{ + showMessage(R.string.login_error) + } + } + } + } } } } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } @@ -102,6 +141,7 @@ constructor( } fun facebookLogInRequest() { + showLoading(R.string.facebook_login) _getLoginStateFlow.value = ResponseUI.loading() viewModelScope.launch { try { @@ -113,12 +153,15 @@ constructor( } Timber.d(authResponse.toString()) } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } @@ -137,16 +180,78 @@ constructor( logInUseCases.saveUser(user) Timber.d(authResponse.toString()) prefs.setIsFacebookUser(true) + hideLoading() + hideSnackBar() + _uiEvents.emit(LoginUiEvent.UserLoggedIn) _getLoginStateFlow.value = ResponseUI.success() } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } } + + fun onEvent(event: LoginScreenEvent) { + when(event){ + is LoginScreenEvent.EnteredPassword -> { + _loginDataState.value = loginDataState.value.copy( + password = event.password + ) + } + is LoginScreenEvent.EnteredUsername -> { + _loginDataState.value = loginDataState.value.copy( + username = event.username + ) + } + LoginScreenEvent.LoginClick -> { + if(_loginDataState.value.username.isEmpty()){ + showMessage(R.string.invalid_username) + return + } + if(_loginDataState.value.password.isEmpty()){ + showMessage(R.string.invalid_password) + return + } + logInRequest(_loginDataState.value.username, _loginDataState.value.password) + } + else -> {} + } + } + + 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) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt index dc3611d5..bcdd570c 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt @@ -1,31 +1,24 @@ package org.aossie.agoraandroid.ui.fragments.auth.signup import android.os.Bundle -import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener -import android.widget.ArrayAdapter -import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.doAfterTextChanged +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.array -import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.data.network.dto.SecurityQuestionDto -import org.aossie.agoraandroid.databinding.FragmentSignUpBinding -import org.aossie.agoraandroid.domain.model.NewUserDtoModel +import androidx.navigation.fragment.findNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toggleIsEnable +import org.aossie.agoraandroid.ui.screens.auth.signup.SignUpScreen +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -37,162 +30,53 @@ constructor( private val viewModelFactory: ViewModelProvider.Factory ) : BaseFragment(viewModelFactory) { - private var securityQuestionOfSignUp: String? = null - private val signUpViewModel: SignUpViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentSignUpBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSignUpBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - signUpViewModel.sessionExpiredListener = this - - binding.signupBtn.setOnClickListener { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - validateAllFields() - } - - val adapter = ArrayAdapter.createFromResource( - requireContext(), array.security_questions, - android.R.layout.simple_spinner_item - ) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + composeView.setContent { - binding.signUpSecurityQuestion.adapter = adapter + val signUpDataState by signUpViewModel.signUpDataState.collectAsState() + val progressErrorState by signUpViewModel.progressAndErrorState.collectAsState() - binding.signUpSecurityQuestion.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected( - adapterView: AdapterView<*>, - view: View?, - i: Int, - l: Long - ) { - securityQuestionOfSignUp = adapterView.getItemAtPosition(i) - .toString() - } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() - override fun onNothingSelected(adapterView: AdapterView<*>?) { - securityQuestionOfSignUp = resources.getStringArray(array.security_questions)[0] + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} } - } - - // Attach text watcher to different text input edit texts - binding.etUsername.doAfterTextChanged { doAfterTextChange() } - binding.etFirstName.doAfterTextChanged { doAfterTextChange() } - binding.etLastName.doAfterTextChanged { doAfterTextChange() } - binding.etEmail.doAfterTextChanged { doAfterTextChange() } - binding.etAnswer.doAfterTextChanged { doAfterTextChange() } - binding.etPassword.doAfterTextChanged { doAfterTextChange() } - - lifecycleScope.launch { - signUpViewModel.getSignUpStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - makeFieldsToggleEnable() - } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(string.verify_account)) - makeFieldsToggleEnable() + AgoraTheme { + SignUpScreen(signUpDataState,progressErrorState) { event -> + when(event) { + BackArrowClick -> { + findNavController().navigateUp() } - ResponseUI.Status.ERROR -> { - notify(it.message) - binding.progressBar.hide() - makeFieldsToggleEnable() + else -> { + signUpViewModel.onEvent(event) } - else -> {} } } } } - } - - private fun makeFieldsToggleEnable() { - binding.signupBtn.toggleIsEnable() - binding.etUsername.toggleIsEnable() - binding.etFirstName.toggleIsEnable() - binding.etLastName.toggleIsEnable() - binding.etEmail.toggleIsEnable() - binding.etPassword.toggleIsEnable() - binding.signUpSecurityQuestion.toggleIsEnable() - binding.securityAnswer.toggleIsEnable() - } - - private fun validateAllFields() { - val userName = binding.signupUserName.editText - ?.text - .toString() - val firstName = binding.signupFirstName.editText - ?.text - .toString() - val lastName = binding.signupLastName.editText - ?.text - .toString() - val userEmail = binding.signupEmail.editText - ?.text - .toString() - val userPass = binding.signupPassword.editText - ?.text - .toString() - val securityQuestionAnswer = binding.securityAnswer.editText - ?.text - .toString() - val securityQuestion = securityQuestionOfSignUp - if (!Patterns.EMAIL_ADDRESS.matcher(userEmail) - .matches() - ) { - binding.signupEmail.error = "Enter a valid email address!!!" - } else if (userPass.length < 6) { - binding.signupPassword.error = "password length must be atleast 6 !!!" - } else { - binding.signupEmail.error = null - binding.signupPassword.error = null - signUpViewModel.signUpRequest( - NewUserDtoModel( - userEmail, firstName, userName, lastName, userPass, - SecurityQuestionDto(securityQuestionAnswer, "", securityQuestion!!) - ) - ) - } - } - private fun doAfterTextChange() { - val usernameInput: String = binding.etUsername.text - .toString() - .trim() - val passwordInput: String = binding.etPassword.text - .toString() - .trim() - val firstNameInput: String = binding.etFirstName.text - .toString() - .trim() - val lastNameInput: String = binding.etLastName.text - .toString() - .trim() - val emailInput: String = binding.etEmail.text - .toString() - .trim() - val answerInput: String = binding.etAnswer.text - .toString() - .trim() - binding.signupBtn.isEnabled = usernameInput.isNotEmpty() && - passwordInput.isNotEmpty() && - firstNameInput.isNotEmpty() && - lastNameInput.isNotEmpty() && - emailInput.isNotEmpty() && - answerInput.isNotEmpty() + signUpViewModel.sessionExpiredListener = this } override fun onSessionExpired() { diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt index 8e94e369..ef5659ca 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt @@ -1,17 +1,31 @@ package org.aossie.agoraandroid.ui.fragments.auth.signup +import android.util.Patterns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.data.network.dto.SecurityQuestionDto import org.aossie.agoraandroid.domain.model.NewUserDtoModel import org.aossie.agoraandroid.domain.useCases.authentication.signUp.SignUpUseCase import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.auth.models.SignUpModel +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredEmail +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredSecurityAnswer +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SelectedSecurityQuestion +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SignUpClICK +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.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import timber.log.Timber import javax.inject.Inject @@ -22,31 +36,132 @@ constructor( private val signUpUseCase: SignUpUseCase ) : ViewModel() { + private val _signUpDataState = MutableStateFlow (SignUpModel()) + val signUpDataState = _signUpDataState.asStateFlow() + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + lateinit var sessionExpiredListener: SessionExpiredListener - private val _getSignUpStateFlow: MutableStateFlow?> = MutableStateFlow(null) - val getSignUpStateFlow = _getSignUpStateFlow.asStateFlow() - fun signUpRequest( + private fun signUpRequest( userDataModel: NewUserDtoModel ) { - _getSignUpStateFlow.value = ResponseUI.loading() + showLoading("Creating account...") viewModelScope.launch { try { val call = signUpUseCase(userDataModel) Timber.d(call) - _getSignUpStateFlow.value = ResponseUI.success() + showMessage(R.string.verify_account) + hideLoading() } catch (e: ApiException) { + hideLoading() if (e.message == "409") { - _getSignUpStateFlow.value = ResponseUI.error(AppConstants.USER_ALREADY_FOUND_MESSAGE) + showMessage(AppConstants.USER_ALREADY_FOUND_MESSAGE) } else { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } catch (e: SessionExpirationException) { + hideLoading() sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + hideLoading() + showMessage(e.message!!) } catch (e: Exception) { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + hideLoading() + showMessage(e.message!!) + } + } + } + + fun onEvent(event: SignUpScreenEvent) { + when(event){ + is EnteredPassword -> { + _signUpDataState.value = signUpDataState.value.copy( + password = event.password + ) + } + is EnteredUsername -> { + _signUpDataState.value = signUpDataState.value.copy( + username = event.username + ) + } + SignUpClICK -> { + validateFields(_signUpDataState.value) + } + is EnteredEmail -> { + _signUpDataState.value = signUpDataState.value.copy( + email = event.email + ) + } + is EnteredFirstName -> { + _signUpDataState.value = signUpDataState.value.copy( + firstName = event.firstName + ) + } + is EnteredLastName -> { + _signUpDataState.value = signUpDataState.value.copy( + lastName = event.lastName + ) + } + is EnteredSecurityAnswer -> { + _signUpDataState.value = signUpDataState.value.copy( + securityAnswer = event.answer + ) + } + is SelectedSecurityQuestion -> { + _signUpDataState.value = signUpDataState.value.copy( + securityQuestion = event.question + ) } + else -> {} } } + + private fun validateFields(value: SignUpModel) { + if(value.username.trim().isEmpty()){ + showMessage(R.string.invalid_username) + } else if (!Patterns.EMAIL_ADDRESS.matcher(value.email.trim()) + .matches() + ) { + showMessage("Enter a valid email address!!!") + } else if (value.password.trim().length < 6) { + showMessage("password length must be atleast 6 !!!") + } else { + signUpRequest( + NewUserDtoModel( + value.email.trim(), value.firstName.trim(), value.username.trim(), value.lastName.trim(), value.password.trim(), + SecurityQuestionDto(value.securityAnswer.trim(), "", value.securityQuestion!!) + ) + ) + } + } + + 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) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt index dc5268e1..9851bece 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt @@ -4,20 +4,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.databinding.FragmentTwoFactorAuthBinding -import org.aossie.agoraandroid.domain.model.UserModel +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.flow.collectLatest import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.fragments.auth.twoFactorAuthentication.TwoFactorAuthViewModel.UiEvents.TwoFactorAuthComplete +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreen +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.OnBackClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.ResendOtpClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.VerifyOtpClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme +import org.aossie.agoraandroid.utilities.navigateSafely import javax.inject.Inject class TwoFactorAuthFragment @@ -27,102 +28,49 @@ constructor( ) : BaseFragment(viewModelFactory) { private var crypto: String? = null - private var user: UserModel? = null private val viewModel: TwoFactorAuthViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentTwoFactorAuthBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentTwoFactorAuthBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { crypto = TwoFactorAuthFragmentArgs.fromBundle(requireArguments()).crypto viewModel.sessionExpiredListener = this - lifecycleScope.launch { - viewModel.user.collect { - if (it != null) { - user = it + composeView.setContent { + val progressErrorState by viewModel.progressAndErrorState + LaunchedEffect(key1 = viewModel) { + viewModel.uiEventsFlow.collectLatest { + when(it) { + TwoFactorAuthComplete -> { + findNavController() + .navigateSafely(TwoFactorAuthFragmentDirections.actionTwoFactorAuthFragmentToHomeFragment()) + } + } } } - } - - binding.btnVerifyOtp.setOnClickListener { - binding.progressBar.show() - val otp = binding.otpTil.editText - ?.text - .toString() - .trim { it <= ' ' } - if (otp.isEmpty()) { - notify(getString(string.enter_otp)) - binding.progressBar.hide() - } else { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - if (binding.cbTrustedDevice.isChecked) { - viewModel.verifyOTP( - otp, binding.cbTrustedDevice.isChecked, user!!.crypto!! - ) - } else { - binding.progressBar.hide() - notify(getString(string.tap_on_checkbox)) + AgoraTheme { + TwoFactorAuthScreen( + progressErrorState = progressErrorState + ) {event -> + when(event) { + ResendOtpClick -> viewModel.resendOTP() + is VerifyOtpClick -> viewModel.verifyOTP(event.otp, event.trustedDevice) + OnBackClick -> findNavController().navigateUp() + } } } } - - binding.tvResendOtp.setOnClickListener { - if (user != null) { - binding.progressBar.show() - viewModel.resendOTP(user!!.username!!) - } else { - notify(getString(string.something_went_wrong_please_try_again_later)) - } - } - - lifecycleScope.launch { - viewModel.verifyOtpResponse.collect { - handleVerifyOtp(it) - } - } - - lifecycleScope.launch { - viewModel.resendOtpResponse.collect { - handleResendOtp(it) - } - } - } - - private fun handleVerifyOtp(response: ResponseUI?) = when (response?.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - Navigation.findNavController(binding.root) - .navigate(TwoFactorAuthFragmentDirections.actionTwoFactorAuthFragmentToHomeFragment()) - } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(response?.message) - } - else -> { // Do Nothing - } - } - - private fun handleResendOtp(response: ResponseUI?) = when (response?.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(string.otp_sent)) - } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(response?.message) - } - else -> { // Do Nothing - } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt index 61649d1c..8ed80515 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt @@ -1,18 +1,24 @@ package org.aossie.agoraandroid.ui.fragments.auth.twoFactorAuthentication +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.model.VerifyOtpDtoModel import org.aossie.agoraandroid.domain.useCases.authentication.twoFactorAuthentication.TwoFactorAuthUseCases 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.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import timber.log.Timber import javax.inject.Inject @@ -24,31 +30,43 @@ constructor( ) : ViewModel() { lateinit var sessionExpiredListener: SessionExpiredListener - val user = twoFactorAuthUseCases.getUser() + private var user:UserModel? = null - private val mVerifyOtpResponse = MutableStateFlow?>(null) + private val _uiEventsFlow = MutableSharedFlow() + val uiEventsFlow = _uiEventsFlow.asSharedFlow() - val verifyOtpResponse: StateFlow?> - get() = mVerifyOtpResponse + private val _progressAndErrorState = mutableStateOf(ScreensState()) + val progressAndErrorState: State = _progressAndErrorState - private val mResendOtpResponse = MutableStateFlow?>(null) - - val resendOtpResponse: StateFlow?> - get() = mResendOtpResponse + init { + viewModelScope.launch { + twoFactorAuthUseCases.getUser().collectLatest { + user = it + } + } + } fun verifyOTP( otp: String, - trustedDevice: Boolean, - crypto: String + trustedDevice: Boolean ) { if (otp.isEmpty()) { - mVerifyOtpResponse.value = ResponseUI.error(AppConstants.INVALID_OTP_MESSAGE) + showMessage(string.enter_otp) + return + } + if(!trustedDevice) { + showMessage(R.string.tap_on_checkbox) + return + } + if(user == null) { + showMessage(R.string.something_went_wrong_please_try_again_later) return } viewModelScope.launch { + showLoading(R.string.verifying_otp) try { val authResponse = - twoFactorAuthUseCases.verifyOTP(VerifyOtpDtoModel(crypto, otp, trustedDevice)) + twoFactorAuthUseCases.verifyOTP(VerifyOtpDtoModel(user!!.crypto, otp, trustedDevice)) authResponse.let { val user = UserModel( it.username, it.email, it.firstName, it.lastName, it.avatarURL, it.crypto, @@ -58,28 +76,33 @@ constructor( ) twoFactorAuthUseCases.saveUser(user) Timber.d(user.toString()) - mVerifyOtpResponse.value = ResponseUI.success() + hideLoading() + _uiEventsFlow.emit(UiEvents.TwoFactorAuthComplete) } } catch (e: ApiException) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } - fun resendOTP( - username: String, - ) { - if (username.isEmpty()) { - mResendOtpResponse.value = ResponseUI.error(AppConstants.LOGIN_AGAIN_MESSAGE) + fun resendOTP() { + if(user == null) { + showMessage(R.string.something_went_wrong_please_try_again_later) + return + } + val username = user!!.username + if (username!!.isEmpty()) { + showMessage(AppConstants.LOGIN_AGAIN_MESSAGE) return } viewModelScope.launch { + showLoading(R.string.sending_otp) try { val authResponse = twoFactorAuthUseCases.resendOTP(username) authResponse.let { @@ -90,17 +113,50 @@ constructor( it.refreshToken?.expiresOn, it.trustedDevice ) twoFactorAuthUseCases.saveUser(user) - mResendOtpResponse.value = ResponseUI.success() + showMessage(R.string.otp_sent) } } catch (e: ApiException) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } -} + + private fun showLoading(message: Any) { + _progressAndErrorState.value=progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + private 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 TwoFactorAuthComplete:UiEvents() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt index 4bc3a507..1c2eaceb 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt @@ -5,38 +5,53 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment +import kotlinx.coroutines.flow.MutableStateFlow import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.databinding.FragmentContactUsBinding +import org.aossie.agoraandroid.ui.screens.contactUs.ContactUsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import org.aossie.agoraandroid.utilities.browse -import org.aossie.agoraandroid.utilities.snackbar /** * A simple [Fragment] subclass. */ class ContactUsFragment : Fragment() { - lateinit var binding: FragmentContactUsBinding + private lateinit var composeView: ComposeView + private val messageState = MutableStateFlow ("") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentContactUsBinding.inflate(layoutInflater) - initListeners() - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } - private fun initListeners() { - binding.btnGitter.setOnClickListener { - openUrl("https://gitter.im/aossie/home") - } - binding.btnGitlab.setOnClickListener { - openUrl("https://gitlab.com/aossie") - } - binding.btnReport.setOnClickListener { - openUrl("https://gitlab.com/aossie/agora-android/issues/new") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + val message = messageState.collectAsState() + AgoraTheme { + ContactUsScreen( + message, + onBtnGitlabClicked = { + openUrl("https://gitlab.com/aossie") + }, + onBtnGitterClicked = { + openUrl("https://gitter.im/aossie/home") + }, + onBtnReportBugClicked = { + openUrl("https://gitlab.com/aossie/agora-android/issues/new") + } + ) { + messageState.value = "" + } + } } } @@ -44,7 +59,7 @@ class ContactUsFragment : Fragment() { try { context?.browse(url) } catch (e: ActivityNotFoundException) { - binding.root.snackbar(resources.getString(string.no_browser)) + messageState.value = resources.getString(string.no_browser) } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt index 18f626ab..8ffdeab0 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt @@ -4,21 +4,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentActiveElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -33,79 +28,49 @@ constructor( private val displayElectionViewModel: DisplayElectionViewModel by viewModels { viewModelFactory } - - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter private val onItemClicked = { _id: String -> val action = ActiveElectionsFragmentDirections.actionActiveElectionsFragmentToElectionDetailsFragment(_id) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentActiveElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentActiveElectionsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvActiveElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + bindUI() + composeView.setContent { + val elections by displayElectionViewModel.activeElections.collectAsState() + val progressErrorState by displayElectionViewModel.progressAndErrorState.collectAsState() + val searchText by displayElectionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + displayElectionViewModel.getActiveElectionsState(it) + }, + onItemClicked = onItemClicked + ) + } } - } - override fun onNetworkConnected() { - bindUI() } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onNetworkConnected() { bindUI() } private fun bindUI() { - lifecycleScope.launch { - try { - val elections = displayElectionViewModel.activeElections.await() - elections.collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() - } - } - } - - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } - } - - private fun filter(query: String) { - val updatedList = displayElectionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + displayElectionViewModel.getActiveElectionsState("") } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt index 3a8552a6..ca2ca034 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt @@ -1,8 +1,18 @@ package org.aossie.agoraandroid.ui.fragments.displayelections +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.model.ElectionModel import org.aossie.agoraandroid.domain.useCases.displayElection.DisplayElectionsUseCases +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.lazyDeferred import java.text.SimpleDateFormat import java.util.Calendar @@ -21,14 +31,78 @@ constructor( .time private val date: String = formatter.format(currentDate) - val activeElections by lazyDeferred { - displayElectionsUseCases.getActiveElections(date) - } + val activeElections = MutableStateFlow>(emptyList()) val pendingElections by lazyDeferred { displayElectionsUseCases.getPendingElections(date) } - val finishedElections by lazyDeferred { - displayElectionsUseCases.getFinishedElections(date) + + val finishedElections = MutableStateFlow>(emptyList()) + val search = mutableStateOf("") + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + fun getFinishedElectionsState(query:String){ + viewModelScope.launch { + try { + displayElectionsUseCases.getFinishedElections(date).collectLatest { list -> + search.value = query + if(query.isEmpty()) { + finishedElections.emit(list) + }else{ + finishedElections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + + fun getActiveElectionsState(query:String) { + viewModelScope.launch { + try { + displayElectionsUseCases.getActiveElections(date).collectLatest { list -> + search.value = query + if(query.isEmpty()) { + activeElections.emit(list) + }else{ + activeElections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + + 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) + ) } fun filter( diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt index ee042eb9..e2d5ac03 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt @@ -4,21 +4,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentFinishedElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -34,38 +29,43 @@ constructor( viewModelFactory } - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter - private val onItemClicked = { _id: String -> val action = FinishedElectionsFragmentDirections.actionFinishedElectionsFragmentToElectionDetailsFragment( _id ) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentFinishedElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentFinishedElectionsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvFinishedElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + bindUI() + composeView.setContent { + val elections by displayElectionViewModel.finishedElections.collectAsState() + val progressErrorState by displayElectionViewModel.progressAndErrorState.collectAsState() + val searchText by displayElectionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + displayElectionViewModel.getFinishedElectionsState(it) + }, + onItemClicked = onItemClicked + ) + } } } @@ -73,42 +73,7 @@ constructor( bindUI() } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - bindUI() - } - private fun bindUI() { - lifecycleScope.launch { - try { - val elections = displayElectionViewModel.finishedElections.await() - elections.collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() - } - } - } - - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } - } - - private fun filter(query: String) { - val updatedList = displayElectionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + displayElectionViewModel.getFinishedElectionsState("") } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt index 2f876631..73b03869 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt @@ -31,6 +31,8 @@ import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetails import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.ViewVotersClick import org.aossie.agoraandroid.ui.theme.AgoraTheme import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.ViewVotersClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -52,6 +54,7 @@ constructor( viewModelFactory } private lateinit var composeView: ComposeView + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -81,6 +84,10 @@ constructor( screenState = progressErrorState, electionDetails = electionDetails, verifiedVoterStatus = verifiedVoterStatus + AgoraTheme { + ElectionDetailsScreen( + screenState = progressErrorState, + electionDetails = electionDetails ) { event-> when(event){ BallotClick -> { @@ -101,6 +108,15 @@ constructor( val intent = Intent(requireActivity(), CastVoteActivity::class.java) intent.putExtra(AppConstants.ELECTION_ID, id!!) startActivity(intent) + ) + findNavController().navigate(action) + } + ViewVotersClick -> { + val action = + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToVotersFragment( + id!! + ) + findNavController().navigate(action) } else -> electionDetailsViewModel.onEvent(event) } @@ -120,6 +136,10 @@ constructor( electionDetailsViewModel.getElectionDetailsById(id ?: "") } + override fun onNetworkConnected() { + electionDetailsViewModel.getElectionDetailsById(id ?: "") + } + private fun setObserver() = lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state = State.STARTED) { electionDetailsViewModel.uiEventsFlow.collectLatest { diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt index ce9ebc88..d6111cda 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt @@ -1,11 +1,19 @@ package org.aossie.agoraandroid.ui.fragments.elections +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.model.ElectionModel import org.aossie.agoraandroid.domain.useCases.electionsAndCalenderView.ElectionsUseCases +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.utilities.AppConstants import java.util.Locale import javax.inject.Inject @@ -15,6 +23,58 @@ constructor( private val electionsUseCases: ElectionsUseCases ) : ViewModel() { + val elections = MutableStateFlow>(emptyList()) + val search = mutableStateOf("") + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + fun getElectionsState(query:String){ + viewModelScope.launch { + try { + electionsUseCases.getElections().collectLatest { list -> + search.value = query + if(query.isEmpty()) { + elections.emit(list) + }else{ + elections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + + 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) + ) + } + fun getElections(): Flow> { viewModelScope.launch { electionsUseCases.fetchAndSaveElection() diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt index 08e0826e..8eedd9dd 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt @@ -4,20 +4,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject class ElectionsFragment @@ -30,78 +25,48 @@ constructor( viewModelFactory } - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter - private val onItemClicked = { _id: String -> val action = ElectionsFragmentDirections .actionElectionsFragmentToElectionDetailsFragment(_id) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentElectionsBinding.inflate(inflater) - return binding.root - } - - override fun onFragmentInitiated() { - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvTotalElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + return ComposeView(requireContext()).also { + composeView = it } } - override fun onNetworkConnected() { - bindUI() - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onFragmentInitiated() { bindUI() - } - - private fun bindUI() { - lifecycleScope.launch { - try { - electionViewModel.getElections() - .collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() + composeView.setContent { + val elections by electionViewModel.elections.collectAsState() + val progressErrorState by electionViewModel.progressAndErrorState.collectAsState() + val searchText by electionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + electionViewModel.getElectionsState(it) + }, + onItemClicked = onItemClicked + ) } } } - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } + private fun bindUI() { + electionViewModel.getElectionsState(electionViewModel.search.value) } - private fun filter(query: String) { - val updatedList = electionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + override fun onNetworkConnected() { + bindUI() } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt index 77e5febb..72f025df 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt @@ -1,28 +1,35 @@ package org.aossie.agoraandroid.ui.fragments.home -import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.doOnLayout +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.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import com.takusemba.spotlight.Spotlight +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.color -import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentHomeBinding import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.auth.login.LoginViewModel -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.TargetData -import org.aossie.agoraandroid.utilities.getSpotlight -import org.aossie.agoraandroid.utilities.scrollToView +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.UserLoggedIn +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.home.HomeScreen +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.ActiveElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.CreateElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.FinishedElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.PendingElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.Refresh +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.TotalElectionClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import timber.log.Timber import java.text.ParseException import java.text.SimpleDateFormat @@ -45,64 +52,68 @@ constructor( viewModelFactory } - private lateinit var binding: FragmentHomeBinding - - private var spotlight: Spotlight? = null - private var spotlightTargets: ArrayList? = null - private var currentSpotlightIndex = 0 + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentHomeBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { loginViewModel.sessionExpiredListener = this - binding.swipeRefresh.setColorSchemeResources(color.logo_yellow, color.logo_green) + composeView.setContent { + val homeScreenDataState by homeViewModel.countMediatorLiveData.observeAsState() - binding.cardViewActiveElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToActiveElectionsFragment()) - } - binding.cardViewPendingElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToPendingElectionsFragment()) - } - binding.cardViewFinishedElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToFinishedElectionsFragment()) - } - binding.cardViewTotalElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToElectionsFragment()) - } - binding.buttonCreateElection.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToCreateElectionFragment()) - } - binding.swipeRefresh.setOnRefreshListener { updateUi() } + val progressErrorState1 = loginViewModel.progressAndErrorState + val progressErrorState2 = homeViewModel.progressAndErrorState - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - // Do Nothing - } - ResponseUI.Status.SUCCESS -> { + val progressErrorState by merge(progressErrorState1,progressErrorState2).collectAsState(initial = ScreensState()) + + LaunchedEffect(key1 = Unit) { + loginViewModel.uiEvents.collectLatest { event -> + when(event) { + UserLoggedIn -> { updateUi() } - ResponseUI.Status.ERROR -> { - notify(it.message) - } else -> {} } } } + + AgoraTheme { + HomeScreen(homeScreenDataState,progressErrorState){ event -> + when(event){ + ActiveElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToActiveElectionsFragment()) + } + CreateElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToCreateElectionFragment()) + } + FinishedElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToFinishedElectionsFragment()) + } + PendingElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToPendingElectionsFragment()) + } + TotalElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToElectionsFragment()) + } + Refresh -> { + updateUi() + } + } + } + } } lifecycleScope.launch { @@ -141,23 +152,7 @@ constructor( } } - homeViewModel.countMediatorLiveData.observe( - viewLifecycleOwner, - { - binding.textViewActiveCount.text = it[ACTIVE_ELECTION_COUNT].toString() - binding.textViewTotalCount.text = it[TOTAL_ELECTION_COUNT].toString() - binding.textViewPendingCount.text = it[PENDING_ELECTION_COUNT].toString() - binding.textViewFinishedCount.text = it[FINISHED_ELECTION_COUNT].toString() - binding.shimmerViewContainer.stopShimmer() - binding.shimmerViewContainer.visibility = View.GONE - binding.constraintLayout.visibility = View.VISIBLE - binding.swipeRefresh.isRefreshing = false // Disables the refresh icon - } - ) updateUi() - binding.root.doOnLayout { - checkIsFirstOpen() - } } override fun onNetworkConnected() { @@ -165,7 +160,6 @@ constructor( } override fun onDestroyView() { - binding.swipeRefresh.setOnRefreshListener(null) homeViewModel.sessionExpiredListener = null loginViewModel.sessionExpiredListener = null super.onDestroyView() @@ -176,70 +170,5 @@ constructor( preferenceProvider.setUpdateNeeded(true) homeViewModel.getElections() } - binding.shimmerViewContainer.startShimmer() - binding.shimmerViewContainer.visibility = View.VISIBLE - binding.constraintLayout.visibility = View.GONE - } - - private fun checkIsFirstOpen() { - lifecycleScope.launch { - if (!preferenceProvider.isDisplayed(binding.root.id.toString()) - .first() - ) { - spotlightTargets = getSpotlightTargets() - preferenceProvider.setDisplayed(binding.root.id.toString()) - showSpotlight() - } - } - } - - private fun showSpotlight() { - spotlightTargets?.let { - if (currentSpotlightIndex in it.indices) { - scrollToView(binding.scrollView, it[currentSpotlightIndex].targetView) - spotlight = requireActivity().getSpotlight( - it[currentSpotlightIndex++], - { - destroySpotlight() - }, - { - it.clear() - destroySpotlight() - }, - { - if (isAdded) { - showSpotlight() - } - } - ) - spotlight?.start() - } - } - } - - private fun getSpotlightTargets(): ArrayList { - val targetData = ArrayList() - targetData.add( - TargetData( - binding.buttonCreateElection, getString(string.Create_Election), - getString(string.create_election_spotlight) - ) - ) - return targetData - } - - private fun destroySpotlight() { - spotlight?.finish() - spotlight = null - } - - override fun onPause() { - super.onPause() - destroySpotlight() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - destroySpotlight() } } 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 17ef2419..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,17 +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 @@ -26,32 +36,53 @@ 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() .time private val date: String = formatter.format(currentDate) + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + 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() + _countMediatorLiveData.value = mutableMapOf( + TOTAL_ELECTION_COUNT to 0, + PENDING_ELECTION_COUNT to 0, + FINISHED_ELECTION_COUNT to 0, + ACTIVE_ELECTION_COUNT to 0 + ) addSource() } fun getElections() { + showLoading("Loading...") GlobalScope.launch { - homeViewModelUseCases.fetchAndSaveElection() + val response = homeViewModelUseCases.fetchAndSaveElection() + response.elections.let { + hideLoading() + } } } private fun addSource() { viewModelScope.launch { + _countMediatorLiveData.addSource(homeViewModelUseCases.getTotalElectionsCount()) { value -> _countMediatorLiveData.value = _countMediatorLiveData.value.apply { this?.let { @@ -91,20 +122,70 @@ 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) + ) + } + + 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) + ) + } } 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/fragments/share/ShareWithOthersFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt index 9b717acb..96b4f927 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt @@ -6,47 +6,58 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import com.facebook.FacebookSdk -import org.aossie.agoraandroid.databinding.FragmentShareWithOthersBinding +import org.aossie.agoraandroid.ui.screens.share.ShareWithOthersScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme /** * A simple [Fragment] subclass. */ class ShareWithOthersFragment : Fragment() { - private lateinit var binding: FragmentShareWithOthersBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentShareWithOthersBinding.inflate(layoutInflater) - binding.btnShare.setOnClickListener { - val shareIntent = Intent(Intent.ACTION_SEND) + return ComposeView(requireContext()).also { + composeView = it + } + } - // Get the app link in the Play Store - val appPackageName = FacebookSdk.getApplicationContext() - .packageName - val strAppLink: String = try { - "https://play.google.com/store/apps/details?id=$appPackageName" - } catch (activityNotFound: ActivityNotFoundException) { - "https://play.google.com/store/apps/details?id=$appPackageName" - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + AgoraTheme { + ShareWithOthersScreen { + val shareIntent = Intent(Intent.ACTION_SEND) - // This is the sharing part - shareIntent.type = "text/link" - val shareBody = - """ + // Get the app link in the Play Store + val appPackageName = FacebookSdk.getApplicationContext() + .packageName + val strAppLink: String = try { + "https://play.google.com/store/apps/details?id=$appPackageName" + } catch (activityNotFound: ActivityNotFoundException) { + "https://play.google.com/store/apps/details?id=$appPackageName" + } + + // This is the sharing part + shareIntent.type = "text/link" + val shareBody = + """ Hey! Download Agora Vote application for Free and create Elections right now $strAppLink """.trimIndent() - val shareSub = "APP NAME/TITLE" - shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSub) - shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody) - startActivity(Intent.createChooser(shareIntent, "Share Agora Vote Using")) + val shareSub = "APP NAME/TITLE" + shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSub) + shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody) + startActivity(Intent.createChooser(shareIntent, "Share Agora Vote Using")) + } + } } - return binding.root } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt new file mode 100644 index 00000000..679382ac --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt @@ -0,0 +1,104 @@ +package org.aossie.agoraandroid.ui.screens.about + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +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.material.icons.Icons +import androidx.compose.material.icons.rounded.NavigateNext +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.ui.Modifier +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 org.aossie.agoraandroid.R +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutScreen(){ + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentPadding = PaddingValues(vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + item { + Image( + painter = painterResource(id = R.drawable.ic_about), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 230.dp) + ) + } + item { + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Text( + text = stringResource(id = R.string.about_info) +" "+ stringResource(id = R.string.about_info2), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Light, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Divider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + ) + } + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IconTextNavigationButton( + text = stringResource(id = R.string.privacy_policy), + iconStart = R.drawable.ic_privacy_policy, + iconEnd = { + Icon( + imageVector = Icons.Rounded.NavigateNext, + contentDescription = "") + }) { + + } + IconTextNavigationButton( + text = stringResource(id = R.string.terms_and_conditions), + iconStart = R.drawable.ic_terms_condition, + iconEnd = { + Icon( + imageVector = Icons.Rounded.NavigateNext, + contentDescription = "") + }) { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt new file mode 100644 index 00000000..faa9d464 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt @@ -0,0 +1,93 @@ +package org.aossie.agoraandroid.ui.screens.auth.forgotPassword + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnBackIconClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnSendLinkClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnUserNameEntered +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +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.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForgotPasswordScreen( + screenState: ScreensState, + userNameState:String, + onEvent:(ForgotPasswordScreenEvents) -> Unit, +){ + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.forgot_password) { + onEvent(OnBackIconClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + painter = painterResource(id = R.drawable.annoyed), + contentDescription = "") + } + item { + Text( + text = stringResource(id = R.string.enter_user_name_reset_pass_link), + style = MaterialTheme.typography.titleLarge + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(OnUserNameEntered(it)) + }, + value = userNameState, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryButton(text = stringResource(id = string.send_link)) { + onEvent(OnSendLinkClick) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt new file mode 100644 index 00000000..c3c71e9f --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt @@ -0,0 +1,8 @@ +package org.aossie.agoraandroid.ui.screens.auth.forgotPassword + +sealed class ForgotPasswordScreenEvents{ + object OnBackIconClick:ForgotPasswordScreenEvents() + object OnSendLinkClick:ForgotPasswordScreenEvents() + object SnackBarActionClick:ForgotPasswordScreenEvents() + data class OnUserNameEntered(val userName:String):ForgotPasswordScreenEvents() +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt new file mode 100644 index 00000000..e8b352e3 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt @@ -0,0 +1,135 @@ +package org.aossie.agoraandroid.ui.screens.auth.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +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.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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.ForgotPasswordClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginFacebookClick +import org.aossie.agoraandroid.ui.screens.auth.models.LoginModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +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.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen( + loginModel: LoginModel, + screenState: ScreensState, + onEvent:(LoginScreenEvent) -> Unit +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.login) { + onEvent(BackArrowClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + painter = painterResource(id = drawable.tree), + contentDescription = "") + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredUsername(it)) + }, + value = loginModel.username, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryPasswordField( + onPasswordChange = { + onEvent(EnteredPassword(it)) + }, + password = loginModel.password, + label = stringResource(id = string.password) + ) + } + item { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(id = string.forgot_password), + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .align(Alignment.CenterEnd) + .clickable { + onEvent(ForgotPasswordClick) + }, + ) + } + } + item { + PrimaryButton(text = stringResource(id = string.login)) { + onEvent(LoginClick) + } + } + item { + Text( + text = stringResource(id = string.or), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + item { + PrimaryButton( + text = stringResource(id = string.continue_with_facebook), + backgroundColor = MaterialTheme.colorScheme.background, + icon = painterResource(id = drawable.ic_facebook_logo) + ) { + onEvent(LoginFacebookClick) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt new file mode 100644 index 00000000..575f043a --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.auth.login.events + +sealed class LoginScreenEvent{ + data class EnteredUsername(val username:String): LoginScreenEvent() + data class EnteredPassword(val password:String): LoginScreenEvent() + object BackArrowClick: LoginScreenEvent() + object LoginClick: LoginScreenEvent() + object LoginFacebookClick: LoginScreenEvent() + object ForgotPasswordClick: LoginScreenEvent() + +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt new file mode 100644 index 00000000..1cb268be --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.screens.auth.login.events + +sealed class LoginUiEvent { + object UserLoggedIn: LoginUiEvent() + data class OnTwoFactorAuthentication(val crypto: String): LoginUiEvent() + +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt new file mode 100644 index 00000000..ba01fec1 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt @@ -0,0 +1,6 @@ +package org.aossie.agoraandroid.ui.screens.auth.models + +data class LoginModel( + val username: String = "", + val password: String = "" +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt new file mode 100644 index 00000000..98745a7c --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.auth.models + +data class SignUpModel( + val username: String = "", + val firstName: String = "", + val lastName: String = "", + val email: String = "", + val password: String = "", + val securityQuestion: String = "", + val securityAnswer: String = "", +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt new file mode 100644 index 00000000..52502980 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt @@ -0,0 +1,154 @@ +package org.aossie.agoraandroid.ui.screens.auth.signup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.models.SignUpModel +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredEmail +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredSecurityAnswer +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SelectedSecurityQuestion +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SignUpClICK +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +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.common.component.PrimaryTitledSpinner +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpScreen( + signUpModel: SignUpModel, + screenState: ScreensState, + onEvent:(SignUpScreenEvent) -> Unit +){ + + val securityQuestionsList = stringArrayResource(id = R.array.security_questions).toList() + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.create_account) { + onEvent(BackArrowClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredUsername(it)) + }, + value = signUpModel.username, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredFirstName(it)) + }, + value = signUpModel.firstName, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.first_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredLastName(it)) + }, + value = signUpModel.lastName, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.last_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredEmail(it)) + }, + value = signUpModel.email, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.email_address), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Email) + ) + } + item { + PrimaryPasswordField( + onPasswordChange = { + onEvent(EnteredPassword(it)) + }, + password = signUpModel.password, + label = stringResource(id = string.password) + ) + } + item { + PrimaryTitledSpinner( + title = stringResource(id = string.secret_security_question), + list = securityQuestionsList, + selectedIndex = if (signUpModel.securityQuestion.isNotEmpty()) securityQuestionsList.indexOf( + signUpModel.securityQuestion + ) else 0, + onItemSelected = { + onEvent(SelectedSecurityQuestion(securityQuestionsList[it])) + } + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredSecurityAnswer(it)) + }, + value = signUpModel.securityAnswer, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.answer) + ) + } + item { + PrimaryButton(text = stringResource(id = string.sign_up)) { + onEvent(SignUpClICK) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt new file mode 100644 index 00000000..f9c65906 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt @@ -0,0 +1,14 @@ +package org.aossie.agoraandroid.ui.screens.auth.signup.events + +sealed class SignUpScreenEvent{ + data class EnteredUsername(val username:String): SignUpScreenEvent() + data class EnteredFirstName(val firstName:String): SignUpScreenEvent() + data class EnteredLastName(val lastName:String): SignUpScreenEvent() + data class EnteredEmail(val email:String): SignUpScreenEvent() + data class EnteredPassword(val password:String): SignUpScreenEvent() + data class SelectedSecurityQuestion(val question:String): SignUpScreenEvent() + data class EnteredSecurityAnswer(val answer:String): SignUpScreenEvent() + object BackArrowClick: SignUpScreenEvent() + object SignUpClICK: SignUpScreenEvent() + +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt new file mode 100644 index 00000000..22de63cb --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt @@ -0,0 +1,95 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun OtpTextField( + modifier: Modifier = Modifier, + otpText: String, + otpCount: Int = 6, + onOtpTextChange: (String, Boolean) -> Unit +) { + LaunchedEffect(Unit) { + if (otpText.length > otpCount) { + throw IllegalArgumentException("Otp text value must not have more than otpCount: $otpCount characters") + } + } + + BasicTextField( + modifier = modifier, + maxLines = 1, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + if (it.text.length <= otpCount) { + onOtpTextChange.invoke(it.text, it.text.length == otpCount) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + decorationBox = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + repeat(otpCount) { index -> + CharView( + index = index, + text = otpText + ) + } + } + } + ) +} + +@Composable +private fun CharView( + index: Int, + text: String +) { + val isFocused = text.length == index + val char = when { + index == text.length -> "0" + index > text.length -> "" + else -> text[index].toString() + } + Text( + modifier = Modifier + .width(40.dp) + .border( + shape = RoundedCornerShape(8.dp), + color = if (isFocused) { + MaterialTheme.colorScheme.outlineVariant + } else { + MaterialTheme.colorScheme.outline + }, + width = 1.dp + ) + .padding(2.dp), + text = char, + style = MaterialTheme.typography.headlineMedium, + color = if (isFocused) { + MaterialTheme.colorScheme.outlineVariant + } else { + MaterialTheme.colorScheme.outline + }, + textAlign = TextAlign.Center + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt new file mode 100644 index 00000000..3b4b5a16 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt @@ -0,0 +1,189 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material.icons.rounded.Schedule +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.utilities.AppConstants.Status.ACTIVE +import org.aossie.agoraandroid.utilities.AppConstants.Status.FINISHED +import org.aossie.agoraandroid.utilities.AppConstants.Status.PENDING +import org.aossie.agoraandroid.utilities.ElectionUtils +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrimaryElectionCard(election: ElectionModel, onClick: (String) -> Unit) { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + val formattedStartingDate: Date? = formatter.parse(election.start!!) + val formattedEndingDate: Date? = formatter.parse(election.end!!) + val currentDate = Calendar.getInstance().time + val outFormat = SimpleDateFormat("dd-MM-yyyy 'at' HH:mm:ss", Locale.ENGLISH) + val endDate = outFormat.format(formattedEndingDate!!) + val startDate = outFormat.format(formattedStartingDate!!) + val status = ElectionUtils.getEventStatus(currentDate, formattedStartingDate, formattedEndingDate) + val icon:Int + val iconColor: Color + val colorContainer: Color + val colorContent: Color + + when(status){ + PENDING -> { + icon = R.drawable.ic_election_pending + iconColor = MaterialTheme.colorScheme.error + colorContainer = MaterialTheme.colorScheme.tertiaryContainer + colorContent = MaterialTheme.colorScheme.onTertiaryContainer + } + ACTIVE -> { + icon = R.drawable.ic_election_active + iconColor = Color(0xff00C537) + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + FINISHED -> { + icon = R.drawable.ic_election_finished + iconColor = Color(0xff1877F2) + colorContainer = MaterialTheme.colorScheme.secondaryContainer + colorContent = MaterialTheme.colorScheme.onSecondaryContainer + } + null -> { + icon = R.drawable.ic_election_active + iconColor = MaterialTheme.colorScheme.primary + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + } + Card( + onClick = { onClick.invoke(election._id) }, + colors = CardDefaults.cardColors( + containerColor = colorContainer, + contentColor = colorContent + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(15.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = election.name!!, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + painter = painterResource(id = icon), + tint = iconColor, + contentDescription = "Election Icons") + } + Text( + text = election.description!!, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Column() { + Text( + text = stringResource(id = string.candidates), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + itemsIndexed(election.candidates!!) { index, item -> + AssistChip( + label = { + Text( + text = item, + style = MaterialTheme.typography.titleMedium + ) + }, + onClick = { }, + border = AssistChipDefaults.assistChipBorder( + borderWidth = 1.dp, + borderColor = colorContent + ) + ) + } + } + } + Divider( + color = MaterialTheme.colorScheme.outline, + thickness = 1.dp, + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column() { + TimeItem( + label = stringResource(id = string.start_at), + text = startDate + ) + TimeItem( + label = stringResource(id = string.end_at), + text = endDate + ) + } + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "Navigate Icon") + } + } + } + + +} + +@Composable +fun TimeItem(label: String, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = Rounded.Schedule, contentDescription = "") + Spacer(modifier = Modifier.width(3.dp)) + Text(text = label, fontWeight = FontWeight.SemiBold) + Text(text = text) + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt new file mode 100644 index 00000000..3b1e6882 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt @@ -0,0 +1,83 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import org.aossie.agoraandroid.R.string + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrimaryElectionSearchBar( + searchText: String, + onSearch: (String) -> Unit, + content: @Composable() (ColumnScope.() -> Unit)? = null +) { + var text by remember { mutableStateOf("") } + var active by remember { mutableStateOf(false) } + LaunchedEffect(key1 = searchText){ + if(searchText.isNotEmpty()){ + text = searchText + onSearch(text) + } + } + SearchBar( + modifier = Modifier.fillMaxWidth(), + query = text, + onQueryChange = { + text = it + onSearch(it) + }, + onSearch = { + active = false + onSearch(it) + }, + active = active, + onActiveChange = { + active = it + }, + placeholder = { + Text(text = stringResource(id = string.search_hint)) + }, + leadingIcon = { + Icon( + imageVector = Rounded.Search, + contentDescription = "Search Icons") + }, + trailingIcon = { + if(active){ + IconButton(onClick = { + if(text.isNotEmpty()){ + text = "" + onSearch("") + }else{ + active = false + } + }) { + Icon( + imageVector = Rounded.Close, + contentDescription = "Close Icons") + } + } + }, + content = { + content?.let { it -> + it() + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt new file mode 100644 index 00000000..8c542a7d --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt @@ -0,0 +1,40 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.ElectionModel + +@Composable +fun PrimaryElectionsList( + elections: List, + onItemClicked: (String) -> Unit +) { + if(elections.isNotEmpty()){ + LazyColumn( + verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.Top), + contentPadding = PaddingValues(20.dp) + ) { + itemsIndexed(elections) { index, item -> + PrimaryElectionCard( + election = item, + onClick = onItemClicked + ) + } + } + }else{ + Box(modifier = Modifier.fillMaxSize()) { + Text(text = stringResource(id = string.empty_elections), modifier = Modifier.align(Alignment.Center)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt new file mode 100644 index 00000000..4fcd2960 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt @@ -0,0 +1,129 @@ +package org.aossie.agoraandroid.ui.screens.contactUs + +import androidx.compose.foundation.Image +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.fillMaxSize +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.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.NavigateNext +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.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimarySnackBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContactUsScreen( + message: State, + onBtnGitlabClicked: () -> Unit, + onBtnGitterClicked: () -> Unit, + onBtnReportBugClicked: () -> Unit, + hideSnackBarClick: () -> Unit +) { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentPadding = PaddingValues(vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + painter = painterResource(id = drawable.contact_us), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 230.dp) + ) + } + item { + Text( + text = stringResource(id = string.feel_free_to_contact_us_on_our_gitter_channel_and_work_with_us_on_gitlab), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Divider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + ) + } + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IconTextNavigationButton( + text = stringResource(id = string.gitlab), + iconStart = drawable.ic_gitlab, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnGitlabClicked.invoke() + } + IconTextNavigationButton( + text = stringResource(id = string.gitter), + iconStart = drawable.ic_gitter, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnGitterClicked.invoke() + } + IconTextNavigationButton( + text = stringResource(id = string.report_a_bug), + iconStart = drawable.ic_bug, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnReportBugClicked.invoke() + } + } + } + } + if(message.value.isNotEmpty()){ + PrimarySnackBar(text = message.value) { + hideSnackBarClick.invoke() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt index efbd20c1..cced4a06 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt @@ -59,7 +59,13 @@ fun ElectionDetailsScreen( sheetBackgroundColor = MaterialTheme.colorScheme.background, sheetContentColor = MaterialTheme.colorScheme.onBackground, sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), - sheetContent = ElectionDetailsBottomSheet(onEvent = onEvent,screenState = screenState) + sheetContent = ElectionDetailsBottomSheet( + onEvent = { + coroutineScope.launch { + modalSheetState.hide() + } + onEvent.invoke(it) + }) ) { Box( modifier = Modifier.fillMaxSize() @@ -82,7 +88,6 @@ fun ElectionDetailsScreen( } } } - PrimaryProgressSnackView(screenState = screenState) if(!screenState.loading.second || !screenState.message.second){ FloatingActionButton( modifier = Modifier @@ -98,8 +103,7 @@ fun ElectionDetailsScreen( contentDescription = "") } } - + PrimaryProgressSnackView(screenState = screenState) } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt new file mode 100644 index 00000000..61429f11 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt @@ -0,0 +1,44 @@ +package org.aossie.agoraandroid.ui.screens.elections + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryElectionSearchBar +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryElectionsList +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView + +@Composable +fun ElectionsScreen( + screenState: ScreensState, + elections: List, + searchText: String, + onSearch: (String) -> Unit, + onItemClicked: (String) -> Unit +) { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)){ + Column { + PrimaryElectionSearchBar( + searchText = searchText, + onSearch = onSearch, + content = { + PrimaryElectionsList(elections, onItemClicked) + } + ) + PrimaryElectionsList(elections, onItemClicked) + } + PrimaryProgressSnackView(screenState = screenState) + } + } + +} + diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt new file mode 100644 index 00000000..d2526f17 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt @@ -0,0 +1,213 @@ +package org.aossie.agoraandroid.ui.screens.home + +import androidx.compose.foundation.Image +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FabPosition +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.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.fragments.home.FINISHED_ELECTION_COUNT +import org.aossie.agoraandroid.ui.fragments.home.PENDING_ELECTION_COUNT +import org.aossie.agoraandroid.ui.fragments.home.TOTAL_ELECTION_COUNT +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.CreateElectionClick + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun HomeScreen( + homeScreenDataState: MutableMap?, + screenState: ScreensState, + onClickEvents: (HomeScreenEvents) -> Unit +) { + + var refreshing by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = screenState.loading.second){ + refreshing = screenState.loading.second + } + + val state = rememberPullRefreshState(refreshing, onRefresh = { + onClickEvents(HomeScreenEvents.Refresh) + }) + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + if(!screenState.loading.second){ + ExtendedFloatingActionButton( + text = { + Text(text = stringResource(id = string.Create_Election)) + }, + icon = { + Icon(imageVector = Rounded.Add, contentDescription = "") + }, + onClick = { + onClickEvents(CreateElectionClick) + }) + } + } + ) { + Box(modifier = Modifier + .padding(it) + .pullRefresh(state)){ + LazyColumn( + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + homeScreenDataState?.toList()?.sortedBy { + it.first + }?.let {list -> + itemsIndexed(list){index, item -> + HomeElectionCard( + item = item, + onClick = onClickEvents + ) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeElectionCard(item: Pair, onClick: (HomeScreenEvents) -> Unit) { + var title: String + var icon: Int + val color = when(item.first){ + TOTAL_ELECTION_COUNT -> { + title = stringResource(id = R.string.total_elections) + icon = R.drawable.img_total_election + Pair( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.colorScheme.onSecondaryContainer + ) + } + PENDING_ELECTION_COUNT -> { + title = stringResource(id = R.string.pending_elections) + icon = R.drawable.img_pending_election + Pair( + MaterialTheme.colorScheme.tertiaryContainer, + MaterialTheme.colorScheme.onTertiaryContainer + ) + } + FINISHED_ELECTION_COUNT -> { + title = stringResource(id = R.string.finished_elections) + icon = R.drawable.img_finished_election + Pair( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.onSurface + ) + } + else -> { + title = stringResource(id = R.string.active_elections) + icon = R.drawable.img_active_election + Pair( + MaterialTheme.colorScheme.primaryContainer, + MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Card( + onClick = { + when(item.first){ + TOTAL_ELECTION_COUNT -> { + onClick(HomeScreenEvents.TotalElectionClick) + } + PENDING_ELECTION_COUNT -> { + onClick(HomeScreenEvents.PendingElectionClick) + } + FINISHED_ELECTION_COUNT -> { + onClick(HomeScreenEvents.FinishedElectionClick) + } + else -> { + onClick(HomeScreenEvents.ActiveElectionClick) + } + } + }, + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = color.first, + contentColor = color.second + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 10.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(25.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = "${item.second} $title" , + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(id = R.string.view_details), + style = MaterialTheme.typography.bodyLarge + ) + } + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Image( + painter = painterResource(id = icon), + contentDescription = "", + modifier = Modifier.size(80.dp) + ) + Icon(imageVector = Rounded.NavigateNext, contentDescription = "") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt new file mode 100644 index 00000000..3722e593 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt @@ -0,0 +1,10 @@ +package org.aossie.agoraandroid.ui.screens.home.events + +sealed class HomeScreenEvents{ + object ActiveElectionClick:HomeScreenEvents() + object TotalElectionClick:HomeScreenEvents() + object PendingElectionClick:HomeScreenEvents() + object FinishedElectionClick:HomeScreenEvents() + object CreateElectionClick:HomeScreenEvents() + object Refresh:HomeScreenEvents() +} 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/ui/screens/share/ShareWithOthersScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt new file mode 100644 index 00000000..80482b06 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt @@ -0,0 +1,59 @@ +package org.aossie.agoraandroid.ui.screens.share + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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 org.aossie.agoraandroid.R +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareWithOthersScreen( + onShareClick:() -> Unit +){ + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(25.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + Image( + painter = painterResource(id = R.drawable.share), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .padding(it) + .heightIn(max = 230.dp) + ) + Text( + text = stringResource(id = R.string.share_with_others_to_make_open_source_a_more_beautiful_place), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Light, + ) + PrimaryButton( + text = stringResource(id = R.string.share), + icon = painterResource(id = R.drawable.ic_share) + ) { + onShareClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt new file mode 100644 index 00000000..803d7c7e --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt @@ -0,0 +1,180 @@ +package org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth + +import androidx.compose.foundation.isSystemInDarkTheme +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIos +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.OnBackClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.ResendOtpClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.VerifyOtpClick +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.OtpTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TwoFactorAuthScreen( + progressErrorState: ScreensState, + onEvent: (TwoFactorAuthScreenEvent) -> Unit +) { + + var otp by remember { + mutableStateOf("") + } + var trustDevice by remember { + mutableStateOf(false) + } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + onEvent(OnBackClick) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBackIos, + contentDescription = "" + ) + } + }, + title = {}, + colors = TopAppBarDefaults.largeTopAppBarColors( + navigationIconContentColor = MaterialTheme.colorScheme.onBackground, + containerColor = MaterialTheme.colorScheme.background + ) + ) + } + ) { + + Box(modifier = Modifier.padding(it)) { + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Text( + text = stringResource(id = string.two_factor_authentication), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold + ) + } + item { + Text( + text = stringResource(id = string.please_enter_the_one_time_password_we_have_sent_to_your_registered_email_address), + style = MaterialTheme.typography.bodyMedium + ) + } + + item { + OtpTextField( + otpText = otp, + onOtpTextChange = { value, otpInputFilled -> + otp = value + } + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.didnt_receive_otp), + style = MaterialTheme.typography.bodyLarge + ) + ClickableText( + text = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.inversePrimary)) { + append(stringResource(id = R.string.resend_otp)) + } + }, + style = MaterialTheme.typography.bodyLarge, + onClick = { + onEvent(ResendOtpClick) + } + ) + } + } + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.do_you_trust_this_device), + style = MaterialTheme.typography.titleMedium, + ) + Checkbox( + checked = trustDevice, + onCheckedChange = { + trustDevice = it + } + ) + } + } + item { + PrimaryButton(text = stringResource(id = R.string.verify)) { + onEvent(VerifyOtpClick(otp,trustDevice)) + } + } + } + + PrimaryProgressSnackView(screenState = progressErrorState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt new file mode 100644 index 00000000..a63c18f4 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth + +sealed class TwoFactorAuthScreenEvent { + object OnBackClick: TwoFactorAuthScreenEvent() + object ResendOtpClick: TwoFactorAuthScreenEvent() + data class VerifyOtpClick(val otp: String, val trustedDevice:Boolean): TwoFactorAuthScreenEvent() +} 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/AppUtils.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt index 0812d673..fbedf36e 100644 --- a/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt @@ -13,6 +13,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.datastore.preferences.preferencesDataStore +import androidx.navigation.NavController +import androidx.navigation.NavDirections +import androidx.navigation.NavGraph import com.google.firebase.ktx.Firebase import com.google.firebase.messaging.ktx.messaging import com.takusemba.spotlight.OnSpotlightListener @@ -102,3 +105,18 @@ fun unsubscribeFromFCM(mail: String?) { } } } + +fun NavController.navigateSafely(direction: NavDirections) { + val currentDestination = currentDestination + if (currentDestination != null) { + val navAction = currentDestination.getAction(direction.actionId) + if (navAction != null) { + val destinationId: Int = navAction.destinationId ?: 0 + val currentNode: NavGraph? = + if (currentDestination is NavGraph) currentDestination else currentDestination.parent + if (destinationId != 0 && currentNode != null && currentNode.findNode(destinationId) != null) { + navigate(direction) + } + } + } +} \ No newline at end of file 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/contact_us.png b/app/src/main/res/drawable/contact_us.png new file mode 100644 index 00000000..6390994d Binary files /dev/null and b/app/src/main/res/drawable/contact_us.png differ diff --git a/app/src/main/res/drawable/facebook_logo.xml b/app/src/main/res/drawable/facebook_logo.xml deleted file mode 100644 index f2908cc6..00000000 --- a/app/src/main/res/drawable/facebook_logo.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about.png b/app/src/main/res/drawable/ic_about.png new file mode 100644 index 00000000..c404d797 Binary files /dev/null and b/app/src/main/res/drawable/ic_about.png differ diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml deleted file mode 100644 index 73261f71..00000000 --- a/app/src/main/res/drawable/ic_about.xml +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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_facebook_logo.xml b/app/src/main/res/drawable/ic_facebook_logo.xml new file mode 100644 index 00000000..eb931903 --- /dev/null +++ b/app/src/main/res/drawable/ic_facebook_logo.xml @@ -0,0 +1,16 @@ + + + + + + + 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_privacy_policy.xml b/app/src/main/res/drawable/ic_privacy_policy.xml new file mode 100644 index 00000000..5c76005e --- /dev/null +++ b/app/src/main/res/drawable/ic_privacy_policy.xml @@ -0,0 +1,9 @@ + + + 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_terms_condition.xml b/app/src/main/res/drawable/ic_terms_condition.xml new file mode 100644 index 00000000..1a5d5cd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_terms_condition.xml @@ -0,0 +1,20 @@ + + + + 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/drawable/img_active_election.png b/app/src/main/res/drawable/img_active_election.png new file mode 100644 index 00000000..988e7288 Binary files /dev/null and b/app/src/main/res/drawable/img_active_election.png differ diff --git a/app/src/main/res/drawable/img_finished_election.png b/app/src/main/res/drawable/img_finished_election.png new file mode 100644 index 00000000..7fbed451 Binary files /dev/null and b/app/src/main/res/drawable/img_finished_election.png differ diff --git a/app/src/main/res/drawable/img_pending_election.png b/app/src/main/res/drawable/img_pending_election.png new file mode 100644 index 00000000..c5257743 Binary files /dev/null and b/app/src/main/res/drawable/img_pending_election.png differ diff --git a/app/src/main/res/drawable/img_total_election.png b/app/src/main/res/drawable/img_total_election.png new file mode 100644 index 00000000..7fe50ff4 Binary files /dev/null and b/app/src/main/res/drawable/img_total_election.png differ diff --git a/app/src/main/res/drawable/share.png b/app/src/main/res/drawable/share.png new file mode 100644 index 00000000..9240206d Binary files /dev/null and b/app/src/main/res/drawable/share.png differ 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_about.xml b/app/src/main/res/layout/fragment_about.xml deleted file mode 100644 index 66df23e2..00000000 --- a/app/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_active_elections.xml b/app/src/main/res/layout/fragment_active_elections.xml deleted file mode 100644 index 7a485d9a..00000000 --- a/app/src/main/res/layout/fragment_active_elections.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contact_us.xml b/app/src/main/res/layout/fragment_contact_us.xml deleted file mode 100644 index a06190ba..00000000 --- a/app/src/main/res/layout/fragment_contact_us.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_elections.xml b/app/src/main/res/layout/fragment_elections.xml deleted file mode 100644 index 147e4a48..00000000 --- a/app/src/main/res/layout/fragment_elections.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_finished_elections.xml b/app/src/main/res/layout/fragment_finished_elections.xml deleted file mode 100644 index 11521ff3..00000000 --- a/app/src/main/res/layout/fragment_finished_elections.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_forgot_password.xml b/app/src/main/res/layout/fragment_forgot_password.xml deleted file mode 100644 index 2221f924..00000000 --- a/app/src/main/res/layout/fragment_forgot_password.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index 102cd1ac..00000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,362 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml deleted file mode 100644 index fba9b0cf..00000000 --- a/app/src/main/res/layout/fragment_login.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/layout/fragment_share_with_others.xml b/app/src/main/res/layout/fragment_share_with_others.xml deleted file mode 100644 index d4ff7547..00000000 --- a/app/src/main/res/layout/fragment_share_with_others.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_sign_up.xml b/app/src/main/res/layout/fragment_sign_up.xml deleted file mode 100644 index 1fff5ae2..00000000 --- a/app/src/main/res/layout/fragment_sign_up.xml +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_two_factor_auth.xml b/app/src/main/res/layout/fragment_two_factor_auth.xml deleted file mode 100644 index b770dacf..00000000 --- a/app/src/main/res/layout/fragment_two_factor_auth.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 5ca8968b..a95c9357 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -34,8 +34,7 @@ + android:label="Login"> + android:label="Sing Up" /> + android:label="Forgot Password" /> + android:label="Home"> + android:label="Elections"> + android:label="Settings"> + android:label="Contact Us" /> + android:label="About Agora" /> + android:label="Share with Others" /> + android:label="Active Elections"> + android:label="Finished Elections"> + android:label="Two Factor Authentication"> diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 00000000..09a3bc22 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,330 @@ + + अब वोट करें + फेसबुक के साथ जारी रखें + लॉग इन करें + साइन अप करें + अब जल्द ही + उपयोगकर्ता नाम + पासवर्ड + लॉग इन करें + उपयोगकर्ता लॉगिन + उपयोगकर्ता साइन अप करें + पहला नाम + उपनाम + मेल पता + क्या आपके पास पहले से एक खाता मौजूद है \? + दाखिल करना + पासवर्ड भूल गए \? + प्रोफ़ाइल + डैशबोर्ड + बातचीत करना + एक बग रिपोर्ट करो + दूसरों के साथ साझा करें + अगोरा के बारे में + मेरे निमंत्रण + संपर्क करें + एगोरा वोट एक मतदान मंच है जहां उपयोगकर्ता चुनाव बना सकते हैं और दोस्तों को वोट डालने के लिए आमंत्रित कर सकते हैं। + यह वोटिंग एल्गोरिदम की एक विस्तृत श्रृंखला का समर्थन करता है, जिनमें से कुछ प्रमुख हैं, जैसे कि बहुमत, समतावादी, ऑस्ट्रेलियाई एसटीवी। + हमारे 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 f44d7342..1623c612 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ Agora Vote - CONTINUE WITH FACEBOOK + Continue With Facebook LOG IN Sign Up + Create Account Agora Logo User Name Password @@ -97,11 +98,13 @@ Swipe to delete Candidate Enter Details of Voters Winner Name + Answer Please Write Your Answer Here Security Question Welcome, %1$s! Link Sent, Please Check Your Emails Invalid Username + Invalid Password Something Went Wrong. Please Try Again Later Connection Established Successfully Completely secret and never shown to anyone @@ -325,5 +328,16 @@ Bar Chart Pie Chart Start time is smaller than Current time + Verifying OTP... + Sending OTP... + Didn`t receive OTP? + NewActivity + Authenticating... + 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 29a6e928..25be4524 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { junitVersion = '4.13.2' testRunnerVersion = '1.4.0' espressoCoreVersion = '3.4.0' - lifecycleVersion = '2.6.1 ' + lifecycleVersion = '2.6.1' lifecycleRuntime = '2.3.1' lifecycleExtensions = '2.2.0' materialDesignVersion = '1.9.0' @@ -25,8 +25,7 @@ buildscript { navFragmentVersion = '2.5.3' navUiVersion = '2.5.3' facebookShimmerVersion = '0.5.0' - facebookLoginVersion = '8.2.0' - facebookAndroidSDK = '8.2.0' + facebookAndroidSDK = '16.0.0' robolectricVersion = '4.10.3' mockWebServerVersion = '4.11.0' mockitoCoreVersion = '5.3.1' @@ -53,10 +52,12 @@ buildscript { spotlight = '2.0.5' biometricLibraryVersion = '1.1.0' kotlinCoroutineTest = '1.7.1' - composeBomVersion = '2023.04.01' + composeBomVersion = '2023.05.01' composeActivityVersion = '1.6.1' accompanist_version = '0.31.2-alpha' coilVersion = '2.4.0' + composeLiveData = '1.4.3' + languageLibrary = '1.3.0' } repositories {