Skip to content

Commit 1bba3bc

Browse files
committed
Time Table added, turn on notification
1 parent 877d5fb commit 1bba3bc

10 files changed

Lines changed: 805 additions & 616 deletions

File tree

app/src/main/java/com/danycli/assignmentchecker/MainActivity.kt

Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ private fun AppEntry() {
8383
var showSplash by remember { mutableStateOf(true) }
8484
var startupCredentials by remember { mutableStateOf<Pair<String, String>?>(null) }
8585
var appSettings by remember { mutableStateOf(AppSettingsStore.get(context)) }
86+
var showNotificationDialog by remember { mutableStateOf(false) }
87+
88+
// Permission launcher for initial prompt
89+
val permissionLauncher = rememberLauncherForActivityResult(
90+
ActivityResultContracts.RequestPermission()
91+
) { granted ->
92+
NotificationPromptStore.markInitialPromptShown(context)
93+
// If denied, we'll handle it via the daily re-prompt
94+
}
8695

8796
LaunchedEffect(Unit) {
8897
val splashStart = System.currentTimeMillis()
@@ -97,6 +106,28 @@ private fun AppEntry() {
97106
showSplash = false
98107
}
99108

109+
// Handle notification prompt logic after splash
110+
LaunchedEffect(showSplash) {
111+
if (showSplash) return@LaunchedEffect
112+
113+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
114+
if (!NotificationPromptStore.hasShownInitialPrompt(context)) {
115+
// First launch: request permission directly
116+
permissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
117+
return@LaunchedEffect
118+
}
119+
}
120+
121+
// Track daily opens and check for re-prompt
122+
val dailyCount = NotificationPromptStore.incrementDailyOpen(context)
123+
if (dailyCount >= 3 &&
124+
!NotificationGate.areNotificationsEnabled(context) &&
125+
!NotificationPromptStore.hasDailyPromptBeenShown(context)
126+
) {
127+
showNotificationDialog = true
128+
}
129+
}
130+
100131
AssignmentCheckerTheme(themeMode = appSettings.themeMode) {
101132
if (showSplash) {
102133
AppSplashScreen()
@@ -109,9 +140,68 @@ private fun AppEntry() {
109140
onSettingsChange = { appSettings = it }
110141
)
111142
}
143+
144+
// Notification re-prompt dialog
145+
if (showNotificationDialog) {
146+
AlertDialog(
147+
onDismissRequest = {
148+
showNotificationDialog = false
149+
NotificationPromptStore.markDailyPromptShown(context)
150+
},
151+
icon = {
152+
Icon(
153+
Icons.Default.Notifications,
154+
contentDescription = null,
155+
tint = MaterialTheme.colorScheme.primary,
156+
modifier = Modifier.size(32.dp)
157+
)
158+
},
159+
title = {
160+
Text(
161+
"Stay Updated",
162+
fontWeight = FontWeight.Bold
163+
)
164+
},
165+
text = {
166+
Text(
167+
"Enable notifications to get instant alerts for new assignments, deadline reminders, and important updates from your portal.",
168+
color = MaterialTheme.colorScheme.onSurfaceVariant
169+
)
170+
},
171+
confirmButton = {
172+
Button(
173+
onClick = {
174+
showNotificationDialog = false
175+
NotificationPromptStore.markDailyPromptShown(context)
176+
// Open app notification settings
177+
val intent = android.content.Intent().apply {
178+
action = android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS
179+
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, context.packageName)
180+
}
181+
context.startActivity(intent)
182+
},
183+
shape = RoundedCornerShape(12.dp)
184+
) {
185+
Text("Turn On", fontWeight = FontWeight.Bold)
186+
}
187+
},
188+
dismissButton = {
189+
TextButton(
190+
onClick = {
191+
showNotificationDialog = false
192+
NotificationPromptStore.markDailyPromptShown(context)
193+
}
194+
) {
195+
Text("Not Now")
196+
}
197+
},
198+
shape = RoundedCornerShape(20.dp)
199+
)
200+
}
112201
}
113202
}
114203

