-
+
+
+ |
Chandram Dutta
@@ -38,6 +35,20 @@ Open in Xcode and run the app in simulator.
|
+
+ Rujin Devkota
+
+
+
+
+
+
+
+
+
+
+
+ |
diff --git a/VITTY/GoogleService-Info.plist b/VITTY/GoogleService-Info.plist
new file mode 100644
index 0000000..57973b4
--- /dev/null
+++ b/VITTY/GoogleService-Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ CLIENT_ID
+ 272763363329-i8n51oo9m30h9it7qq9ufmd0lahnmm63.apps.googleusercontent.com
+ REVERSED_CLIENT_ID
+ com.googleusercontent.apps.272763363329-i8n51oo9m30h9it7qq9ufmd0lahnmm63
+ ANDROID_CLIENT_ID
+ 272763363329-143lqjkb0i5a75lc0iglc26jlb61po0c.apps.googleusercontent.com
+ API_KEY
+ AIzaSyCJYYDMdzQiNiY0pxqbrglEw85BSlGgHBc
+ GCM_SENDER_ID
+ 272763363329
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ com.gdscvit.vittyios
+ PROJECT_ID
+ vitty-dscvit
+ STORAGE_BUCKET
+ vitty-dscvit.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:272763363329:ios:3b020b67f7527e83e2e000
+
+
\ No newline at end of file
diff --git a/VITTY/VITTY.xcodeproj/project.pbxproj b/VITTY/VITTY.xcodeproj/project.pbxproj
index f6dafb4..1b57bd7 100644
--- a/VITTY/VITTY.xcodeproj/project.pbxproj
+++ b/VITTY/VITTY.xcodeproj/project.pbxproj
@@ -75,6 +75,7 @@
4BD63D742D70547E00EEF5D7 /* EmptyClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD63D722D70547E00EEF5D7 /* EmptyClass.swift */; };
4BD63D772D70610B00EEF5D7 /* EmptyClassAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD63D762D70610000EEF5D7 /* EmptyClassAPIService.swift */; };
4BD63D7A2D70636400EEF5D7 /* EmptyClassRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD63D792D70635D00EEF5D7 /* EmptyClassRoomViewModel.swift */; };
+ 4BD77EA22EBBCE050027F234 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4BD77EA12EBBCE050027F234 /* GoogleService-Info.plist */; };
4BF03C992D7819E30098C803 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF03C982D7819E00098C803 /* Notes.swift */; };
4BF03C9B2D7838C80098C803 /* NotesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF03C9A2D7838C50098C803 /* NotesHelper.swift */; };
4BF0C77B2D93108B00016202 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF0C77A2D93108500016202 /* SearchBar.swift */; };
@@ -241,6 +242,7 @@
4BD63D722D70547E00EEF5D7 /* EmptyClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyClass.swift; sourceTree = ""; };
4BD63D762D70610000EEF5D7 /* EmptyClassAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyClassAPIService.swift; sourceTree = ""; };
4BD63D792D70635D00EEF5D7 /* EmptyClassRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyClassRoomViewModel.swift; sourceTree = ""; };
+ 4BD77EA12EBBCE050027F234 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
4BF03C982D7819E00098C803 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = ""; };
4BF03C9A2D7838C50098C803 /* NotesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesHelper.swift; sourceTree = ""; };
4BF0C77A2D93108500016202 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; };
@@ -418,6 +420,7 @@
children = (
4BC853C52DF6F71B0092B2E2 /* VittyWidgetExtension.entitlements */,
4B1A500E2E3E61530060314D /* GoogleService-Info.plist */,
+ 4BD77EA12EBBCE050027F234 /* GoogleService-Info.plist */,
4B8FB2C72E39D29F00E50AE2 /* GoogleService-Info.plist */,
5251A7FF2B46E3C000D44CFE /* .swift-format */,
314A408E27383BEC0058082F /* VITTYApp.swift */,
@@ -1127,6 +1130,7 @@
31128CFA2772F57E0084C9EA /* Poppins-SemiBoldItalic.ttf in Resources */,
31128CFC2772F57E0084C9EA /* Poppins-Regular.ttf in Resources */,
4B8FB2C82E39D29F00E50AE2 /* GoogleService-Info.plist in Resources */,
+ 4BD77EA22EBBCE050027F234 /* GoogleService-Info.plist in Resources */,
314A409627383BEE0058082F /* Preview Assets.xcassets in Resources */,
314A409327383BEE0058082F /* Assets.xcassets in Resources */,
);
@@ -1437,6 +1441,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"VITTY/Preview Content\"";
+ DEVELOPMENT_TEAM = C7RX29D33F;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VITTY/Info.plist;
diff --git a/VITTY/VITTY/Academics/VIewModel/ReminderNotifcationManager.swift b/VITTY/VITTY/Academics/VIewModel/ReminderNotifcationManager.swift
index 5cf4d87..db40700 100644
--- a/VITTY/VITTY/Academics/VIewModel/ReminderNotifcationManager.swift
+++ b/VITTY/VITTY/Academics/VIewModel/ReminderNotifcationManager.swift
@@ -24,39 +24,97 @@ class NotificationManager {
}
func scheduleNotification(title: String, body: String, date: Date, identifier: String) {
- let content = UNMutableNotificationContent()
- content.title = title
- content.body = body
- content.sound = .default
+ // Check if notifications are enabled
+ guard UserDefaults.standard.bool(forKey: "notificationsEnabled") else {
+ print("Notifications are disabled. Skipping reminder notification scheduling.")
+ return
+ }
+
+ // Check if date is in the past
+ guard date > Date() else {
+ print("Cannot schedule notification for past date: \(date)")
+ return
+ }
+
+ // Check notification authorization
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
+ guard settings.authorizationStatus == .authorized else {
+ print("Notification authorization not granted. Cannot schedule reminder.")
+ return
+ }
+
+ let content = UNMutableNotificationContent()
+ content.title = title
+ content.body = body
+ content.sound = .default
- let triggerDate = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date)
- let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)
+ let triggerDate = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date)
+ let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)
- let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
+ let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
- UNUserNotificationCenter.current().add(request) { error in
- if let error = error {
- print("Failed to schedule notification: \(error)")
+ UNUserNotificationCenter.current().add(request) { error in
+ if let error = error {
+ print("Failed to schedule notification: \(error)")
+ } else {
+ print("Successfully scheduled reminder notification: '\(title)' at \(date)")
+ }
}
}
}
- func scheduleReminderNotifications(title: String, date: Date,subject: String) {
-
+ func scheduleReminderNotifications(title: String, date: Date, subject: String) {
+ // Check if notifications are enabled
+ guard UserDefaults.standard.bool(forKey: "notificationsEnabled") else {
+ print("Notifications are disabled. Skipping reminder notification scheduling.")
+ return
+ }
+
+ // Check if date is in the past
+ guard date > Date() else {
+ print("Cannot schedule reminder notifications for past date: \(date)")
+ return
+ }
+
+ // Create unique identifier using title, subject, and timestamp
+ let timestamp = Int(date.timeIntervalSince1970)
+ let baseIdentifier = "reminder-\(subject)-\(title)-\(timestamp)"
+
+ // Schedule exact time notification
scheduleNotification(
title: "\(subject): \(title)",
body: "Your scheduled reminder is now.",
date: date,
- identifier: "\(title)-exact"
+ identifier: "\(baseIdentifier)-exact"
)
+ // Calculate 10 minutes before, handling edge cases safely
+ guard let tenMinutesBefore = Calendar.current.date(byAdding: .minute, value: -10, to: date),
+ tenMinutesBefore > Date() else {
+ print("Cannot schedule 10-minute reminder notification (too close to current time or in the past)")
+ // Still schedule the exact time notification
+ return
+ }
- let tenMinutesBefore = Calendar.current.date(byAdding: .minute, value: -10, to: date)!
scheduleNotification(
title: "Upcoming Reminder: \(title)",
body: "Your reminder is in 10 minutes.",
date: tenMinutesBefore,
- identifier: "\(title)-early"
+ identifier: "\(baseIdentifier)-early"
)
}
+
+ /// Cancel notifications for a specific reminder
+ func cancelReminderNotifications(title: String, date: Date, subject: String) {
+ let timestamp = Int(date.timeIntervalSince1970)
+ let baseIdentifier = "reminder-\(subject)-\(title)-\(timestamp)"
+
+ let identifiers = [
+ "\(baseIdentifier)-exact",
+ "\(baseIdentifier)-early"
+ ]
+
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
+ print("Cancelled reminder notifications for: \(title)")
+ }
}
diff --git a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift
index 4090398..eaf7987 100644
--- a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift
+++ b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift
@@ -28,19 +28,21 @@ struct JoinGroup: View {
var body: some View {
ZStack {
- VStack(spacing: 20) {
+ VStack(spacing: 0) {
+ // Drag indicator
Capsule()
.fill(Color.gray.opacity(0.5))
.frame(width: 50, height: 5)
- .padding(.top, 10)
+ .padding(.top, 8)
- Spacer().frame(height: 7)
+ // Title
Text("Join Circle")
.font(.system(size: 21, weight: .bold))
.foregroundColor(.white)
+ .padding(.top, 20)
+ .padding(.bottom, 24)
- Spacer().frame(width: 20)
-
+ // Input section
VStack(alignment: .leading, spacing: 10) {
Text("Enter circle code")
.font(.system(size: 16, weight: .bold))
@@ -63,54 +65,9 @@ struct JoinGroup: View {
}
.padding(.horizontal, 20)
- HStack {
- Rectangle()
- .fill(Color.gray.opacity(0.5))
- .frame(height: 1)
- Text("OR")
- .font(.system(size: 16, weight: .bold))
- .foregroundColor(.white)
- .padding(.horizontal, 10)
- Rectangle()
- .fill(Color.gray.opacity(0.5))
- .frame(height: 1)
- }
- .padding(.horizontal, 20)
-
- HStack {
- Text("Scan QR Code")
- .font(.system(size: 16, weight: .bold))
- .foregroundColor(Color("Accent"))
- .padding(.leading, 20)
- Spacer()
- }
-
- Button(action: {
- openCameraApp()
- }) {
- VStack {
- Image(systemName: "qrcode.viewfinder")
- .resizable()
- .scaledToFit()
- .frame(width: 100, height: 100)
- .foregroundColor(Color.white)
-
- Text("Tap to open camera")
- .font(.system(size: 14))
- .foregroundColor(.gray)
- }
- .frame(width: screenWidth * 0.8, height: screenHeight * 0.25)
- .background(Color.black.opacity(0.3))
- .cornerRadius(12)
- .overlay(
- RoundedRectangle(cornerRadius: 12)
- .stroke(Color.gray.opacity(0.5), lineWidth: 1)
- )
- }
- .disabled(isJoining)
-
Spacer()
+ // Join button
HStack {
Spacer()
Button(action: {
@@ -135,7 +92,7 @@ struct JoinGroup: View {
}
.padding(.bottom, 20)
}
- .presentationDetents([.height(screenHeight * 0.65)])
+ .presentationDetents([.height(screenHeight * 0.35)])
.background(Color("Secondary"))
if showToast {
@@ -174,26 +131,6 @@ struct JoinGroup: View {
}
}
- // MARK: - Open Camera App
-
- private func openCameraApp() {
- guard let url = URL(string: "camera:") else {
- showToast(message: "Camera not available", isError: true)
- return
- }
-
- if UIApplication.shared.canOpenURL(url) {
- UIApplication.shared.open(url)
- } else {
-
- if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
- showToast(message: "Please open Camera app manually", isError: false)
- } else {
- showToast(message: "Camera app not available", isError: true)
- }
- }
- }
-
// MARK: - Handle Deep Link
private func handleDeepLink(_ url: URL) {
diff --git a/VITTY/VITTY/Home/View/HomeView.swift b/VITTY/VITTY/Home/View/HomeView.swift
index e0928e2..d879b32 100644
--- a/VITTY/VITTY/Home/View/HomeView.swift
+++ b/VITTY/VITTY/Home/View/HomeView.swift
@@ -417,7 +417,7 @@ struct HomeView: View {
ZStack {
if !showProfileSidebar {
Button {
- withAnimation(.easeInOut(duration: 0.8)) {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showProfileSidebar = true
}
} label: {
@@ -455,7 +455,7 @@ struct HomeView: View {
.ignoresSafeArea()
.transition(.opacity)
.onTapGesture {
- withAnimation(.easeInOut(duration: 0.8)) {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showProfileSidebar = false
}
}
@@ -468,7 +468,7 @@ struct HomeView: View {
showLogoutAlert: $showLogoutAlert
)
.frame(width: UIScreen.main.bounds.width * 0.75)
- .transition(.move(edge: .trailing))
+ .transition(.move(edge: .trailing).combined(with: .opacity))
}
}
}
diff --git a/VITTY/VITTY/Settings/View/SettingsView.swift b/VITTY/VITTY/Settings/View/SettingsView.swift
index b504576..835cc68 100644
--- a/VITTY/VITTY/Settings/View/SettingsView.swift
+++ b/VITTY/VITTY/Settings/View/SettingsView.swift
@@ -270,22 +270,21 @@ struct SettingsView: View {
isSyncing = true
Task {
- let syncViewModel = TimeTableView.TimeTableViewModel()
-
- await syncViewModel.forceSync(
- username: username,
- authToken: authToken,
- context: modelContext
- )
-
- await MainActor.run {
- isSyncing = false
+ do {
+ // Fetch updated timetable from API
+ let remoteTimeTable = try await TimeTableAPIService.shared.getTimeTable(
+ with: username,
+ authToken: authToken
+ )
-
- if syncViewModel.stage == .data {
- showSyncMessage("Timetable synced successfully!", success: true)
- } else {
- showSyncMessage("Sync failed. Please try again.", success: false)
+ await MainActor.run {
+ updateLocalTimetable(with: remoteTimeTable)
+ }
+
+ } catch {
+ await MainActor.run {
+ isSyncing = false
+ showSyncMessage("Sync failed: \(error.localizedDescription)", success: false)
}
}
}
@@ -340,10 +339,22 @@ struct SettingsView: View {
isSyncing = false
showSyncMessage("Timetable synced successfully!", success: true)
- NotificationCenter.default.post(
- name: NSNotification.Name("TimetableDidChange"),
- object: nil
- )
+ // Update viewModel timetable to trigger notification rescheduling
+ viewModel.timetable = finalTimeTable
+
+ // Post notification to update TimeTableView
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ NotificationCenter.default.post(
+ name: NSNotification.Name("TimetableDidChange"),
+ object: nil
+ )
+
+ // Also post a refresh notification
+ NotificationCenter.default.post(
+ name: NSNotification.Name("RefreshTimetableFromSettings"),
+ object: nil
+ )
+ }
} catch {
isSyncing = false
@@ -360,6 +371,9 @@ struct SettingsView: View {
isSyncing = false
showSyncMessage("Timetable synced successfully!", success: true)
+ // Update viewModel timetable to trigger notification rescheduling
+ viewModel.timetable = timeTable
+
NotificationCenter.default.post(
name: NSNotification.Name("TimetableDidChange"),
object: nil
diff --git a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift
index 3873306..7bcb385 100644
--- a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift
+++ b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift
@@ -10,21 +10,46 @@ class SettingsViewModel: ObservableObject {
requestPermissionAndSchedule()
} else {
-
+
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
- print("All pending notifications have been cleared.")
+ print("All pending notifications have been cleared (notifications disabled).")
showNotificationDisabledAlert = true
}
}
}
- @Published var timetable: TimeTable?
+ @Published var timetable: TimeTable? {
+ didSet {
+
+ if notificationsEnabled, let timetable = timetable {
+
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
+ DispatchQueue.main.async {
+ if settings.authorizationStatus == .authorized {
+ self.scheduleAllNotifications(from: timetable)
+ }
+ }
+ }
+ }
+ }
+ }
@Published var showNotificationDisabledAlert = false
init(timetable: TimeTable? = nil) {
self.timetable = timetable
checkNotificationAuthorization()
+
+
+ if notificationsEnabled, let timetable = timetable {
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
+ DispatchQueue.main.async {
+ if settings.authorizationStatus == .authorized {
+ self.scheduleAllNotifications(from: timetable)
+ }
+ }
+ }
+ }
}
@@ -58,8 +83,14 @@ class SettingsViewModel: ObservableObject {
func scheduleAllNotifications(from timetable: TimeTable) {
+
+ guard notificationsEnabled else {
+ print("Notifications are disabled. Skipping scheduling.")
+ return
+ }
- UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
+
+ removeClassNotifications()
let weekdays: [(Int, [Lecture])] = [
(1, timetable.sunday), (2, timetable.monday), (3, timetable.tuesday),
@@ -69,65 +100,98 @@ class SettingsViewModel: ObservableObject {
for (weekday, lectures) in weekdays {
for lecture in lectures {
-
- scheduleNotificationForNextOccurence(lecture: lecture, weekday: weekday)
+ scheduleRecurringNotification(lecture: lecture, weekday: weekday)
+ }
+ }
+ print("Scheduled all recurring weekly notifications.")
+ }
+
+ /// Remove only class notifications, preserving reminder notifications
+ private func removeClassNotifications() {
+ UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
+ let classNotificationIdentifiers = requests
+ .filter { request in
+
+ let identifier = request.identifier
+ return !identifier.hasPrefix("reminder-") &&
+ (identifier.contains("-reminder-") || identifier.contains("-start-"))
+ }
+ .map { $0.identifier }
+
+ if !classNotificationIdentifiers.isEmpty {
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: classNotificationIdentifiers)
+ print("Removed \(classNotificationIdentifiers.count) class notification(s)")
}
}
- print("Scheduled all notifications for the next 7 days.")
}
- private func scheduleNotificationForNextOccurence(lecture: Lecture, weekday: Int) {
+ private func scheduleRecurringNotification(lecture: Lecture, weekday: Int) {
guard let time = parseTime(from: lecture.startTime) else { return }
- var dateComponents = DateComponents()
- dateComponents.hour = Calendar.current.component(.hour, from: time)
- dateComponents.minute = Calendar.current.component(.minute, from: time)
- dateComponents.weekday = weekday
+ let hour = Calendar.current.component(.hour, from: time)
+ let minute = Calendar.current.component(.minute, from: time)
+
+ var reminderComponents = DateComponents()
+ reminderComponents.weekday = weekday
- guard let nextTriggerDate = Calendar.current.nextDate(after: Date(), matching: dateComponents, matchingPolicy: .nextTime) else { return }
-
- scheduleNotification(
+ if minute >= 10 {
+ reminderComponents.hour = hour
+ reminderComponents.minute = minute - 10
+ } else {
+
+ reminderComponents.hour = hour > 0 ? hour - 1 : 23
+ reminderComponents.minute = 60 + minute - 10
+ }
+
+
+ var startComponents = DateComponents()
+ startComponents.weekday = weekday
+ startComponents.hour = hour
+ startComponents.minute = minute
+
+ scheduleWeeklyNotification(
lectureName: lecture.name,
- date: nextTriggerDate,
+ components: reminderComponents,
title: "Upcoming Class",
body: "\(lecture.name) starts in 10 minutes.",
- minutesBefore: 10
+ identifier: "\(lecture.name)-reminder-\(weekday)"
)
-
- scheduleNotification(
+ scheduleWeeklyNotification(
lectureName: lecture.name,
- date: nextTriggerDate,
+ components: startComponents,
title: "Class Starting!",
body: "\(lecture.name) is starting now.",
- minutesBefore: 0
+ identifier: "\(lecture.name)-start-\(weekday)"
)
}
- private func scheduleNotification(lectureName: String, date: Date, title: String, body: String, minutesBefore: Int) {
+ private func scheduleWeeklyNotification(
+ lectureName: String,
+ components: DateComponents,
+ title: String,
+ body: String,
+ identifier: String
+ ) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
-
+
- guard let triggerDate = Calendar.current.date(byAdding: .minute, value: -minutesBefore, to: date) else { return }
- let triggerComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: triggerDate)
+ let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
- let trigger = UNCalendarNotificationTrigger(dateMatching: triggerComponents, repeats: false)
-
- let identifier = "\(lectureName)-\(title)-\(triggerDate.timeIntervalSince1970)"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
- print("Error scheduling notification for \(lectureName): \(error.localizedDescription)")
+ print("Error scheduling recurring notification for \(lectureName): \(error.localizedDescription)")
} else {
- print("Successfully scheduled notification: '\(title)' for \(lectureName)")
+ print("Successfully scheduled recurring notification: '\(title)' for \(lectureName) on weekday \(components.weekday ?? 0)")
}
}
}
diff --git a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift
index 0782046..52ba40e 100644
--- a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift
+++ b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift
@@ -55,6 +55,10 @@ struct TimeTableView: View {
.onChange(of: scenePhase) { _, newPhase in
handleScenePhaseChange(newPhase)
}
+ .onReceive(NotificationCenter.default.publisher(for: Notification.Name("RefreshTimetableFromSettings"))) { _ in
+ logger.debug("Received refresh notification from settings")
+ loadTimetable()
+ }
}
@ViewBuilder
diff --git a/VITTY/VITTY/UserProfileSideBar/SideBar.swift b/VITTY/VITTY/UserProfileSideBar/SideBar.swift
index f96f4d6..fcfbfdf 100644
--- a/VITTY/VITTY/UserProfileSideBar/SideBar.swift
+++ b/VITTY/VITTY/UserProfileSideBar/SideBar.swift
@@ -119,9 +119,8 @@ struct UserProfileSidebar: View {
.frame(width: UIScreen.main.bounds.width * 0.75, alignment: .leading)
.frame(maxHeight: .infinity)
.background(Color("Background"))
- .transition(.move(edge: .trailing))
+ .transition(.move(edge: .trailing).combined(with: .opacity))
}
- .animation(.easeInOut(duration: 0.3), value: isPresented)
.sheet(isPresented: $showSupportDialog) {
SupportDialog()
}
diff --git a/VITTY/VittyWidget/AppIntent.swift b/VITTY/VittyWidget/AppIntent.swift
index 23db796..70c9844 100644
--- a/VITTY/VittyWidget/AppIntent.swift
+++ b/VITTY/VittyWidget/AppIntent.swift
@@ -16,3 +16,4 @@ struct ConfigurationAppIntent: WidgetConfigurationIntent {
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
}
+
diff --git a/VITTY/VittyWidget/Control/EntryControlViews/ScheduleEntryControlView.swift b/VITTY/VittyWidget/Control/EntryControlViews/ScheduleEntryControlView.swift
index c2935b8..6611a94 100644
--- a/VITTY/VittyWidget/Control/EntryControlViews/ScheduleEntryControlView.swift
+++ b/VITTY/VittyWidget/Control/EntryControlViews/ScheduleEntryControlView.swift
@@ -13,23 +13,34 @@ struct VittyWidgetEntryView: View {
@Environment(\.widgetFamily) var family
var body: some View {
- ZStack {
- Color(hex: "#041727")
- .ignoresSafeArea()
+ switch family {
+ case .accessoryCircular:
+ CircularLockScreenWidgetView(entry: entry)
+
+ case .accessoryRectangular:
+ RectangularLockScreenWidgetView(entry: entry)
+
+ case .accessoryInline:
+ InlineLockScreenWidgetView(entry: entry)
- switch family {
- case .systemSmall:
- ScheduleSmallWidgetView(entry: entry)
- case .systemMedium:
- ScheduleMediumWidgetView(entry: entry)
- case .systemLarge:
- ScheduleLargeWidgetView(entry: entry)
+ default:
+ ZStack {
+ Color(hex: "#041727")
+ .ignoresSafeArea()
- default:
- Text("Unsupported size")
+ switch family {
+ case .systemSmall:
+ ScheduleSmallWidgetView(entry: entry)
+ case .systemMedium:
+ ScheduleMediumWidgetView(entry: entry)
+ case .systemLarge:
+ ScheduleLargeWidgetView(entry: entry)
+ default:
+ EmptyView()
+ }
}
+
}
- .containerBackground(for: .widget) { Color(hex: "#041727") }
}
}
@@ -44,6 +55,13 @@ struct VittyWidget: Widget {
}
.configurationDisplayName("Vitty Widget")
.description("Widget with different designs based on size.")
- .supportedFamilies([.systemSmall, .systemMedium,.systemLarge])
+ .supportedFamilies([
+ .systemSmall,
+ .systemMedium,
+ .systemLarge,
+ .accessoryCircular,
+ .accessoryRectangular,
+ .accessoryInline
+ ])
}
}
diff --git a/VITTY/VittyWidget/Views/LargeWidget.swift b/VITTY/VittyWidget/Views/LargeWidget.swift
index fa82c4d..99a92f6 100644
--- a/VITTY/VittyWidget/Views/LargeWidget.swift
+++ b/VITTY/VittyWidget/Views/LargeWidget.swift
@@ -67,6 +67,8 @@ struct LargeDueWidgetView: View {
}
}
+
+
struct ScheduleLargeWidgetView: View {
var entry: ScheduleEntry
diff --git a/VITTY/VittyWidget/Views/LockScreenWidgets.swift b/VITTY/VittyWidget/Views/LockScreenWidgets.swift
new file mode 100644
index 0000000..32cc50f
--- /dev/null
+++ b/VITTY/VittyWidget/Views/LockScreenWidgets.swift
@@ -0,0 +1,342 @@
+//
+// LockScreenWidgets.swift
+// VittyWidget
+//
+// Created on 6/12/25.
+//
+
+import SwiftUI
+import WidgetKit
+
+
+struct CircularLockScreenWidgetView: View {
+ var entry: ScheduleEntry
+
+ var body: some View {
+ ZStack {
+
+ Circle()
+ .stroke(.white.opacity(0.2), lineWidth: 4.5)
+
+
+ if entry.total > 0 {
+ let progress = CGFloat(entry.completed) / CGFloat(entry.total)
+ Circle()
+ .trim(from: 0, to: progress)
+ .stroke(
+ .tint,
+ style: StrokeStyle(lineWidth: 4.5, lineCap: .round)
+ )
+ .rotationEffect(.degrees(-90))
+ }
+
+
+ VStack(spacing: 1) {
+ if let nextClass = entry.classes.first {
+
+ Text(formatTime(nextClass.time))
+ .font(.system(size: 11, weight: .bold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.6)
+
+
+ if entry.total > 0 {
+ Text("\(entry.completed)/\(entry.total)")
+ .font(.system(size: 7, weight: .semibold, design: .rounded))
+ .foregroundStyle(.secondary)
+ }
+ } else if entry.completed == entry.total && entry.total > 0 {
+
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 18))
+ .foregroundStyle(.tint)
+
+ Text("Done")
+ .font(.system(size: 7, weight: .bold, design: .rounded))
+ .foregroundStyle(.secondary)
+ } else {
+
+ Image(systemName: "calendar")
+ .font(.system(size: 14))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+
+ private func formatTime(_ timeString: String) -> String {
+ let components = timeString.components(separatedBy: " - ")
+ guard let startTime = components.first else { return "" }
+ return startTime
+ }
+}
+
+// MARK: - Rectangular Lock Screen Widget
+struct RectangularLockScreenWidgetView: View {
+ var entry: ScheduleEntry
+
+ var body: some View {
+ HStack(spacing: 10) {
+
+ VStack(spacing: 1) {
+ if entry.total > 0 {
+ ZStack {
+
+ Circle()
+ .stroke(.white.opacity(0.15), lineWidth: 3.5)
+ .frame(width: 26, height: 26)
+
+
+ Circle()
+ .trim(from: 0, to: CGFloat(entry.completed) / CGFloat(entry.total))
+ .stroke(
+ .tint,
+ style: StrokeStyle(lineWidth: 3.5, lineCap: .round)
+ )
+ .frame(width: 26, height: 26)
+ .rotationEffect(.degrees(-90))
+
+
+ Text("\(entry.completed)")
+ .font(.system(size: 9, weight: .bold, design: .rounded))
+ .foregroundStyle(.primary)
+ }
+
+
+ Text("/\(entry.total)")
+ .font(.system(size: 7, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ } else {
+ Image(systemName: "calendar")
+ .font(.system(size: 13))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .frame(width: 32)
+
+
+ VStack(alignment: .leading, spacing: 3) {
+ if let currentClass = getCurrentClass() {
+
+ HStack(spacing: 5) {
+ Circle()
+ .fill(.tint)
+ .frame(width: 5, height: 5)
+
+ Text("Now: \(currentClass.title)")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ }
+
+ if let nextClass = getNextUpcomingClass() {
+ Text("Next: \(nextClass.title) • \(formatTime(nextClass.time))")
+ .font(.system(size: 10, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ } else {
+ Text("\(formatTime(currentClass.time))")
+ .font(.system(size: 10, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ } else if let nextClass = entry.classes.first {
+
+ Text(nextClass.title)
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+
+ HStack(spacing: 4) {
+ Image(systemName: "clock")
+ .font(.system(size: 8))
+ Text("\(formatTime(nextClass.time))")
+ .font(.system(size: 10, weight: .medium, design: .rounded))
+ }
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ } else if entry.completed == entry.total && entry.total > 0 {
+
+ HStack(spacing: 4) {
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 10))
+ Text("All \(entry.total) classes completed")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ }
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+
+ Text("Great work today! 🎉")
+ .font(.system(size: 10, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ } else {
+ // No classes
+ HStack(spacing: 4) {
+ Image(systemName: "calendar.badge.plus")
+ .font(.system(size: 10))
+ Text("No classes today")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ }
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding(.horizontal, 6)
+ }
+
+ private func getCurrentClass() -> Classes? {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "h:mm a"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ let calendar = Calendar.current
+ let now = Date()
+
+ return entry.classes.first { classItem in
+ let components = classItem.time.components(separatedBy: " - ")
+ guard components.count == 2,
+ let startTime = formatter.date(from: components[0]),
+ let endTime = formatter.date(from: components[1]) else {
+ return false
+ }
+
+ let startToday = calendar.date(
+ bySettingHour: calendar.component(.hour, from: startTime),
+ minute: calendar.component(.minute, from: startTime),
+ second: 0,
+ of: now
+ )
+
+ let endToday = calendar.date(
+ bySettingHour: calendar.component(.hour, from: endTime),
+ minute: calendar.component(.minute, from: endTime),
+ second: 0,
+ of: now
+ )
+
+ guard let start = startToday, let end = endToday else { return false }
+ return now >= start && now <= end
+ }
+ }
+
+ private func getNextUpcomingClass() -> Classes? {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "h:mm a"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ let calendar = Calendar.current
+ let now = Date()
+
+ let upcoming = entry.classes.filter { classItem in
+ let components = classItem.time.components(separatedBy: " - ")
+ guard components.count == 2,
+ let endTime = formatter.date(from: components[1]) else {
+ return false
+ }
+
+ let endToday = calendar.date(
+ bySettingHour: calendar.component(.hour, from: endTime),
+ minute: calendar.component(.minute, from: endTime),
+ second: 0,
+ of: now
+ )
+
+ guard let end = endToday else { return false }
+ return now < end
+ }
+
+ return upcoming.first
+ }
+
+ private func formatTime(_ timeString: String) -> String {
+ let components = timeString.components(separatedBy: " - ")
+ guard let startTime = components.first else { return "" }
+ return startTime
+ }
+}
+
+// MARK: - Inline Lock Screen Widget
+struct InlineLockScreenWidgetView: View {
+ var entry: ScheduleEntry
+
+ var body: some View {
+ HStack(spacing: 5) {
+ if let currentClass = getCurrentClass() {
+ Image(systemName: "clock.fill")
+ .font(.system(size: 10))
+ .foregroundStyle(.tint)
+ Text("\(currentClass.title) • \(formatTime(currentClass.time))")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ } else if let nextClass = entry.classes.first {
+ Image(systemName: "calendar")
+ .font(.system(size: 10))
+ .foregroundStyle(.tint)
+ Text("Next: \(nextClass.title) at \(formatTime(nextClass.time))")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ } else if entry.completed == entry.total && entry.total > 0 {
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 10))
+ .foregroundStyle(.tint)
+ Text("All \(entry.total) classes completed")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ } else {
+ Image(systemName: "calendar.badge.plus")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ Text("No classes today")
+ .font(.system(size: 12, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ }
+ }
+
+ private func getCurrentClass() -> Classes? {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "h:mm a"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ let calendar = Calendar.current
+ let now = Date()
+
+ return entry.classes.first { classItem in
+ let components = classItem.time.components(separatedBy: " - ")
+ guard components.count == 2,
+ let startTime = formatter.date(from: components[0]),
+ let endTime = formatter.date(from: components[1]) else {
+ return false
+ }
+
+ let startToday = calendar.date(
+ bySettingHour: calendar.component(.hour, from: startTime),
+ minute: calendar.component(.minute, from: startTime),
+ second: 0,
+ of: now
+ )
+
+ let endToday = calendar.date(
+ bySettingHour: calendar.component(.hour, from: endTime),
+ minute: calendar.component(.minute, from: endTime),
+ second: 0,
+ of: now
+ )
+
+ guard let start = startToday, let end = endToday else { return false }
+ return now >= start && now <= end
+ }
+ }
+
+ private func formatTime(_ timeString: String) -> String {
+ let components = timeString.components(separatedBy: " - ")
+ guard let startTime = components.first else { return "" }
+ return startTime
+ }
+}
+
diff --git a/VITTY/VittyWidget/Views/SmallWidget.swift b/VITTY/VittyWidget/Views/SmallWidget.swift
index 445c285..a1cafa9 100644
--- a/VITTY/VittyWidget/Views/SmallWidget.swift
+++ b/VITTY/VittyWidget/Views/SmallWidget.swift
@@ -5,13 +5,6 @@
// Created by Rujin Devkota on 2/25/25.
//
-//
-// SmallWidget.swift
-// VITTY
-//
-// Created by Rujin Devkota on 2/25/25.
-//
-
import WidgetKit
import SwiftUI
|