From 7a8cad905238cacd2148c59da62b4aeb099c19db Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Sun, 3 Aug 2025 12:40:09 +0530 Subject: [PATCH 1/8] chore: update version to 3.0.1 and modify support contact details --- app/build.gradle | 4 ++-- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 816d9c0..47e6dca 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 41 - versionName "3.0.0" + versionCode 43 + versionName "3.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 844eecd..0bf6f78 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,8 +88,8 @@ con.dscvit.vitty-Updates Directions Facing any issues? Contact us! - We are available on Telegram throughout the day ready to fix your problems! - https://dscv.it/telegram + Reach out support in case of any issues! + https://vitty.dscvit.com Support SJT101 Academics From e1420c5246edcc7a9d18cff1401c83c634ddaa8d Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Mon, 4 Aug 2025 01:59:06 +0530 Subject: [PATCH 2/8] feat: implement maintenance mode with dedicated activity and UI components --- app/src/main/AndroidManifest.xml | 3 + .../com/dscvit/vitty/activity/AuthActivity.kt | 47 ++- .../vitty/activity/MaintenanceActivity.kt | 52 ++++ .../dscvit/vitty/ui/main/MainComposeApp.kt | 36 ++- .../vitty/ui/maintenance/MaintenanceBanner.kt | 270 ++++++++++++++++++ .../vitty/ui/maintenance/MaintenanceScreen.kt | 115 ++++++++ .../dscvit/vitty/util/MaintenanceChecker.kt | 78 +++++ app/src/main/res/drawable/ic_maintenance.xml | 10 + app/src/main/res/drawable/ic_screwdriver.xml | 10 + .../main/res/layout/activity_maintenance.xml | 22 ++ app/src/main/res/values/strings.xml | 8 + 11 files changed, 637 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt create mode 100644 app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt create mode 100644 app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt create mode 100644 app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt create mode 100644 app/src/main/res/drawable/ic_maintenance.xml create mode 100644 app/src/main/res/drawable/ic_screwdriver.xml create mode 100644 app/src/main/res/layout/activity_maintenance.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39cb9bf..9f8938b 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,9 @@ + diff --git a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt index 7882045..a58db1c 100755 --- a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt +++ b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt @@ -10,6 +10,7 @@ import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.databinding.DataBindingUtil @@ -24,6 +25,7 @@ import com.dscvit.vitty.util.Constants.TOKEN import com.dscvit.vitty.util.Constants.UID import com.dscvit.vitty.util.Constants.USER_INFO import com.dscvit.vitty.util.NotificationPermissionHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -62,6 +64,22 @@ class AuthActivity : AppCompatActivity() { configureGoogleSignIn() setupUI() + setupBackPressedHandler() + } + + private fun setupBackPressedHandler() { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + binding.apply { + if (introPager.currentItem == 0 || loginClick) { + finish() + } else { + introPager.currentItem-- + } + } + } + } + onBackPressedDispatcher.addCallback(this, callback) } private fun setupNotificationPermissionLauncher() { @@ -266,8 +284,9 @@ class AuthActivity : AppCompatActivity() { val email = firebaseAuth.currentUser?.email Timber.d("Firebase authentication successful - uid: $uid, email: $email") saveInfo(acct.idToken, uid) - authViewModel.signInAndGetTimeTable("", "", uid ?: "", "") - leadToNextPage() + + // Quick maintenance check for new login + checkMaintenanceBeforeProceed() } else { Timber.e("Firebase authentication failed: ${authResult.exception?.message}") logoutFailed() @@ -278,6 +297,20 @@ class AuthActivity : AppCompatActivity() { } } + private fun checkMaintenanceBeforeProceed() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (isUnderMaintenance) { + binding.loadingView.visibility = View.GONE + val intent = Intent(this, MaintenanceActivity::class.java) + startActivity(intent) + finish() + } else { + authViewModel.signInAndGetTimeTable("", "", firebaseAuth.currentUser?.uid ?: "", "") + leadToNextPage() + } + } + } + private fun leadToNextPage() { authViewModel.signInResponse.observe(this) { if (it != null) { @@ -323,14 +356,4 @@ class AuthActivity : AppCompatActivity() { } } } - - override fun onBackPressed() { - binding.apply { - if (introPager.currentItem == 0 || loginClick) { - super.onBackPressed() - } else { - introPager.currentItem-- - } - } - } } diff --git a/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt new file mode 100644 index 0000000..1b4fef1 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt @@ -0,0 +1,52 @@ +package com.dscvit.vitty.activity + +import android.content.Intent +import android.os.Bundle +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentActivity +import com.dscvit.vitty.R +import com.dscvit.vitty.databinding.ActivityMaintenanceBinding +import com.dscvit.vitty.theme.VittyTheme +import com.dscvit.vitty.ui.maintenance.MaintenanceScreen +import com.dscvit.vitty.util.MaintenanceChecker + +class MaintenanceActivity : FragmentActivity() { + + private lateinit var binding: ActivityMaintenanceBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_maintenance) + + binding.composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@MaintenanceActivity) + ) + setContent { + VittyTheme { + MaintenanceScreen( + onRetryClick = { retryConnection() }, + onExitClick = { exitApp() } + ) + } + } + } + } + + private fun retryConnection() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (!isUnderMaintenance) { + val intent = Intent(this, InstructionsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + } + } + } + + private fun exitApp() { + finishAffinity() + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 0437040..5fc1a86 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -103,8 +103,10 @@ import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Analytics import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.LogoutHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.dscvit.vitty.util.SemesterUtils import com.dscvit.vitty.util.UtilFunctions +import com.dscvit.vitty.ui.maintenance.MaintenanceBannerDialog import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.net.URLDecoder @@ -129,6 +131,10 @@ fun MainComposeApp() { var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } var showCampusDialog by remember { mutableStateOf(campus.isEmpty()) } + + // Maintenance banner state + var showMaintenanceBanner by remember { mutableStateOf(false) } + var lastMaintenanceCheck by remember { mutableStateOf(0L) } DisposableEffect(Unit) { val listener = @@ -208,13 +214,25 @@ fun MainComposeApp() { } } + LaunchedEffect(Unit) { + if (MaintenanceChecker.isNetworkAvailable(context)) { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + android.util.Log.d("MaintenanceCheck", "Dialog check result: isUnderMaintenance=$isUnderMaintenance") + showMaintenanceBanner = isUnderMaintenance + lastMaintenanceCheck = System.currentTimeMillis() + } + } else { + android.util.Log.d("MaintenanceCheck", "No network available") + } + } + VittyTheme { ModalNavigationDrawer( drawerState = drawerState, drawerContent = { DrawerContent( navController = navController, - onCloseDrawer = { scope.launch { drawerState.close() } }, + onCloseDrawer = { scope.launch { drawerState.close() } } ) }, ) { @@ -866,6 +884,20 @@ fun MainComposeApp() { onDismiss = { showCampusDialog = false }, ) } + + // Maintenance Banner Dialog + MaintenanceBannerDialog( + isVisible = showMaintenanceBanner, + onDismiss = { + showMaintenanceBanner = false + }, + onRetryClick = { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + showMaintenanceBanner = isUnderMaintenance + lastMaintenanceCheck = System.currentTimeMillis() + } + } + ) } } } @@ -1091,7 +1123,7 @@ fun NavigationItem( @Composable fun DrawerContent( navController: NavHostController, - onCloseDrawer: () -> Unit, + onCloseDrawer: () -> Unit ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt new file mode 100644 index 0000000..161adc5 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt @@ -0,0 +1,270 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceBannerDialog( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + if (isVisible) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = Background + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header with close button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Maintenance icon with gradient background + Box( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + brush = Brush.radialGradient( + colors = listOf( + Accent.copy(alpha = 0.2f), + Accent.copy(alpha = 0.1f) + ) + ) + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(40.dp), + tint = Accent + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = "Server Maintenance", + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = TextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Message + Text( + text = "The server is currently under maintenance. Some features may be temporarily unavailable.", + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 18.sp + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Retry button + Button( + onClick = onRetryClick, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Retry", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + + // Dismiss button + OutlinedButton( + onClick = onDismiss, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Accent.copy(alpha = 0.8f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Continue", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Footer + Text( + text = "Thank you for your patience", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +fun MaintenanceBanner( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { -it } + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { -it } + ) + fadeOut() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Secondary.copy(alpha = 0.95f) + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(24.dp), + tint = Accent + ) + + Spacer(modifier = Modifier.width(12.dp)) + + // Text content + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "Server Maintenance", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Accent + ) + Text( + text = "Some features may be limited", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + // Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + onClick = onRetryClick, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = "Retry", + fontSize = 12.sp, + color = Accent, + fontWeight = FontWeight.Medium + ) + } + + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt new file mode 100644 index 0000000..429d00d --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt @@ -0,0 +1,115 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +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.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceScreen( + onRetryClick: () -> Unit, + onExitClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = stringResource(R.string.maintenance_icon_desc), + modifier = Modifier.size(120.dp), + colorFilter = ColorFilter.tint(Accent) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_title), + fontSize = 28.sp, + fontWeight = FontWeight.SemiBold, + color = TextColor, + textAlign = TextAlign.Center, + lineHeight = 36.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.maintenance_message), + fontSize = 16.sp, + color = Accent, + textAlign = TextAlign.Center, + lineHeight = 20.sp + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.maintenance_description), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 16.sp + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Button( + onClick = onRetryClick, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.retry_connection), + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = onExitClick + ) { + Text( + text = stringResource(R.string.exit_app), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_footer), + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt new file mode 100644 index 0000000..d492282 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt @@ -0,0 +1,78 @@ +package com.dscvit.vitty.util + +import android.content.Context +import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.util.concurrent.TimeUnit + +object MaintenanceChecker { + + private const val MAINTENANCE_CHECK_TIMEOUT = 5L + private const val API_OPERATIONAL_MESSAGE = "Welcome to VITTY API!🎉" + + private var isChecking = false + + fun checkMaintenanceStatusAsync( + context: Context, + onResult: (isUnderMaintenance: Boolean) -> Unit + ) { + if (isChecking) return + + isChecking = true + + CoroutineScope(Dispatchers.IO).launch { + val isUnderMaintenance = try { + val client = OkHttpClient.Builder() + .connectTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .callTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .build() + + val request = Request.Builder() + .url(COMMUNITY_BASE_URL) + .build() + + val response = client.newCall(request).execute() + val responseBody = response.body?.string() + + val result = when { + !response.isSuccessful -> { + Timber.d("Server returned error code: ${response.code}") + true // Server error = maintenance + } + responseBody == null -> { + Timber.d("No response body received") + true // No response = maintenance + } + responseBody.contains(API_OPERATIONAL_MESSAGE) -> { + Timber.d("API operational message found") + false // API working + } + else -> { + Timber.d("Different response received: $responseBody") + true // Different response = maintenance + } + } + + Timber.d("Maintenance check: isUnderMaintenance=$result, response=$responseBody") + result + + } catch (e: Exception) { + Timber.e(e, "Maintenance check failed") + false // Network error = don't assume maintenance, just fail silently + } + + withContext(Dispatchers.Main) { + isChecking = false + onResult(isUnderMaintenance) + } + } + } + + fun isNetworkAvailable(context: Context): Boolean { + return UtilFunctions.isNetworkAvailable(context) + } +} diff --git a/app/src/main/res/drawable/ic_maintenance.xml b/app/src/main/res/drawable/ic_maintenance.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_maintenance.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_screwdriver.xml b/app/src/main/res/drawable/ic_screwdriver.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_screwdriver.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_maintenance.xml b/app/src/main/res/layout/activity_maintenance.xml new file mode 100644 index 0000000..3475e7a --- /dev/null +++ b/app/src/main/res/layout/activity_maintenance.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bf6f78..882d931 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,4 +132,12 @@ Hello blank fragment + + Server Under Maintenance + We\'re currently performing server maintenance to improve your experience. + Please check back in a few minutes. We\'ll be back online soon! + Try Again + Exit App + Thank you for your patience + Maintenance Icon From 515785d6159fd215b2ae74a02a88aabaacecd2d6 Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Mon, 4 Aug 2025 12:19:06 +0530 Subject: [PATCH 3/8] refactor: server status check functionality and listener interface --- .../network/api/community/APICommunity.kt | 3 + .../api/community/APICommunityRestClient.kt | 26 +++++ .../community/RetrofitCommunityListener.kt | 12 +++ .../dscvit/vitty/ui/main/MainComposeApp.kt | 10 +- .../dscvit/vitty/util/MaintenanceChecker.kt | 99 ++++++++----------- 5 files changed, 84 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index bbc5bfe..f3a0380 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -29,6 +29,9 @@ import retrofit2.http.Path import retrofit2.http.Query interface APICommunity { + @GET("/") + fun checkServerStatus(): Call + @Headers("Content-Type: application/json") @POST("/api/v3/auth/check-username") fun checkUsername( diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index d9ef161..5ef302a 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -1050,4 +1050,30 @@ class APICommunityRestClient { }, ) } + + fun checkServerStatus( + retrofitServerStatusListener: RetrofitServerStatusListener, + ) { + mApiUser = retrofit.create(APICommunity::class.java) + val apiServerStatusCall = mApiUser!!.checkServerStatus() + apiServerStatusCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("ServerStatus: ${response.body()}") + retrofitServerStatusListener.onSuccess(call, response.body(), response.isSuccessful) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("ServerStatusError: ${t.message}") + retrofitServerStatusListener.onError(call, t) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt index 41ab0b8..f178876 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt @@ -169,3 +169,15 @@ interface RetrofitActiveFriendsListener { t: Throwable, ) } + +interface RetrofitServerStatusListener { + fun onSuccess( + call: Call, + response: String?, + isSuccessful: Boolean + ) + fun onError( + call: Call, + t: Throwable + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 5fc1a86..58d4ad1 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -115,6 +115,7 @@ import java.nio.charset.StandardCharsets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber @Composable fun MainComposeApp() { @@ -134,7 +135,6 @@ fun MainComposeApp() { // Maintenance banner state var showMaintenanceBanner by remember { mutableStateOf(false) } - var lastMaintenanceCheck by remember { mutableStateOf(0L) } DisposableEffect(Unit) { val listener = @@ -215,14 +215,13 @@ fun MainComposeApp() { } LaunchedEffect(Unit) { - if (MaintenanceChecker.isNetworkAvailable(context)) { + if (UtilFunctions.isNetworkAvailable(context)) { MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> - android.util.Log.d("MaintenanceCheck", "Dialog check result: isUnderMaintenance=$isUnderMaintenance") + Timber.d("Dialog check result: isUnderMaintenance=%s", isUnderMaintenance) showMaintenanceBanner = isUnderMaintenance - lastMaintenanceCheck = System.currentTimeMillis() } } else { - android.util.Log.d("MaintenanceCheck", "No network available") + Timber.d("No network available") } } @@ -894,7 +893,6 @@ fun MainComposeApp() { onRetryClick = { MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> showMaintenanceBanner = isUnderMaintenance - lastMaintenanceCheck = System.currentTimeMillis() } } ) diff --git a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt index d492282..e03b6f3 100644 --- a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt +++ b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt @@ -1,78 +1,57 @@ package com.dscvit.vitty.util import android.content.Context -import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL -import kotlinx.coroutines.* -import okhttp3.OkHttpClient -import okhttp3.Request +import com.dscvit.vitty.network.api.community.APICommunityRestClient +import com.dscvit.vitty.network.api.community.RetrofitServerStatusListener +import retrofit2.Call import timber.log.Timber -import java.util.concurrent.TimeUnit object MaintenanceChecker { - - private const val MAINTENANCE_CHECK_TIMEOUT = 5L + private const val API_OPERATIONAL_MESSAGE = "Welcome to VITTY API!🎉" - private var isChecking = false - + fun checkMaintenanceStatusAsync( context: Context, onResult: (isUnderMaintenance: Boolean) -> Unit ) { if (isChecking) return - + isChecking = true - - CoroutineScope(Dispatchers.IO).launch { - val isUnderMaintenance = try { - val client = OkHttpClient.Builder() - .connectTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .callTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .build() - - val request = Request.Builder() - .url(COMMUNITY_BASE_URL) - .build() - - val response = client.newCall(request).execute() - val responseBody = response.body?.string() - - val result = when { - !response.isSuccessful -> { - Timber.d("Server returned error code: ${response.code}") - true // Server error = maintenance - } - responseBody == null -> { - Timber.d("No response body received") - true // No response = maintenance - } - responseBody.contains(API_OPERATIONAL_MESSAGE) -> { - Timber.d("API operational message found") - false // API working - } - else -> { - Timber.d("Different response received: $responseBody") - true // Different response = maintenance + + APICommunityRestClient.instance.checkServerStatus( + object : RetrofitServerStatusListener { + override fun onSuccess(call: Call, response: String?, isSuccessful: Boolean) { + val result = when { + !isSuccessful -> { + Timber.d("Server returned error") + true // Server error = maintenance + } + response == null -> { + Timber.d("No response body received") + true // No response = maintenance + } + response.contains(API_OPERATIONAL_MESSAGE) -> { + Timber.d("API operational message found") + false // API working + } + else -> { + Timber.d("Different response received: $response") + true // Different response = maintenance + } } + + Timber.d("Maintenance check: isUnderMaintenance=$result, response=$response") + isChecking = false + onResult(result) + } + + override fun onError(call: Call, t: Throwable) { + Timber.e(t, "Maintenance check failed") + isChecking = false + onResult(false) // Network error = don't assume maintenance } - - Timber.d("Maintenance check: isUnderMaintenance=$result, response=$responseBody") - result - - } catch (e: Exception) { - Timber.e(e, "Maintenance check failed") - false // Network error = don't assume maintenance, just fail silently - } - - withContext(Dispatchers.Main) { - isChecking = false - onResult(isUnderMaintenance) } - } - } - - fun isNetworkAvailable(context: Context): Boolean { - return UtilFunctions.isNetworkAvailable(context) + ) } -} +} \ No newline at end of file From 9050224a787c0c7e939fc223514a88d12af531cc Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Tue, 5 Aug 2025 11:19:13 +0530 Subject: [PATCH 4/8] fix: add backend time string parsing utility and update timestamp handling in TodayWidget --- app/build.gradle | 2 +- .../com/dscvit/vitty/util/UtilFunctions.kt | 29 +++++++++++++++++++ .../com/dscvit/vitty/widget/TodayWidget.kt | 22 +++----------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 47e6dca..20d3f6d 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 43 + versionCode 44 versionName "3.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release diff --git a/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt b/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt index eaab2ef..94fbd41 100755 --- a/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt +++ b/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt @@ -23,6 +23,7 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale +import java.util.TimeZone object UtilFunctions { fun openLink( @@ -144,4 +145,32 @@ object UtilFunctions { intent.putExtra(Intent.EXTRA_STREAM, bitmapUri) context.startActivity(Intent.createChooser(intent, "Share")) } + + fun parseBackendTimeString(timeString: String): Date? { + try { + val timeRegex = """T(\d{2}):(\d{2}):(\d{2})""".toRegex() + val match = timeRegex.find(timeString) + + if (match != null) { + val hours = match.groupValues[1].toInt() + val minutes = match.groupValues[2].toInt() + val seconds = match.groupValues[3].toInt() + + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hours) + calendar.set(Calendar.MINUTE, minutes) + calendar.set(Calendar.SECOND, seconds) + calendar.set(Calendar.MILLISECOND, 0) + + return calendar.time + } else { + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.Builder().setLanguage("en").setRegion("IN").build()) + sdf.timeZone = TimeZone.getTimeZone("Asia/Kolkata") + return sdf.parse(timeString) + } + } catch (e: Exception) { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Kolkata")) + return calendar.time + } + } } diff --git a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt index 218a58b..844214d 100755 --- a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt +++ b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt @@ -306,7 +306,7 @@ suspend fun fetchTodayData( var endTime = parseTimeToTimestamp(period.end_time).toDate() val simpleDateFormat = - SimpleDateFormat("h:mm a", Locale.getDefault()) + SimpleDateFormat("h:mm a", Locale("en", "IN")) val sTime: String = simpleDateFormat.format(startTime).uppercase(Locale.ROOT) val eTime: String = @@ -347,20 +347,11 @@ suspend fun fetchTodayData( fun parseTimeToTimestamp(timeString: String): Timestamp = try { - val sanitizedTime = - if (timeString.contains("+05:53")) { - timeString.replace("+05:53", "+05:30") - } else { - timeString - } - val time = replaceYearIfZero(sanitizedTime) - - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()) - val date = dateFormat.parse(time) + val date = UtilFunctions.parseBackendTimeString(timeString) if (date != null) { Timestamp(date) } else { - Timber.d("Date parsing error: Unable to parse sanitized time: $time") + Timber.d("Date parsing error: Unable to parse time: $timeString") Timestamp.now() } } catch (e: Exception) { @@ -368,9 +359,4 @@ fun parseTimeToTimestamp(timeString: String): Timestamp = Timestamp.now() } -fun replaceYearIfZero(dateStr: String): String = - if (dateStr.startsWith("0")) { - "2023" + dateStr.substring(4) - } else { - dateStr - } + From 1c5b675202f97c76fdb7628f9c7ec9905df172d9 Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Wed, 6 Aug 2025 23:12:38 +0530 Subject: [PATCH 5/8] fix: update API endpoint for fetching empty classrooms --- .../java/com/dscvit/vitty/network/api/community/APICommunity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index f3a0380..c15c6db 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -214,7 +214,7 @@ interface APICommunity { @Path("username") username: String, ): Call - @GET("/api/v3/timetable/emptyClassRooms") + @GET("/api/v3/users/emptyClassRooms") fun getEmptyClassrooms( @Header("Authorization") authToken: String, @Query("slot") slot: String, From e349a807f2f252c9afed4a04533638284aa1998a Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Wed, 6 Aug 2025 23:12:58 +0530 Subject: [PATCH 6/8] feat: add support reporting feature with email integration --- .../emptyclassrooms/EmptyClassroomsContent.kt | 252 ++++++++++++++++ .../dscvit/vitty/ui/main/MainComposeApp.kt | 268 +++++++++++++++++- 2 files changed, 510 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt index 2c97b80..0ded184 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt @@ -1,5 +1,8 @@ package com.dscvit.vitty.ui.emptyclassrooms +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -7,12 +10,15 @@ 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.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.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -25,6 +31,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -33,8 +42,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -73,6 +84,8 @@ fun EmptyClassroomsContent( var isLoading by remember { mutableStateOf(true) } var selectedSlot by remember { mutableStateOf("A1") } var errorMessage by remember { mutableStateOf(null) } + var showReportDialog by remember { mutableStateOf(false) } + var selectedClassroomForReport by remember { mutableStateOf(null) } val regularSlots = listOf( @@ -153,6 +166,17 @@ fun EmptyClassroomsContent( } }, actions = { + Box { + IconButton(onClick = { + showReportDialog = true + }) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Report Incorrect Data", + tint = TextColor, + ) + } + } IconButton(onClick = { fetchEmptyClassrooms(selectedSlot) }) { Icon( imageVector = Icons.Default.Refresh, @@ -296,6 +320,20 @@ fun EmptyClassroomsContent( } } } + + // Report Dialog + if (showReportDialog) { + ReportIncorrectDataDialog( + availableClassrooms = emptyClassrooms, + selectedSlot = selectedSlot, + context = context, + prefs = prefs, + onDismiss = { + showReportDialog = false + selectedClassroomForReport = null + } + ) + } } @Composable @@ -377,3 +415,217 @@ private fun ClassroomCard(classroom: String) { } } } + +@Composable +private fun ReportIncorrectDataDialog( + availableClassrooms: List, + selectedSlot: String, + context: Context, + prefs: android.content.SharedPreferences, + onDismiss: () -> Unit +) { + val username = prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + val name = prefs.getString(Constants.COMMUNITY_NAME, "") ?: "" + val campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Unknown" + + var selectedClassroom by remember { mutableStateOf(null) } + + val currentDate = remember { + java.text.SimpleDateFormat("MMM dd, yyyy 'at' hh:mm a", java.util.Locale.getDefault()) + .format(java.util.Date()) + } + + fun openEmailClient(classroom: String) { + val emailBody = """ +Dear VITTY Support Team, + +I would like to report incorrect classroom data: + +REPORT DETAILS: +- Reported Classroom: $classroom +- Time Slot: $selectedSlot +- Date & Time: $currentDate +- Campus: ${campus.capitalize()} + +USER INFORMATION: +- Username: $username +- Name: $name + +ISSUE DESCRIPTION: +The classroom "$classroom" is listed as empty for slot $selectedSlot, but it appears to be occupied or incorrectly marked. + +Please verify and update the classroom availability data. + +Thank you for your attention to this matter. + +Best regards, +$name +VITTY Android App + """.trimIndent() + + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_EMAIL, arrayOf("dscvit.vitty@gmail.com")) + putExtra(Intent.EXTRA_SUBJECT, "Report Incorrect Classroom Data - $classroom ($selectedSlot)") + putExtra(Intent.EXTRA_TEXT, emailBody) + } + + try { + context.startActivity(Intent.createChooser(emailIntent, "Send Email")) + onDismiss() + } catch (e: Exception) { + // Fallback if no email client available + val fallbackIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, emailBody) + } + context.startActivity(Intent.createChooser(fallbackIntent, "Share Report")) + onDismiss() + } + } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = Accent, + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = "Report Incorrect Data", + style = MaterialTheme.typography.headlineSmall, + color = TextColor, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + }, + text = { + Column { + Text( + text = "Select the classroom that is incorrectly marked as empty for slot $selectedSlot", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Select Classroom:", + style = MaterialTheme.typography.titleSmall, + color = TextColor, + fontWeight = FontWeight.SemiBold + ) + + Spacer(modifier = Modifier.height(12.dp)) + + if (availableClassrooms.isEmpty()) { + Text( + text = "No classrooms available for slot $selectedSlot", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center + ) + } else { + LazyColumn( + modifier = Modifier.height(200.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(availableClassrooms) { classroom -> + ClassroomSelectionItem( + classroom = classroom, + isSelected = selectedClassroom == classroom, + onClick = { selectedClassroom = classroom } + ) + } + } + } + } + }, + confirmButton = { + Button( + onClick = { + selectedClassroom?.let { classroom -> + openEmailClient(classroom) + } + }, + enabled = selectedClassroom != null, + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background, + disabledContainerColor = Accent.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Send Report", + fontWeight = FontWeight.SemiBold + ) + } + }, + dismissButton = { + OutlinedButton( + onClick = onDismiss, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = TextColor + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Cancel", + fontWeight = FontWeight.Medium + ) + } + }, + containerColor = Secondary, + titleContentColor = TextColor, + textContentColor = Accent + ) +} + +@Composable +private fun ClassroomSelectionItem( + classroom: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() }, + colors = CardDefaults.cardColors( + containerColor = if (isSelected) Accent.copy(alpha = 0.2f) else Background + ), + shape = RoundedCornerShape(8.dp), + border = if (isSelected) BorderStroke(2.dp, Accent) else null + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background( + if (isSelected) Accent else TextColor.copy(alpha = 0.3f), + CircleShape + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = classroom, + style = MaterialTheme.typography.bodyMedium, + color = TextColor, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 2593f15..e8fcc1c 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -3,6 +3,7 @@ package com.dscvit.vitty.ui.main import android.app.Activity import android.content.Intent import android.content.SharedPreferences +import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -30,6 +31,7 @@ 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.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -44,6 +46,10 @@ import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -62,6 +68,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -1132,6 +1139,7 @@ fun DrawerContent( val name = remember { prefs.getString(Constants.COMMUNITY_NAME, "") ?: "Name" } var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } + var showSupportDialog by remember { mutableStateOf(false) } DisposableEffect(Unit) { val listener = @@ -1216,15 +1224,39 @@ fun DrawerContent( ) }, label = { - Text( - modifier = Modifier.padding(start = 24.dp), - text = "Find Empty Classroom", - color = TextColor, - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, - ), - ) + Row( + modifier = Modifier.padding(start = 24.dp).fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Find Empty Classroom", + color = TextColor, + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier + .background( + Accent, + RoundedCornerShape(8.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = "BETA", + color = Background, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.ExtraBold + ), + fontSize = 12.sp + ) + } + } }, selected = false, onClick = { @@ -1237,6 +1269,11 @@ fun DrawerContent( selectedContainerColor = Secondary, ), ) + } else { + Timber.d("DrawerContent: ❌ HIDING Empty Classroom option") + Timber.d("DrawerContent: Campus '$campus' is not 'vellore'") + Timber.d("DrawerContent: Campus is empty: ${campus.isEmpty()}") + Timber.d("DrawerContent: Campus is blank: ${campus.isBlank()}") } NavigationDrawerItem( @@ -1338,7 +1375,7 @@ fun DrawerContent( selected = false, onClick = { onCloseDrawer() - UtilFunctions.openLink(context, context.getString(R.string.telegram_link)) + showSupportDialog = true }, colors = NavigationDrawerItemDefaults.colors( @@ -1387,6 +1424,15 @@ fun DrawerContent( ) } } + + // Support Dialog + if (showSupportDialog) { + SupportDialog( + context = context, + prefs = prefs, + onDismiss = { showSupportDialog = false } + ) + } } private suspend fun loadCachedCourses(prefs: SharedPreferences): List = @@ -1444,3 +1490,205 @@ private fun extractCoursesFromTimetable(userResponse: UserResponse): List Unit +) { + val username = prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + val name = prefs.getString(Constants.COMMUNITY_NAME, "") ?: "" + val campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Unknown" + + val deviceInfo = remember { + """ + **Device Information:** + - Device: ${Build.MODEL} + - Manufacturer: ${Build.MANUFACTURER} + - OS: Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) + - App Version: ${context.packageManager.getPackageInfo(context.packageName, 0).versionName} + + **User Information:** + - Username: $username + - Name: $name + - Campus: $campus + """.trimIndent() + } + + fun openEmailSupport() { + val emailTemplate = """ +Dear VITTY Support Team, + +I am experiencing an issue with the VITTY Android app and would like to report it. + +$deviceInfo + +**Describe the bug** +[Please provide a clear and concise description of what the bug is] + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behaviour** +[Please provide a clear and concise description of what you expected to happen] + +**Screenshots** +[If applicable, please attach screenshots to help explain your problem] + +**Additional context** +[Add any other context about the problem here] + +Thank you for your time and assistance! + +Best regards, +$name +VITTY Android App User + """.trimIndent() + + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_EMAIL, arrayOf("dscvit.vitty@gmail.com")) + putExtra(Intent.EXTRA_SUBJECT, "Bug Report - VITTY Android v${context.packageManager.getPackageInfo(context.packageName, 0).versionName}") + putExtra(Intent.EXTRA_TEXT, emailTemplate) + } + + try { + context.startActivity(Intent.createChooser(emailIntent, "Send Bug Report")) + onDismiss() + } catch (e: Exception) { + // Fallback if no email client available + val fallbackIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, emailTemplate) + } + context.startActivity(Intent.createChooser(fallbackIntent, "Share Bug Report")) + onDismiss() + } + } + + fun openGitHub() { + UtilFunctions.openLink(context, "https://github.com/GDGVIT/vitty-app/issues") + onDismiss() + } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_support), + contentDescription = null, + tint = Accent, + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = "Get Support", + style = MaterialTheme.typography.headlineSmall, + color = TextColor, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Troubleshooting Steps:", + style = MaterialTheme.typography.titleMedium, + color = TextColor, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val troubleshootingSteps = listOf( + "1. Update the app from Play Store", + "2. Log out and log back in", + "3. Clear app cache (Settings > Apps > Vitty > Storage)" + ) + + troubleshootingSteps.forEach { step -> + Text( + text = step, + style = MaterialTheme.typography.bodySmall, + color = TextColor.copy(alpha = 0.8f), + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(6.dp)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Still need help?", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + fontWeight = FontWeight.SemiBold + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Email Support Button + OutlinedButton( + onClick = { openEmailSupport() }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Accent + ), + border = BorderStroke(1.dp, Accent), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Email Support", + fontWeight = FontWeight.Medium, + fontSize = 16.sp + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // GitHub Issues Button + OutlinedButton( + onClick = { openGitHub() }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = TextColor + ), + border = BorderStroke(1.dp, TextColor.copy(alpha = 0.3f)), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "GitHub Issues", + fontWeight = FontWeight.Medium, + fontSize = 16.sp + ) + } + } + }, + confirmButton = {}, + dismissButton = { + OutlinedButton( + onClick = onDismiss, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = TextColor + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Close", + fontWeight = FontWeight.Medium + ) + } + }, + containerColor = Secondary, + titleContentColor = TextColor, + textContentColor = Accent + ) +} From 9043f0e5206eebb859d70bc1132552b98eb68b42 Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Wed, 6 Aug 2025 23:29:54 +0530 Subject: [PATCH 7/8] ui: add overlay to report incorrect data dialog for improved user interaction --- .../emptyclassrooms/EmptyClassroomsContent.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt index 0ded184..f82d7df 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt @@ -45,7 +45,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -56,6 +55,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -429,6 +429,7 @@ private fun ReportIncorrectDataDialog( val campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Unknown" var selectedClassroom by remember { mutableStateOf(null) } + var showOverlay by remember { mutableStateOf(true) } val currentDate = remember { java.text.SimpleDateFormat("MMM dd, yyyy 'at' hh:mm a", java.util.Locale.getDefault()) @@ -484,8 +485,22 @@ VITTY Android App } } - AlertDialog( - onDismissRequest = onDismiss, + if (showOverlay) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Background.copy(alpha = 0.5f)) + .clickable { + showOverlay = false + onDismiss() + }, + contentAlignment = Alignment.Center + ) { + AlertDialog( + onDismissRequest = { + showOverlay = false + onDismiss() + }, icon = { Icon( imageVector = Icons.Default.Warning, @@ -585,6 +600,8 @@ VITTY Android App titleContentColor = TextColor, textContentColor = Accent ) + } + } } @Composable From 8a1f07e25ec61c5c42fd3b18977c96cfc332e8ce Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Thu, 7 Aug 2025 01:26:03 +0530 Subject: [PATCH 8/8] fix+chore: alignment of beta tag, inc version --- app/build.gradle | 4 +- .../dscvit/vitty/ui/main/MainComposeApp.kt | 74 ++++++++++--------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 20d3f6d..1ad2ab9 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 44 - versionName "3.0.1" + versionCode 45 + versionName "3.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index e8fcc1c..a072f9f 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -1224,10 +1224,8 @@ fun DrawerContent( ) }, label = { - Row( - modifier = Modifier.padding(start = 24.dp).fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + Column( + modifier = Modifier.padding(start = 24.dp) ) { Text( text = "Find Empty Classroom", @@ -1236,24 +1234,23 @@ fun DrawerContent( MaterialTheme.typography.labelLarge.copy( fontWeight = FontWeight.Normal, ), - modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) Box( modifier = Modifier .background( Accent, - RoundedCornerShape(8.dp) + RoundedCornerShape(6.dp) ) - .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text( text = "BETA", color = Background, - style = MaterialTheme.typography.labelMedium.copy( + style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.ExtraBold ), - fontSize = 12.sp + fontSize = 10.sp ) } } @@ -1606,7 +1603,7 @@ VITTY Android App User fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) val troubleshootingSteps = listOf( "1. Update the app from Play Store", @@ -1617,73 +1614,80 @@ VITTY Android App User troubleshootingSteps.forEach { step -> Text( text = step, - style = MaterialTheme.typography.bodySmall, - color = TextColor.copy(alpha = 0.8f), - fontSize = 14.sp + style = MaterialTheme.typography.bodyMedium, + color = TextColor.copy(alpha = 0.9f), + fontSize = 15.sp, + lineHeight = 20.sp ) - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(20.dp)) Text( text = "Still need help?", - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.titleSmall, color = Accent, fontWeight = FontWeight.SemiBold ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(16.dp)) // Email Support Button - OutlinedButton( + Button( onClick = { openEmailSupport() }, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = Accent + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background ), - border = BorderStroke(1.dp, Accent), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(12.dp), + elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp) ) { Text( text = "Email Support", - fontWeight = FontWeight.Medium, - fontSize = 16.sp + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 4.dp) ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) // GitHub Issues Button OutlinedButton( onClick = { openGitHub() }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.outlinedButtonColors( - contentColor = TextColor + contentColor = Accent ), - border = BorderStroke(1.dp, TextColor.copy(alpha = 0.3f)), - shape = RoundedCornerShape(8.dp) + border = BorderStroke(2.dp, Accent), + shape = RoundedCornerShape(12.dp) ) { Text( text = "GitHub Issues", - fontWeight = FontWeight.Medium, - fontSize = 16.sp + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 4.dp) ) } } }, confirmButton = {}, dismissButton = { - OutlinedButton( + Button( onClick = onDismiss, - colors = ButtonDefaults.outlinedButtonColors( + colors = ButtonDefaults.buttonColors( + containerColor = Secondary, contentColor = TextColor ), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, TextColor.copy(alpha = 0.2f)) ) { Text( text = "Close", - fontWeight = FontWeight.Medium + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp ) } },