204+
@OptIn(ExperimentalMaterial3Api::class)
115205
@Composable
116206
fun MainScreen(
117207
viewModel: MainViewModel,
@@ -144,6 +234,8 @@ fun MainScreen(
144234
val context = LocalContext.current
145235
var activeUploads by remember { mutableStateOf<List<QueuedUpload>>(emptyList()) }
146236
var activeDownloads by remember { mutableStateOf<List<QueuedDownload>>(emptyList()) }
237+
var showTimetableModal by remember { mutableStateOf(false) }
238+
var timetableError by remember { mutableStateOf<String?>(null) }
147239

148240
LaunchedEffect(Unit) {
149241
while (true) {
@@ -163,7 +255,6 @@ fun MainScreen(
163255
!isLoggedIn -> AppPage.LOGIN
164256
currentScreen == ScreenType.SETTINGS -> AppPage.SETTINGS
165257
currentScreen == ScreenType.DOWNLOADS -> AppPage.DOWNLOADS
166-
currentScreen == ScreenType.TIMETABLE -> AppPage.TIMETABLE
167258
currentScreen == ScreenType.HISTORICAL -> AppPage.HISTORICAL
168259
else -> AppPage.PENDING
169260
}
@@ -234,11 +325,18 @@ fun MainScreen(
234325
historical = dashboardData.historicalAssignments
235326
)
236327
}
237-
238-
val fetchedTimetable = viewModel.loadTimetable()
239-
if (fetchedTimetable.isNotEmpty()) {
240-
timetableLectures = fetchedTimetable
241-
TimetableCacheStore.saveSnapshot(context, fetchedTimetable)
328+
timetableError = null
329+
try {
330+
val fetchedTimetable = viewModel.loadTimetable()
331+
if (fetchedTimetable.isNotEmpty()) {
332+
timetableLectures = fetchedTimetable
333+
TimetableCacheStore.saveSnapshot(context, fetchedTimetable)
334+
}
335+
} catch (e: PortalSystemException) {
336+
timetableError = e.message
337+
Log.e("MainActivity", "Timetable fetch failed: ${e.message}")
338+
} catch (e: Exception) {
339+
Log.e("MainActivity", "Timetable fetch unknown error", e)
242340
}
243341
}
244342

@@ -284,10 +382,7 @@ fun MainScreen(
284382
currentScreen = ScreenType.PENDING
285383
pendingExitConfirmation = false
286384
}
287-
AppPage.TIMETABLE -> {
288-
currentScreen = ScreenType.PENDING
289-
pendingExitConfirmation = false
290-
}
385+
291386
AppPage.DOWNLOADS -> {
292387
currentScreen = ScreenType.PENDING
293388
pendingExitConfirmation = false
@@ -724,22 +819,7 @@ fun MainScreen(
724819
},
725820
lastSyncedMs = lastSyncedMs,
726821
timetableLectures = timetableLectures,
727-
onNavigateToTimetable = { currentScreen = ScreenType.TIMETABLE }
728-
)
729-
}
730-
AppPage.TIMETABLE -> {
731-
TimetableScreen(
732-
lectures = timetableLectures,
733-
isRefreshing = isPendingRefreshing,
734-
onRefresh = {
735-
if (isPendingRefreshing) return@TimetableScreen
736-
isPendingRefreshing = true
737-
scope.launch {
738-
runCatching { refreshAssignmentsState() }
739-
isPendingRefreshing = false
740-
}
741-
},
742-
lastSyncedMs = lastSyncedMs
822+
onNavigateToTimetable = { showTimetableModal = true }
743823
)
744824
}
745825
AppPage.DOWNLOADS -> {
@@ -920,20 +1000,26 @@ fun MainScreen(
9201000
val loadingMessage = when {
9211001
pageForUi == AppPage.LOGIN -> "Signing in..."
9221002
loadingTargetScreen == ScreenType.HISTORICAL -> "Loading historical assignments..."
923-
loadingTargetScreen == ScreenType.TIMETABLE -> "Loading timetable..."
9241003
else -> "Loading assignments..."
9251004
}
9261005
LoadingStatusOverlay(message = loadingMessage)
9271006
}
9281007

929-
if (isLoggedIn && !showCaptchaDialog) {
930-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
931-
AssignlyBottomNavigation(
932-
currentScreen = currentScreen,
933-
onNavigate = { currentScreen = it }
934-
)
935-
}
936-
}
1008+
1009+
}
1010+
}
1011+
1012+
if (showTimetableModal) {
1013+
ModalBottomSheet(
1014+
onDismissRequest = { showTimetableModal = false },
1015+
containerColor = MaterialTheme.colorScheme.background,
1016+
dragHandle = { BottomSheetDefaults.DragHandle() }
1017+
) {
1018+
TimetableBottomSheetContent(
1019+
lectures = timetableLectures,
1020+
timetableError = timetableError,
1021+
onClose = { showTimetableModal = false }
1022+
)
9371023
}
9381024
}
9391025

0 commit comments

Comments
 (0)