From 3449a0aeb3c13df926c349c449156c82de0ada30 Mon Sep 17 00:00:00 2001 From: rujin2003 Date: Sat, 5 Jul 2025 16:40:02 +0545 Subject: [PATCH 1/4] fix : merge issues --- .../xcshareddata/swiftpm/Package.resolved | 2 +- VITTY/VITTY/Academics/View/CourseRefs.swift | 580 +----------------- VITTY/VITTY/Academics/View/Notes.swift | 196 ------ .../VITTY/Academics/View/RemindersData.swift | 9 - .../VITTY/Auth/ViewModels/AuthViewModel.swift | 7 - VITTY/VITTY/Connect/Models/CircleModel.swift | 1 - .../View/Circles/Components/CirclesRow.swift | 59 -- .../View/Circles/Components/CreateGroup.swift | 5 - .../View/Circles/Components/JoinGroup.swift | 19 - .../View/Circles/View/InsideCircle.swift | 5 - VITTY/VITTY/Connect/View/ConnectPage.swift | 12 - .../Connect/View/Freinds/View/Freinds.swift | 37 -- .../ViewModel/CommunityPageViewModel.swift | 76 --- .../ViewModel/SettingsViewModel.swift | 35 -- VITTY/VITTY/TimeTable/Models/TimeTable.swift | 46 -- .../ViewModel/TimeTableViewModel.swift | 14 - .../TimeTable/Views/LectureDetailView.swift | 11 - .../VITTY/TimeTable/Views/TimeTableView.swift | 25 - VITTY/VITTY/UserProfileSideBar/SideBar.swift | 98 --- 19 files changed, 22 insertions(+), 1215 deletions(-) diff --git a/VITTY/VITTY.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VITTY/VITTY.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ad0468c..898596b 100644 --- a/VITTY/VITTY.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VITTY/VITTY.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5b3893af96db1c7a5e8d26d4b6f7b87fb1b4833e12c80c07842b211a8ba6e753", + "originHash" : "639d5d90ba6550e8200b28dab400da314adf7395e4d9c9622a0c3a880ab8780b", "pins" : [ { "identity" : "abseil-cpp-binary", diff --git a/VITTY/VITTY/Academics/View/CourseRefs.swift b/VITTY/VITTY/Academics/View/CourseRefs.swift index e43f3ab..615a5eb 100644 --- a/VITTY/VITTY/Academics/View/CourseRefs.swift +++ b/VITTY/VITTY/Academics/View/CourseRefs.swift @@ -5,17 +5,9 @@ // Created by Rujin Devkota on 2/27/25. -// -// Academics.swift -// VITTY -// -// Created by Rujin Devkota on 2/27/25. - - import SwiftUI import SwiftData -struct OCourseRefs: View { struct OCourseRefs: View { var courseName: String var courseInstitution: String @@ -25,7 +17,6 @@ struct OCourseRefs: View { @State private var showBottomSheet = false @State private var showReminderSheet = false @State private var showNotes = false - @State private var showNotes = false @State private var navigateToNotesEditor = false @State var showCourseNotes: Bool = false @State private var selectedNote: CreateNoteModel? @@ -45,7 +36,6 @@ struct OCourseRefs: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext - @Environment(\.modelContext) private var modelContext private let maxVisible = 4 @@ -53,19 +43,6 @@ struct OCourseRefs: View { @Query private var courseNotes: [CreateNoteModel] @Query private var courseFiles: [UploadedFile] - enum ContentType: String, CaseIterable { - case notes = "Notes" - case files = "Files" - - var icon: String { - switch self { - case .notes: return "doc.text" - case .files: return "folder" - } - } - } - @Query private var courseFiles: [UploadedFile] - enum ContentType: String, CaseIterable { case notes = "Notes" case files = "Files" @@ -93,7 +70,6 @@ struct OCourseRefs: View { let notesPredicate = #Predicate { $0.courseId == courseCode - $0.courseId == courseCode } _courseNotes = Query( FetchDescriptor(predicate: notesPredicate, sortBy: [SortDescriptor(\.createdAt, order: .reverse)]) @@ -118,34 +94,6 @@ struct OCourseRefs: View { } } - private var filteredFiles: [UploadedFile] { - if searchText.isEmpty { - return courseFiles - } else { - return courseFiles.filter { file in - file.fileName.localizedCaseInsensitiveContains(searchText) - } - } - - - let filesPredicate = #Predicate { - $0.courseCode == courseCode - } - _courseFiles = Query( - FetchDescriptor(predicate: filesPredicate, sortBy: [SortDescriptor(\.uploadDate, order: .reverse)]) - ) - } - - private var filteredNotes: [CreateNoteModel] { - if searchText.isEmpty { - return courseNotes - } else { - return courseNotes.filter { note in - note.noteName.localizedCaseInsensitiveContains(searchText) - } - } - } - private var filteredFiles: [UploadedFile] { if searchText.isEmpty { return courseFiles @@ -180,14 +128,6 @@ struct OCourseRefs: View { Spacer() - if selectedContentType == .files && !courseFiles.isEmpty { - Button("View All") { - showFileGallery = true - } - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color("Secondary")) - - if selectedContentType == .files && !courseFiles.isEmpty { Button("View All") { showFileGallery = true @@ -200,7 +140,6 @@ struct OCourseRefs: View { HStack { Spacer() - TextField(selectedContentType == .notes ? "Search notes..." : "Search files...", text: $searchText) TextField(selectedContentType == .notes ? "Search notes..." : "Search files...", text: $searchText) .padding(10) .frame(width: UIScreen.main.bounds.width * 0.85) @@ -208,18 +147,12 @@ struct OCourseRefs: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) .foregroundColor(.white) - .foregroundColor(.white) Spacer() } - Spacer().frame(height: 15) - - - - Spacer().frame(height: 15) Text("\(courseName) - \(courseInstitution)") @@ -245,24 +178,6 @@ struct OCourseRefs: View { } .padding(.horizontal) .padding(.top, 10) - - HStack(spacing: 12) { - ForEach(ContentType.allCases, id: \.self) { contentType in - ContentTypeTab( - contentType: contentType, - isSelected: selectedContentType == contentType, - count: contentType == .notes ? filteredNotes.count : filteredFiles.count - ) { - withAnimation(.easeInOut(duration: 0.2)) { - selectedContentType = contentType - searchText = "" - } - } - } - Spacer() - } - .padding(.horizontal) - .padding(.top, 10) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -284,16 +199,6 @@ struct OCourseRefs: View { VStack(alignment: .leading, spacing: 15) { if selectedContentType == .notes { - if filteredNotes.isEmpty { - EmptyStateView( - icon: searchText.isEmpty ? "doc.text" : "magnifyingglass", - title: searchText.isEmpty ? "No notes found for this course" : "No notes match your search", - subtitle: searchText.isEmpty ? nil : "Try searching with different keywords" - ) - } else { - ForEach(filteredNotes, id: \.createdAt) { note in - if selectedContentType == .notes { - if filteredNotes.isEmpty { EmptyStateView( icon: searchText.isEmpty ? "doc.text" : "magnifyingglass", @@ -339,50 +244,6 @@ struct OCourseRefs: View { } } - if filteredFiles.count > 6 { - Button("View All \(filteredFiles.count) Files") { - showFileGallery = true - } - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color("Secondary")) - .padding(.top, 8) - .frame(maxWidth: .infinity) - } - description: note.cachedPlainText, - isLoading: loadingNoteId == note.createdAt, - onDelete: { - noteToDelete = note - showDeleteAlert = true - } - ) - .onTapGesture { - openNote(note) - } - } - } - } else { - - if filteredFiles.isEmpty { - EmptyStateView( - icon: searchText.isEmpty ? "folder" : "magnifyingglass", - title: searchText.isEmpty ? "No files found for this course" : "No files match your search", - subtitle: searchText.isEmpty ? "Upload some files to get started" : "Try searching with different keywords" - ) - } else { - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()) - ], spacing: 12) { - ForEach(Array(filteredFiles.prefix(6)), id: \.id) { file in - CompactFileCard(file: file) { - - - showimgDeleteAlert = true - fileToDelete = file - } - } - } - if filteredFiles.count > 6 { Button("View All \(filteredFiles.count) Files") { showFileGallery = true @@ -535,27 +396,9 @@ struct OCourseRefs: View { .sheet(isPresented: $showFileGallery) { FileGalleryView(courseCode: courseCode) } - .sheet(isPresented: $showFileUpload) { - FileUploadView(courseName: courseName, courseCode: courseCode) - } - .sheet(isPresented: $showFileGallery) { - FileGalleryView(courseCode: courseCode) - } .navigationDestination(isPresented: $navigateToNotesEditor) { NoteEditorView(courseCode: courseCode, courseName: courseName, courseIns: courseInstitution, courseSlot: slot) } - .sheet(isPresented: $showNotes, content: { - NoteEditorView( - existingNote: selectedNote, - preloadedAttributedString: preloadedAttributedString, - courseCode: courseCode, - courseName: courseName, - courseIns: courseInstitution, - courseSlot: slot - ) - }) - NoteEditorView(courseCode: courseCode, courseName: courseName, courseIns: courseInstitution, courseSlot: slot) - } .sheet(isPresented: $showNotes, content: { NoteEditorView( existingNote: selectedNote, @@ -963,361 +806,37 @@ struct CompactFileCard: View { enum NoteLoadingError: Error { case invalidData case unarchiveFailed - guard let data = Data(base64Encoded: note.noteContent) else { - throw NoteLoadingError.invalidData - } - - if let attributedString = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: data) { - return attributedString - } else { - throw NoteLoadingError.unarchiveFailed - } - } - - private func deleteNote() { - guard let note = noteToDelete else { return } - - let impactFeedback = UIImpactFeedbackGenerator(style: .medium) - impactFeedback.impactOccurred() - - modelContext.delete(note) - - do { - try modelContext.save() - } catch { - print("Failed to delete note: \(error)") - } - - showDeleteAlert = false - noteToDelete = nil - } - - private func deleteFile(){ - guard let file = fileToDelete else{return} - let impactFeedback = UIImpactFeedbackGenerator(style: .medium) - impactFeedback.impactOccurred() - - modelContext.delete(file) - do { - try modelContext.save() - } catch { - print("Failed to delete file: \(error)") - } - showimgDeleteAlert = false - - } - } - -struct ContentTypeTab: View { - let contentType: OCourseRefs.ContentType - let isSelected: Bool - let count: Int - let action: () -> Void +struct BottomSheetButton: View { + var icon: String + var title: String + var action: (() -> Void)? = nil var body: some View { - Button(action: action) { - HStack(spacing: 8) { - Image(systemName: contentType.icon) - .font(.system(size: 14)) - - Text(contentType.rawValue) - .font(.system(size: 14, weight: .medium)) - - if count > 0 { - Text("(\(count))") - .font(.system(size: 12)) - .opacity(0.8) - } + Button(action: { + action?() + }) { + VStack { + Image(icon) + .font(.title) + .padding() + .background(Color.white) + .clipShape(Circle()) + Text(title) + .font(.footnote) + .foregroundColor(.white) } - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(isSelected ? Color("Accent") : Color("Secondary")) - .foregroundColor(isSelected ? .black : .white) - .cornerRadius(20) + .frame(maxWidth: .infinity) + .padding(.bottom, 10) } } } -struct EmptyStateView: View { - let icon: String - let title: String - let subtitle: String? - - var body: some View { - VStack(spacing: 16) { - Image(systemName: icon) - .font(.system(size: 48)) - .foregroundColor(.gray.opacity(0.6)) - - Text(title) - .foregroundColor(.gray) - .font(.system(size: 16, weight: .medium)) - .multilineTextAlignment(.center) - - if let subtitle = subtitle { - Text(subtitle) - .foregroundColor(.gray.opacity(0.8)) - .font(.system(size: 14)) - .multilineTextAlignment(.center) - } - } - .frame(maxWidth: .infinity) - .padding(.top, 60) - } -} -struct CompactFileCard: View { - let file: UploadedFile - let onDelete: (() -> Void)? - - @State private var showFileViewer = false - @State private var showActionSheet = false - @State private var fileImage: UIImage? - @State private var imageLoadError = false - @State private var isLoading = true - - init(file: UploadedFile, onDelete: (() -> Void)? = nil) { - self.file = file - self.onDelete = onDelete - } - - var body: some View { - VStack(spacing: 8) { - - if file.isImage && !imageLoadError { - Group { - if let image = fileImage { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - } else if isLoading { - Rectangle() - .fill(Color.gray.opacity(0.3)) - .overlay( - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - ) - } else { - Rectangle() - .fill(Color.red.opacity(0.3)) - .overlay( - VStack(spacing: 4) { - Image(systemName: "exclamationmark.triangle") - .foregroundColor(.red) - .font(.system(size: 16)) - Text("Not found") - .font(.caption2) - .foregroundColor(.red) - } - ) - } - } - .frame(height: 80) - .clipped() - .cornerRadius(8) - } else { - Rectangle() - .fill(getFileTypeColor(file.fileType).opacity(0.2)) - .frame(height: 80) - .overlay( - VStack(spacing: 4) { - Image(systemName: getFileTypeIcon(file.fileType)) - .font(.system(size: 24)) - .foregroundColor(getFileTypeColor(file.fileType)) - - Text(file.fileType.uppercased()) - .font(.caption2) - .fontWeight(.bold) - .foregroundColor(getFileTypeColor(file.fileType)) - } - ) - .cornerRadius(8) - } - - - VStack(alignment: .leading, spacing: 2) { - Text(file.fileName) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.white) - .lineLimit(2) - .multilineTextAlignment(.leading) - - Text(FileManagerHelper.shared.formatFileSize(file.fileSize)) - .font(.caption2) - .foregroundColor(.gray) - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .onTapGesture { - showFileViewer = true - } - .onLongPressGesture(minimumDuration: 0.5) { - let impactFeedback = UIImpactFeedbackGenerator(style: .medium) - impactFeedback.impactOccurred() - showActionSheet = true - } - .onAppear { - if file.isImage { - loadImageFile() - } - } - .sheet(isPresented: $showFileViewer) { - EnhancedFileViewerSheet(file: file) - } - .confirmationDialog("File Options", isPresented: $showActionSheet, titleVisibility: .visible) { - Button("Share") { - shareFile() - } - - if let onDelete = onDelete { - Button("Delete", role: .destructive) { - onDelete() - } - } - - Button("Cancel", role: .cancel) {} - } message: { - Text("Choose an action for \(file.fileName)") - } - } - - // MARK: - File Loading Methods - - private func loadImageFile() { - isLoading = true - imageLoadError = false - - Task { - let imagePaths = [file.thumbnailPath, file.localPath].compactMap { $0 } - var loadedImage: UIImage? - - for path in imagePaths { - if let data = FileManagerHelper.shared.loadFileWithFallback(from: path, courseCode: file.courseCode), - let image = UIImage(data: data) { - loadedImage = image - break - } - } - - await MainActor.run { - if let image = loadedImage { - self.fileImage = image - self.imageLoadError = false - } else { - self.imageLoadError = true - } - self.isLoading = false - } - } - } - - private func shareFile() { - guard let data = FileManagerHelper.shared.loadFileWithFallback(from: file.localPath, courseCode: file.courseCode) else { - print("Cannot share file: File not found") - return - } - - let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(file.fileName) - - do { - if FileManager.default.fileExists(atPath: tempURL.path) { - try FileManager.default.removeItem(at: tempURL) - } - try data.write(to: tempURL) - - let activityVC = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let rootViewController = window.rootViewController { - - if let popover = activityVC.popoverPresentationController { - popover.sourceView = window - popover.sourceRect = CGRect(x: window.bounds.midX, y: window.bounds.midY, width: 0, height: 0) - popover.permittedArrowDirections = [] - } - - rootViewController.present(activityVC, animated: true) - } - } catch { - print("Error sharing file: \(error)") - } - } - - private func getFileTypeIcon(_ fileType: String) -> String { - switch fileType.lowercased() { - case "pdf": - return "doc.richtext.fill" - case "txt": - return "doc.text.fill" - case "rtf", "rtfd": - return "doc.richtext.fill" - case "doc", "docx": - return "doc.fill" - case "jpg", "jpeg", "png", "gif", "heic": - return "photo.fill" - default: - return "doc.fill" - } - } - - private func getFileTypeColor(_ fileType: String) -> Color { - switch fileType.lowercased() { - case "pdf": - return .red - case "txt": - return .blue - case "rtf", "rtfd": - return .purple - case "doc", "docx": - return .blue - case "jpg", "jpeg", "png", "gif", "heic": - return .green - default: - return .gray - } - } -} - -// MARK: - Error Handling -enum NoteLoadingError: Error { - case invalidData - case unarchiveFailed -} - - -struct BottomSheetButton: View { - var icon: String - var title: String - var action: (() -> Void)? = nil - - var body: some View { - Button(action: { - action?() - }) { - VStack { - Image(icon) - .font(.title) - .padding() - .background(Color.white) - .clipShape(Circle()) - Text(title) - .font(.footnote) - .foregroundColor(.white) - } - .frame(maxWidth: .infinity) - .padding(.bottom, 10) - } - } -} - -struct TagView: View { - var reminder: Remainder - +struct TagView: View { + var reminder: Remainder + var body: some View { HStack { Circle() @@ -1346,7 +865,6 @@ struct TagView: View { } } - struct MoreTagView: View { var count: Int @@ -1369,11 +887,6 @@ struct CourseCardNotes: View { @State private var showComingSoonAlert = false - var isLoading: Bool = false - var onDelete: () -> Void - - @State private var showComingSoonAlert = false - var body: some View { HStack { VStack(alignment: .leading, spacing: 6) { @@ -1389,52 +902,6 @@ struct CourseCardNotes: View { Spacer() - if isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - .padding(.trailing, 8) - } else { - Menu { - Button(role: .destructive) { - let feedback = UISelectionFeedbackGenerator() - feedback.selectionChanged() - onDelete() - } label: { - Label("Delete", systemImage: "trash") - } - - Button { - let feedback = UISelectionFeedbackGenerator() - feedback.selectionChanged() - showComingSoonAlert = true - } label: { - Label("Export Markdown", systemImage: "square.and.arrow.down") - } - - } label: { - Image(systemName: "ellipsis") - .rotationEffect(.degrees(90)) - .foregroundColor(.white) - .font(.system(size: 20, weight: .medium)) - .padding(8) - .clipShape(Circle()) - } - } - HStack { - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.headline) - .foregroundColor(.white) - - Text(description) - .font(.subheadline) - .foregroundColor(.gray) - .lineLimit(2) - } - - Spacer() - if isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) @@ -1477,11 +944,6 @@ struct CourseCardNotes: View { .alert("Feature coming soon", isPresented: $showComingSoonAlert) { Button("OK", role: .cancel) { } } - .opacity(isLoading ? 0.7 : 1.0) - .animation(.easeInOut(duration: 0.2), value: isLoading) - .alert("Feature coming soon", isPresented: $showComingSoonAlert) { - Button("OK", role: .cancel) { } - } } } diff --git a/VITTY/VITTY/Academics/View/Notes.swift b/VITTY/VITTY/Academics/View/Notes.swift index 50465eb..0058060 100644 --- a/VITTY/VITTY/Academics/View/Notes.swift +++ b/VITTY/VITTY/Academics/View/Notes.swift @@ -4,12 +4,6 @@ // // Created by Rujin Devkota on 2/27/25. -// -// Academics.swift -// VITTY -// -// Created by Rujin Devkota on 2/27/25. - import SwiftUI import UIKit @@ -19,7 +13,6 @@ struct RichTextView: UIViewRepresentable { @Binding var typingAttributes: [NSAttributedString.Key: Any] @Binding var isEmpty: Bool - func makeUIView(context: Context) -> UITextView { let textView = UITextView() @@ -32,7 +25,6 @@ struct RichTextView: UIViewRepresentable { textView.textColor = .white - textView.attributedText = attributedText textView.selectedRange = selectedRange @@ -41,33 +33,26 @@ struct RichTextView: UIViewRepresentable { func updateUIView(_ uiView: UITextView, context: Context) { - if context.coordinator.isUpdating { return } - if !uiView.attributedText.isEqual(to: attributedText) { let previousSelectedRange = uiView.selectedRange context.coordinator.isUpdating = true uiView.attributedText = attributedText - if previousSelectedRange.location <= uiView.attributedText.length { let maxRange = min(previousSelectedRange.location + previousSelectedRange.length, uiView.attributedText.length) let validRange = NSRange(location: previousSelectedRange.location, length: maxRange - previousSelectedRange.location) uiView.selectedRange = validRange - let maxRange = min(previousSelectedRange.location + previousSelectedRange.length, uiView.attributedText.length) - let validRange = NSRange(location: previousSelectedRange.location, length: maxRange - previousSelectedRange.location) - uiView.selectedRange = validRange } context.coordinator.isUpdating = false } - if !NSEqualRanges(uiView.selectedRange, selectedRange) && selectedRange.location <= uiView.attributedText.length && NSMaxRange(selectedRange) <= uiView.attributedText.length { @@ -77,7 +62,6 @@ struct RichTextView: UIViewRepresentable { } - if !NSDictionary(dictionary: uiView.typingAttributes).isEqual(to: typingAttributes) { uiView.typingAttributes = typingAttributes } @@ -97,27 +81,21 @@ struct RichTextView: UIViewRepresentable { func textViewDidChange(_ textView: UITextView) { - guard !isUpdating else { return } isUpdating = true defer { isUpdating = false } - parent.attributedText = NSMutableAttributedString(attributedString: textView.attributedText) parent.isEmpty = textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - parent.typingAttributes = textView.typingAttributes - - parent.typingAttributes = textView.typingAttributes } func textViewDidChangeSelection(_ textView: UITextView) { - guard !isUpdating else { return } isUpdating = true @@ -140,28 +118,11 @@ struct RichTextView: UIViewRepresentable { - - - if textView.selectedRange.length == 0 && textView.selectedRange.location > 0 { - - let location = min(textView.selectedRange.location - 1, textView.attributedText.length - 1) - if location >= 0 { - let attributes = textView.attributedText.attributes(at: location, effectiveRange: nil) - parent.typingAttributes = attributes - } - } - } - } -} - - - struct NoteEditorView: View { @Environment(\.dismiss) private var dismiss @Environment(AcademicsViewModel.self) private var academicsViewModel @Environment(AuthViewModel.self) private var authViewModel @Environment(\.presentationMode) var presentationMode - @Environment(\.presentationMode) var presentationMode @State private var attributedText = NSMutableAttributedString() @State private var selectedRange = NSRange(location: 0, length: 0) @@ -172,7 +133,6 @@ struct NoteEditorView: View { let existingNote: CreateNoteModel? let preloadedAttributedString: NSAttributedString? // Pre-processed content - let preloadedAttributedString: NSAttributedString? // Pre-processed content @State private var selectedFont: UIFont = UIFont.systemFont(ofSize: 18) @State private var selectedColor: Color = .white @State private var showFontPicker = false @@ -181,21 +141,16 @@ struct NoteEditorView: View { @State private var hasUnsavedChanges = false @State private var isInitialized = false @State private var goback = false - @State private var goback = false @Environment(\.modelContext) private var modelContext let courseCode: String let courseName: String let courseIns : String let courseSlot : String - let courseIns : String - let courseSlot : String - init(existingNote: CreateNoteModel? = nil, preloadedAttributedString: NSAttributedString? = nil, courseCode: String, courseName: String,courseIns: String , courseSlot: String) { init(existingNote: CreateNoteModel? = nil, preloadedAttributedString: NSAttributedString? = nil, courseCode: String, courseName: String,courseIns: String , courseSlot: String) { self.existingNote = existingNote self.preloadedAttributedString = preloadedAttributedString - self.preloadedAttributedString = preloadedAttributedString self.courseCode = existingNote?.courseId ?? courseCode self.courseName = existingNote?.courseName ?? courseName self.courseIns = courseIns @@ -221,17 +176,6 @@ struct NoteEditorView: View { isInitialized = true } else { - Task { @MainActor in - await loadNoteContent(note) - isInitialized = true - } - if let preloaded = preloadedAttributedString { - - attributedText = NSMutableAttributedString(attributedString: preloaded) - isEmpty = preloaded.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - isInitialized = true - } else { - Task { @MainActor in await loadNoteContent(note) isInitialized = true @@ -239,7 +183,6 @@ struct NoteEditorView: View { } } else { - attributedText = NSMutableAttributedString() isEmpty = true isInitialized = true @@ -256,15 +199,6 @@ struct NoteEditorView: View { } - do { - - if let cachedAttributedString = note.cachedAttributedString { - attributedText = NSMutableAttributedString(attributedString: cachedAttributedString) - isEmpty = cachedAttributedString.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - return - } - - do { guard let data = Data(base64Encoded: note.noteContent) else { print("Failed to decode base64 data") @@ -290,12 +224,10 @@ struct NoteEditorView: View { func saveContent() { guard hasUnsavedChanges || existingNote == nil else { - handleBackNavigation() handleBackNavigation() return } - do { let data = try NSKeyedArchiver.archivedData(withRootObject: attributedText, requiringSecureCoding: false) let dataString = data.base64EncodedString() @@ -307,8 +239,6 @@ struct NoteEditorView: View { note.createdAt = Date.now CreateNoteModel.clearCache() - - CreateNoteModel.clearCache() } else { let newNote = CreateNoteModel( noteName: title, @@ -356,26 +286,21 @@ struct NoteEditorView: View { if isInitialized { VStack { - headerView - textEditorView - toolbarView } } else { - ProgressView("Loading...") .foregroundColor(.white) } - if showFontPicker { fontPickerOverlay } @@ -402,11 +327,9 @@ struct NoteEditorView: View { private var headerView: some View { HStack { - Button(action: { handleBackNavigation() }) { Button(action: { handleBackNavigation() }) { Image(systemName: "chevron.left") .foregroundColor(Color("Accent")).font(.title2) - .foregroundColor(Color("Accent")).font(.title2) } Spacer() Text("Note") @@ -447,7 +370,6 @@ struct NoteEditorView: View { private var toolbarView: some View { HStack(spacing: 20) { - Button(action: { showFontPicker.toggle() showFontSizePicker = false @@ -457,7 +379,6 @@ struct NoteEditorView: View { } - Button(action: { showFontSizePicker.toggle() showFontPicker = false @@ -473,13 +394,11 @@ struct NoteEditorView: View { } - formatButton(action: toggleBold, icon: "bold", isActive: isBoldActive()) formatButton(action: toggleItalic, icon: "italic", isActive: isItalicActive()) formatButton(action: toggleUnderline, icon: "underline", isActive: isUnderlineActive()) - ColorPicker("", selection: $selectedColor, supportsOpacity: false) .labelsHidden() .frame(width: 30, height: 30) @@ -488,7 +407,6 @@ struct NoteEditorView: View { } - Button(action: addBulletPoints) { Image(systemName: "list.bullet") .foregroundColor(Color("Accent")) @@ -588,7 +506,6 @@ struct NoteEditorView: View { } - func addBulletPoints() { guard selectedRange.length > 0 else { return } @@ -606,12 +523,10 @@ struct NoteEditorView: View { func isBoldActive() -> Bool { return checkTraitActive(.traitBold) - return checkTraitActive(.traitBold) } func isItalicActive() -> Bool { return checkTraitActive(.traitItalic) - return checkTraitActive(.traitItalic) } func isUnderlineActive() -> Bool { @@ -641,28 +556,6 @@ struct NoteEditorView: View { } } - private func checkTraitActive(_ trait: UIFontDescriptor.SymbolicTraits) -> Bool { - if selectedRange.length > 0 { - var hasTraitThroughout = true - let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) - - attributedText.enumerateAttribute(.font, in: NSRange(location: selectedRange.location, length: endLocation - selectedRange.location), options: []) { value, range, stop in - if let font = value as? UIFont { - if !font.fontDescriptor.symbolicTraits.contains(trait) { - hasTraitThroughout = false - stop.pointee = true - } - } - } - return hasTraitThroughout - } else { - if let font = typingAttributes[.font] as? UIFont { - return font.fontDescriptor.symbolicTraits.contains(trait) - } - return false - } - } - private func getCurrentFont() -> UIFont { if selectedRange.length > 0 && selectedRange.location < attributedText.length { return attributedText.attribute(.font, at: selectedRange.location, effectiveRange: nil) as? UIFont ?? UIFont.systemFont(ofSize: 18) @@ -680,8 +573,6 @@ struct NoteEditorView: View { } func applyFontFamily(_ font: UIFont) { - let size = getCurrentFont().pointSize - let newFont = UIFont(name: font.fontName, size: size) ?? font let size = getCurrentFont().pointSize let newFont = UIFont(name: font.fontName, size: size) ?? font applyAttribute(.font, value: newFont) @@ -699,38 +590,6 @@ struct NoteEditorView: View { let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - mutableAttributedString.enumerateAttribute(.font, in: range, options: []) { value, subRange, _ in - if let font = value as? UIFont { - var traits = font.fontDescriptor.symbolicTraits - if traits.contains(.traitBold) { - traits.remove(.traitBold) - } else { - traits.insert(.traitBold) - } - if let newFontDescriptor = font.fontDescriptor.withSymbolicTraits(traits) { - let newFont = UIFont(descriptor: newFontDescriptor, size: font.pointSize) - mutableAttributedString.addAttribute(.font, value: newFont, range: subRange) - } - } - } - attributedText = mutableAttributedString - } else { - let currentFont = typingAttributes[.font] as? UIFont ?? UIFont.systemFont(ofSize: 18) - var traits = currentFont.fontDescriptor.symbolicTraits - if traits.contains(.traitBold) { - traits.remove(.traitBold) - } else { - traits.insert(.traitBold) - } - if let newFontDescriptor = currentFont.fontDescriptor.withSymbolicTraits(traits) { - let newFont = UIFont(descriptor: newFontDescriptor, size: currentFont.pointSize) - typingAttributes[.font] = newFont - } - if selectedRange.length > 0 { - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) - let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - mutableAttributedString.enumerateAttribute(.font, in: range, options: []) { value, subRange, _ in if let font = value as? UIFont { var traits = font.fontDescriptor.symbolicTraits @@ -760,7 +619,6 @@ struct NoteEditorView: View { } } hasUnsavedChanges = true - hasUnsavedChanges = true } func toggleItalic() { @@ -769,38 +627,6 @@ struct NoteEditorView: View { let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - mutableAttributedString.enumerateAttribute(.font, in: range, options: []) { value, subRange, _ in - if let font = value as? UIFont { - var traits = font.fontDescriptor.symbolicTraits - if traits.contains(.traitItalic) { - traits.remove(.traitItalic) - } else { - traits.insert(.traitItalic) - } - if let newFontDescriptor = font.fontDescriptor.withSymbolicTraits(traits) { - let newFont = UIFont(descriptor: newFontDescriptor, size: font.pointSize) - mutableAttributedString.addAttribute(.font, value: newFont, range: subRange) - } - } - } - attributedText = mutableAttributedString - } else { - let currentFont = typingAttributes[.font] as? UIFont ?? UIFont.systemFont(ofSize: 18) - var traits = currentFont.fontDescriptor.symbolicTraits - if traits.contains(.traitItalic) { - traits.remove(.traitItalic) - } else { - traits.insert(.traitItalic) - } - if let newFontDescriptor = currentFont.fontDescriptor.withSymbolicTraits(traits) { - let newFont = UIFont(descriptor: newFontDescriptor, size: currentFont.pointSize) - typingAttributes[.font] = newFont - } - if selectedRange.length > 0 { - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) - let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - mutableAttributedString.enumerateAttribute(.font, in: range, options: []) { value, subRange, _ in if let font = value as? UIFont { var traits = font.fontDescriptor.symbolicTraits @@ -830,10 +656,8 @@ struct NoteEditorView: View { } } hasUnsavedChanges = true - hasUnsavedChanges = true } - func toggleUnderline() { if selectedRange.length > 0 { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) @@ -852,23 +676,6 @@ struct NoteEditorView: View { typingAttributes[.underlineStyle] = newUnderline } hasUnsavedChanges = true - if selectedRange.length > 0 { - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) - let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - - mutableAttributedString.enumerateAttribute(.underlineStyle, in: range, options: []) { value, subRange, _ in - let currentUnderline = value as? Int ?? 0 - let newUnderline = currentUnderline == NSUnderlineStyle.single.rawValue ? 0 : NSUnderlineStyle.single.rawValue - mutableAttributedString.addAttribute(.underlineStyle, value: newUnderline, range: subRange) - } - attributedText = mutableAttributedString - } else { - let currentUnderline = typingAttributes[.underlineStyle] as? Int ?? 0 - let newUnderline = currentUnderline == NSUnderlineStyle.single.rawValue ? 0 : NSUnderlineStyle.single.rawValue - typingAttributes[.underlineStyle] = newUnderline - } - hasUnsavedChanges = true } func applyAttribute(_ key: NSAttributedString.Key, value: Any) { @@ -877,9 +684,6 @@ struct NoteEditorView: View { let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) mutableAttributedString.addAttribute(key, value: value, range: range) - let endLocation = min(selectedRange.location + selectedRange.length, attributedText.length) - let range = NSRange(location: selectedRange.location, length: endLocation - selectedRange.location) - mutableAttributedString.addAttribute(key, value: value, range: range) attributedText = mutableAttributedString } else { typingAttributes[key] = value diff --git a/VITTY/VITTY/Academics/View/RemindersData.swift b/VITTY/VITTY/Academics/View/RemindersData.swift index f6937a5..08722b0 100644 --- a/VITTY/VITTY/Academics/View/RemindersData.swift +++ b/VITTY/VITTY/Academics/View/RemindersData.swift @@ -7,12 +7,10 @@ import SwiftUI import SwiftData - struct RemindersView: View { @Environment(\.modelContext) private var modelContext @Query private var allReminders: [Remainder] @Query private var timeTables: [TimeTable] - @Query private var timeTables: [TimeTable] @State private var searchText = "" @State private var selectedTab = 0 @@ -67,17 +65,10 @@ struct RemindersView: View { }.sorted { $0.daysToGo < $1.daysToGo } } - // Extract courses from timetable - private var availableCourses: [Course] { - let courses = timeTables.first.map { extractCourses(from: $0) } ?? [] - return courses - } - var body: some View { ScrollView { VStack(spacing: 0) { - HStack { Image(systemName: "magnifyingglass") .foregroundColor(.gray) diff --git a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift index 5181cb3..7805525 100644 --- a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift +++ b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift @@ -57,9 +57,6 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { - - - var isLoading: Bool = false var isLoadingApple: Bool = false let firebaseAuth = Auth.auth() @@ -166,11 +163,9 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { func signInServer(username: String, regNo: String) async { - logger.info("Signing into server... from uuid \(self.loggedInFirebaseUser?.uid ?? "empty")") logger.info("Signing into server... from uuid \(self.loggedInFirebaseUser?.uid ?? "empty")") do { - self.loggedInBackendUser = try await AuthAPIService.shared .signInUser( with: AuthRequestBody( @@ -189,8 +184,6 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { } print("this is kinda empty : \(self.loggedInBackendUser?.name ?? "")") logger.info("Signed into server \(self.loggedInBackendUser?.name ?? "empty")") - print("this is kinda empty : \(self.loggedInBackendUser?.name ?? "")") - logger.info("Signed into server \(self.loggedInBackendUser?.name ?? "empty")") } diff --git a/VITTY/VITTY/Connect/Models/CircleModel.swift b/VITTY/VITTY/Connect/Models/CircleModel.swift index 7323d35..eaea8f2 100644 --- a/VITTY/VITTY/Connect/Models/CircleModel.swift +++ b/VITTY/VITTY/Connect/Models/CircleModel.swift @@ -82,7 +82,6 @@ struct CircleUserTemp: Codable { struct CircleUserResponseTemp: Codable { let data: [CircleUserTemp] - enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey { case data } diff --git a/VITTY/VITTY/Connect/View/Circles/Components/CirclesRow.swift b/VITTY/VITTY/Connect/View/Circles/Components/CirclesRow.swift index 94420d9..6808ae4 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/CirclesRow.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/CirclesRow.swift @@ -18,30 +18,6 @@ struct CirclesRow: View { } - private var busyCount: Int { - circleMembers.filter { - $0.status != nil && $0.status != "available" && $0.status != "free" - }.count - } - - private var availableCount: Int { - circleMembers.filter { - $0.status == nil || $0.status == "available" || $0.status == "free" - }.count - } - - private var isLoadingMembers: Bool { - communityPageViewModel.isLoadingCircleMembers(for: circle.circleID) - } - @Environment(CommunityPageViewModel.self) private var communityPageViewModel - @Environment(AuthViewModel.self) private var authViewModel - - - private var circleMembers: [CircleUserTemp] { - communityPageViewModel.circleMembers(for: circle.circleID) - } - - private var busyCount: Int { circleMembers.filter { $0.status != nil && $0.status != "available" && $0.status != "free" @@ -99,40 +75,6 @@ struct CirclesRow: View { } - if circleMembers.isEmpty && !isLoadingMembers { - Text("No members") - .font(Font.custom("Poppins-Regular", size: 12)) - .foregroundStyle(Color("Accent").opacity(0.7)) - } - } - } - if isLoadingMembers { - HStack { - ProgressView() - .scaleEffect(0.7) - Text("Loading...") - .font(Font.custom("Poppins-Regular", size: 12)) - .foregroundStyle(Color("Accent")) - } - } else { - HStack { - - if busyCount > 0 { - Image("inclass").resizable().frame(width: 20, height: 20) - Text("\(busyCount) busy").foregroundStyle(Color("Accent")) - - if availableCount > 0 { - Spacer().frame(width: 20) - } - } - - - if availableCount > 0 { - Image("available").resizable().frame(width: 20, height: 20) - Text("\(availableCount) available").foregroundStyle(Color("Accent")) - } - - if circleMembers.isEmpty && !isLoadingMembers { Text("No members") .font(Font.custom("Poppins-Regular", size: 12)) @@ -163,7 +105,6 @@ struct CirclesRow: View { func cleanName(_ fullName: String) -> String { - let pattern = "\\b\\d{2}[A-Z]+\\d+\\b" let pattern = "\\b\\d{2}[A-Z]+\\d+\\b" let regex = try? NSRegularExpression(pattern: pattern, options: []) diff --git a/VITTY/VITTY/Connect/View/Circles/Components/CreateGroup.swift b/VITTY/VITTY/Connect/View/Circles/Components/CreateGroup.swift index 1b2df7b..61567ce 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/CreateGroup.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/CreateGroup.swift @@ -6,7 +6,6 @@ import SwiftUI import Alamofire -import Alamofire struct CreateGroup: View { let screenHeight = UIScreen.main.bounds.height @@ -44,7 +43,6 @@ struct CreateGroup: View { Spacer().frame(height: 20) - Button(action: { showImagePicker = true }) { @@ -122,7 +120,6 @@ struct CreateGroup: View { Button(action: { showFriendSelector = true - showFriendSelector = true }) { Image(systemName: "person.badge.plus") .foregroundColor(.white) @@ -226,7 +223,6 @@ struct CreateGroup: View { Spacer() Button(action: { createGroup() - createGroup() }) { HStack { if isCreatingGroup { @@ -243,7 +239,6 @@ struct CreateGroup: View { .cornerRadius(10) } .disabled(groupName.isEmpty || isCreatingGroup) - .disabled(groupName.isEmpty || isCreatingGroup) .padding(.trailing, 20) } .padding(.bottom, 20) diff --git a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift index 3ed9272..00a4f94 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift @@ -3,21 +3,14 @@ // // Created by Rujin Devkota on 2/28/25. // -// JoinGroup.swift -// VITTY -// -// Created by Rujin Devkota on 2/28/25. -// import SwiftUI import AVFoundation import UIKit -import UIKit struct JoinGroup: View { let screenHeight = UIScreen.main.bounds.height let screenWidth = UIScreen.main.bounds.width - @Binding var groupCode: String @State private var isScanning = false @State private var scannedCode: String = "" @@ -33,18 +26,6 @@ struct JoinGroup: View { @Environment(CommunityPageViewModel.self) private var communityPageViewModel @Environment(\.dismiss) private var dismiss - @State private var showingAlert = false - @State private var alertMessage = "" - @State private var isJoining = false - @State private var showToast = false - @State private var toastMessage = "" - @State private var circleName = "" - @State private var localGroupCode = "" - - @Environment(AuthViewModel.self) private var authViewModel - @Environment(CommunityPageViewModel.self) private var communityPageViewModel - @Environment(\.dismiss) private var dismiss - var body: some View { ZStack { VStack(spacing: 20) { diff --git a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift index 24f0fac..3056419 100644 --- a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift +++ b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift @@ -382,7 +382,6 @@ struct InsideCircle: View { }) { Image(systemName: "chevron.left") .foregroundColor(.white).font(.title2) - .foregroundColor(.white).font(.title2) } Spacer() Text("Circle") @@ -391,13 +390,10 @@ struct InsideCircle: View { Spacer() Button(action: { showCircleMenu = true - showCircleMenu = true }) { - Image(systemName: "ellipsis") Image(systemName: "ellipsis") .foregroundColor(.white) .font(.system(size: 18)) - .font(.system(size: 18)) } } .padding() @@ -413,7 +409,6 @@ struct InsideCircle: View { .foregroundColor(.white) Spacer() - } Spacer().frame(height: 5) HStack { diff --git a/VITTY/VITTY/Connect/View/ConnectPage.swift b/VITTY/VITTY/Connect/View/ConnectPage.swift index 238cb83..d04ac36 100644 --- a/VITTY/VITTY/Connect/View/ConnectPage.swift +++ b/VITTY/VITTY/Connect/View/ConnectPage.swift @@ -34,16 +34,12 @@ struct ConnectPage: View { @State private var activeSheet: SheetType? @State private var showCircleMenu = false @Environment(\.dismiss) private var dismiss - @State private var activeSheet: SheetType? - @State private var showCircleMenu = false - @Environment(\.dismiss) private var dismiss @Binding var isCreatingGroup : Bool @State private var isAddFriendsViewPresented = false @State private var selectedTab = 0 @State private var hasLoadedInitialData = false - @State private var hasLoadedInitialData = false var body: some View { ZStack { @@ -72,7 +68,6 @@ struct ConnectPage: View { .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) } - if isCircleView == false { Button(action: { isShowingRequestView.toggle() @@ -106,22 +101,15 @@ struct ConnectPage: View { } ) .offset(x: UIScreen.main.bounds.width*0.4228, y: UIScreen.main.bounds.height*0.38901*(-1)) - } else { - ) - .offset(x: UIScreen.main.bounds.width*0.4228, y: UIScreen.main.bounds.height*0.38901*(-1)) } else { Button(action: { showCircleMenu = true - showCircleMenu = true }) { - Image(systemName: "ellipsis") Image(systemName: "ellipsis") .foregroundColor(.white) .font(.system(size: 18)) - .font(.system(size: 18)) } .offset(x: UIScreen.main.bounds.width*0.4228, y: UIScreen.main.bounds.height*0.38901*(-1)) - .offset(x: UIScreen.main.bounds.width*0.4228, y: UIScreen.main.bounds.height*0.38901*(-1)) } } .overlay( diff --git a/VITTY/VITTY/Connect/View/Freinds/View/Freinds.swift b/VITTY/VITTY/Connect/View/Freinds/View/Freinds.swift index 5c2a02c..f477d6a 100644 --- a/VITTY/VITTY/Connect/View/Freinds/View/Freinds.swift +++ b/VITTY/VITTY/Connect/View/Freinds/View/Freinds.swift @@ -6,7 +6,6 @@ // import SwiftUI - struct FriendsView: View { @State private var searchText = "" @State private var selectedFilterOption = 0 @@ -21,8 +20,6 @@ struct FriendsView: View { Spacer().frame(height: 8) - - HStack { FilterPill(title: "Available", isSelected: selectedFilterOption == 0) .onTapGesture { @@ -38,7 +35,6 @@ struct FriendsView: View { Spacer().frame(height: 7) - if communityPageViewModel.errorFreinds { Spacer() VStack(spacing: 5) { @@ -58,35 +54,17 @@ struct FriendsView: View { Spacer() } else { - let filteredFriends = communityPageViewModel.friends.filter { friend in - let matchesSearch: Bool - let matchesSearch: Bool if searchText.isEmpty { matchesSearch = true - matchesSearch = true } else { - matchesSearch = friend.username.localizedCaseInsensitiveContains(searchText) || matchesSearch = friend.username.localizedCaseInsensitiveContains(searchText) || (friend.name.localizedCaseInsensitiveContains(searchText) ?? false) } - let matchesFilter: Bool - switch selectedFilterOption { - case 0: - matchesFilter = friend.currentStatus.status == "free" - case 1: - matchesFilter = true - default: - matchesFilter = true - } - - return matchesSearch && matchesFilter - - let matchesFilter: Bool switch selectedFilterOption { case 0: @@ -116,26 +94,11 @@ struct FriendsView: View { .font(Font.custom("Poppins-Regular", size: 16)) .foregroundColor(.white) .multilineTextAlignment(.center) - VStack(spacing: 5) { - if selectedFilterOption == 0 && !searchText.isEmpty { - Text("No available friends match your search") - } else if selectedFilterOption == 0 { - Text("No friends are currently available") - } else if !searchText.isEmpty { - Text("No friends match your search") - } else { - Text("You don't have any friends yet") - } - } - .font(Font.custom("Poppins-Regular", size: 16)) - .foregroundColor(.white) - .multilineTextAlignment(.center) Spacer() } else { ScrollView { VStack(spacing: 10) { ForEach(filteredFriends, id: \.username) { friend in - NavigationLink(destination: TimeTableView(friend: friend,isFriendsTimeTable: true)) { NavigationLink(destination: TimeTableView(friend: friend,isFriendsTimeTable: true)) { FriendRow(friend: friend) } diff --git a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift index 186343a..8ed78a1 100644 --- a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift +++ b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift @@ -5,7 +5,6 @@ // Created by Chandram Dutta on 04/01/24. // // -// import Foundation import Alamofire @@ -17,28 +16,19 @@ class CommunityPageViewModel { var circles = [CircleModel]() var circleRequests = [CircleRequest]() - var circleRequests = [CircleRequest]() - var loadingFreinds = false var loadingCircle = false var loadingCircleMembers = false var loadingCircleRequests = false var loadingRequestAction = false - var loadingCircleRequests = false - var loadingRequestAction = false var errorFreinds = false var errorCircle = false var errorCircleMembers = false var errorCircleRequests = false - var errorCircleRequests = false - var circleMembers = [CircleUserTemp]() - var circleMembersDict: [String: [CircleUserTemp]] = [:] - var loadingCircleMembersDict: [String: Bool] = [:] - var circleMembersDict: [String: [CircleUserTemp]] = [:] var loadingCircleMembersDict: [String: Bool] = [:] @@ -66,26 +56,14 @@ class CommunityPageViewModel { DispatchQueue.main.async { self.loadingFreinds = false - switch response.result { - DispatchQueue.main.async { - self.loadingFreinds = false - switch response.result { case .success(let data): self.friends = data.data self.errorFreinds = false - self.errorFreinds = false - case .failure(let error): self.logger.error("Error fetching friends: \(error)") - if self.friends.isEmpty { - self.errorFreinds = true - } - } - self.logger.error("Error fetching friends: \(error)") - if self.friends.isEmpty { self.errorFreinds = true } @@ -103,15 +81,6 @@ class CommunityPageViewModel { } - self.errorCircle = false - - func fetchCircleData(from url: String, token: String, loading: Bool = false) { - - if loading || circles.isEmpty { - self.loadingCircle = true - } - - self.errorCircle = false AF.request(url, method: .get, headers: ["Authorization": "Token \(token)"]) @@ -120,19 +89,12 @@ class CommunityPageViewModel { DispatchQueue.main.async { self.loadingCircle = false - switch response.result { - DispatchQueue.main.async { - self.loadingCircle = false - switch response.result { case .success(let data): self.circles = data.data self.errorCircle = false print("Successfully fetched circles: \(data.data)") - self.errorCircle = false - print("Successfully fetched circles: \(data.data)") - case .failure(let error): self.logger.error("Error fetching circles: \(error)") @@ -285,8 +247,6 @@ class CommunityPageViewModel { AF.request(url, method: .get, headers: ["Authorization": "Token \(token)"]) .validate() .responseDecodable(of: CircleUserResponseTemp.self) { response in - DispatchQueue.main.async { - switch response.result { DispatchQueue.main.async { switch response.result { case .success(let data): @@ -299,29 +259,9 @@ class CommunityPageViewModel { } print("Successfully fetched circle members: \(data.data)") - if let circleID = circleID { - self.circleMembersDict[circleID] = data.data - self.loadingCircleMembersDict[circleID] = false - } else { - self.circleMembers = data.data - self.loadingCircleMembers = false - } - print("Successfully fetched circle members: \(data.data)") - case .failure(let error): self.logger.error("Error fetching circle members: \(error)") - if let circleID = circleID { - self.loadingCircleMembersDict[circleID] = false - } else { - self.loadingCircleMembers = false - if self.circleMembers.isEmpty { - self.errorCircleMembers = true - } - } - } - self.logger.error("Error fetching circle members: \(error)") - if let circleID = circleID { self.loadingCircleMembersDict[circleID] = false } else { @@ -335,12 +275,7 @@ class CommunityPageViewModel { } } - //MARK : Circle Leave - func fetchCircleLeave(from url: String, token: String, loading: Bool = false) { - if loading { - self.loadingCircleMembers = true - } func fetchCircleLeave(from url: String, token: String, loading: Bool = false) { if loading { self.loadingCircleMembers = true @@ -349,31 +284,20 @@ class CommunityPageViewModel { AF.request(url, method: .get, headers: ["Authorization": "Token \(token)"]) .validate() .responseDecodable(of: CircleUserResponseTemp.self) { response in - DispatchQueue.main.async { - self.loadingCircleMembers = false DispatchQueue.main.async { self.loadingCircleMembers = false - switch response.result { switch response.result { case .success(let data): self.circleMembers = data.data print("Successfully fetched circle members after leave: \(data.data)") - self.circleMembers = data.data - print("Successfully fetched circle members after leave: \(data.data)") - case .failure(let error): self.logger.error("Error fetching circle members: \(error)") if self.circleMembers.isEmpty { self.errorCircleMembers = true } } - self.logger.error("Error fetching circle members: \(error)") - if self.circleMembers.isEmpty { - self.errorCircleMembers = true - } - } } } } diff --git a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift index af0eb8a..ae92f18 100644 --- a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift +++ b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift @@ -3,7 +3,6 @@ import SwiftUI import UserNotifications class SettingsViewModel : ObservableObject{ - @Published var notificationsEnabled: Bool = false { @Published var notificationsEnabled: Bool = false { didSet { UserDefaults.standard.set(notificationsEnabled, forKey: "notificationsEnabled") @@ -18,15 +17,12 @@ class SettingsViewModel : ObservableObject{ } } - @Published var timetable: TimeTable? - @Published var showNotificationDisabledAlert = false @Published var timetable: TimeTable? @Published var showNotificationDisabledAlert = false init(timetable: TimeTable? = nil) { self.timetable = timetable - self.notificationsEnabled = UserDefaults.standard.bool(forKey: "notificationsEnabled") checkNotificationAuthorization() } @@ -59,9 +55,6 @@ class SettingsViewModel : ObservableObject{ // Clear existing notifications first UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - // Clear existing notifications first - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - let weekdays: [(Int, [Lecture])] = [ (2, timetable.monday), // Monday = 2 (3, timetable.tuesday), // Tuesday = 3 @@ -70,13 +63,6 @@ class SettingsViewModel : ObservableObject{ (6, timetable.friday), // Friday = 6 (7, timetable.saturday), // Saturday = 7 (1, timetable.sunday) // Sunday = 1 - (2, timetable.monday), // Monday = 2 - (3, timetable.tuesday), // Tuesday = 3 - (4, timetable.wednesday), // Wednesday = 4 - (5, timetable.thursday), // Thursday = 5 - (6, timetable.friday), // Friday = 6 - (7, timetable.saturday), // Saturday = 7 - (1, timetable.sunday) // Sunday = 1 ] for (weekday, lectures) in weekdays { @@ -87,24 +73,14 @@ class SettingsViewModel : ObservableObject{ } - guard let startDate = parseLectureTime(lecture.startTime, weekday: weekday) else { - print("Failed to parse time for lecture: \(lecture.name) with time: \(lecture.startTime)") - continue - } - - scheduleNotification(for: lecture.name, at: startDate, title: "Class Starting", minutesBefore: 0) - - scheduleNotification(for: lecture.name, at: startDate, title: "Upcoming Class", minutesBefore: 10) } } print("Scheduled notifications for all lectures") - - print("Scheduled notifications for all lectures") } private func scheduleNotification(for lectureName: String, at date: Date, title: String, minutesBefore: Int) { @@ -118,10 +94,8 @@ class SettingsViewModel : ObservableObject{ let triggerComponents = Calendar.current.dateComponents([.weekday, .hour, .minute], from: triggerDate) let trigger = UNCalendarNotificationTrigger(dateMatching: triggerComponents, repeats: true) - let identifier = "\(lectureName)-\(title)-\(minutesBefore)min-weekday\(triggerComponents.weekday ?? 0)" let identifier = "\(lectureName)-\(title)-\(minutesBefore)min-weekday\(triggerComponents.weekday ?? 0)" let request = UNNotificationRequest( - identifier: identifier, identifier: identifier, content: content, trigger: trigger @@ -134,18 +108,9 @@ class SettingsViewModel : ObservableObject{ print("Successfully scheduled notification: \(identifier)") } } - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("Error scheduling notification: \(error)") - } else { - print("Successfully scheduled notification: \(identifier)") - } - } } - - private func parseLectureTime(_ timeString: String, weekday: Int) -> Date? { let formattedTimeString = formatTime(time: timeString) diff --git a/VITTY/VITTY/TimeTable/Models/TimeTable.swift b/VITTY/VITTY/TimeTable/Models/TimeTable.swift index d958067..da1201d 100644 --- a/VITTY/VITTY/TimeTable/Models/TimeTable.swift +++ b/VITTY/VITTY/TimeTable/Models/TimeTable.swift @@ -209,7 +209,6 @@ class Lecture: Codable, Identifiable, Comparable { } } - extension TimeTable { var isEmpty: Bool { monday.isEmpty && tuesday.isEmpty && wednesday.isEmpty && @@ -220,26 +219,14 @@ extension TimeTable { let formattedTime = formatTime(time: lecture.startTime) - guard formattedTime != "Failed to parse the time string." else { return nil } - - private func extractStartTime(from lecture: Lecture) -> Date? { - let formattedTime = formatTime(time: lecture.startTime) - - guard formattedTime != "Failed to parse the time string." else { return nil } - let formatter = DateFormatter() - formatter.dateFormat = "h:mm a" - formatter.locale = Locale(identifier: "en_US_POSIX") let formatter = DateFormatter() formatter.dateFormat = "h:mm a" formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.date(from: formattedTime) } - return formatter.date(from: formattedTime) - } - func classesFor(date: Date) -> [Classes] { @@ -266,10 +253,6 @@ extension TimeTable { ) } - // Sort using the original lecture objects instead of formatted strings - return lectures.sorted { lecture1, lecture2 in - guard let time1 = extractStartTime(from: lecture1), - let time2 = extractStartTime(from: lecture2) else { // Sort using the original lecture objects instead of formatted strings return lectures.sorted { lecture1, lecture2 in guard let time1 = extractStartTime(from: lecture1), @@ -285,15 +268,6 @@ extension TimeTable { ) } } - return time1 < time2 - }.map { - Classes( - title: $0.name, - time: "\(formatTime(time: $0.startTime)) - \(formatTime(time: $0.endTime))", - slot: $0.slot - ) - } - } private func formatTime(time: String) -> String { var timeComponents = time.components(separatedBy: "T").last ?? "" @@ -312,26 +286,6 @@ extension TimeTable { } } - func isDifferentFrom(_ other: TimeTable) -> Bool { - return monday != other.monday || - tuesday != other.tuesday || - wednesday != other.wednesday || - thursday != other.thursday || - friday != other.friday || - saturday != other.saturday || - sunday != other.sunday - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss" - if let date = dateFormatter.date(from: timeComponents) { - dateFormatter.dateFormat = "h:mm a" - let formattedTime = dateFormatter.string(from: date) - return (formattedTime) - } - else { - return ("Failed to parse the time string.") - } - } - func isDifferentFrom(_ other: TimeTable) -> Bool { return monday != other.monday || tuesday != other.tuesday || diff --git a/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift b/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift index 7eb83d0..e86dcac 100644 --- a/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift +++ b/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift @@ -14,12 +14,8 @@ public enum Stage { case loading case error case data - case loading - case error - case data } - extension TimeTableView { @Observable class TimeTableViewModel { @@ -248,9 +244,6 @@ extension TimeTableView { } } - var updatedTimeTable: TimeTable? { - timeTable - } var updatedTimeTable: TimeTable? { timeTable } @@ -260,11 +253,4 @@ extension TimeTableView { logger.debug("Sync status reset") } } - func resetSyncStatus() { - hasSyncedThisSession = false - logger.debug("Sync status reset") - } - } } - - diff --git a/VITTY/VITTY/TimeTable/Views/LectureDetailView.swift b/VITTY/VITTY/TimeTable/Views/LectureDetailView.swift index 4140f97..989713c 100644 --- a/VITTY/VITTY/TimeTable/Views/LectureDetailView.swift +++ b/VITTY/VITTY/TimeTable/Views/LectureDetailView.swift @@ -105,15 +105,4 @@ struct LectureDetailView: View { return ("Failed to parse the time string.") } } - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss" - if let date = dateFormatter.date(from: timeComponents) { - dateFormatter.dateFormat = "h:mm a" - let formattedTime = dateFormatter.string(from: date) - return (formattedTime) - } - else { - return ("Failed to parse the time string.") - } - } } diff --git a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift index f7ec92a..2795344 100644 --- a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift +++ b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift @@ -1,5 +1,4 @@ - import OSLog import SwiftData import SwiftUI @@ -8,22 +7,15 @@ struct TimeTableView: View { @Environment(AuthViewModel.self) private var authViewModel @Environment(\.modelContext) private var context @Environment(\.scenePhase) private var scenePhase - @Environment(\.scenePhase) private var scenePhase private let daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - @State private var viewModel = TimeTableViewModel() @State private var selectedLecture: Lecture? = nil @Query private var timetableItem: [TimeTable] @Environment(\.dismiss) private var dismiss - @Query private var timetableItem: [TimeTable] - @Environment(\.dismiss) private var dismiss let friend: Friend? - var isFriendsTimeTable: Bool - - var isFriendsTimeTable: Bool private let logger = Logger( @@ -33,9 +25,7 @@ struct TimeTableView: View { ) ) - var body: some View { - NavigationStack { NavigationStack { ZStack { BackgroundView() @@ -50,18 +40,6 @@ struct TimeTableView: View { }.padding(8) } - switch viewModel.stage { - VStack { - if isFriendsTimeTable { - HStack { - Button(action: { dismiss() }) { - Image(systemName: "chevron.left") - .foregroundColor(Color("Accent")).font(.title2) - } - Spacer() - }.padding(8) - } - switch viewModel.stage { case .loading: VStack { @@ -86,12 +64,10 @@ struct TimeTableView: View { Text(day) .foregroundStyle(daysOfWeek[viewModel.dayNo] == day ? Color("Background") : Color("Accent")) - ? Color("Background") : Color("Accent")) .frame(width: 60, height: 54) .background( daysOfWeek[viewModel.dayNo] == day ? Color("Accent") : Color.clear - ? Color("Accent") : Color.clear ) .onTapGesture { withAnimation { @@ -110,7 +86,6 @@ struct TimeTableView: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) - if viewModel.lectures.isEmpty { Spacer() Text("No classes today!") diff --git a/VITTY/VITTY/UserProfileSideBar/SideBar.swift b/VITTY/VITTY/UserProfileSideBar/SideBar.swift index 8bc6646..f6fa929 100644 --- a/VITTY/VITTY/UserProfileSideBar/SideBar.swift +++ b/VITTY/VITTY/UserProfileSideBar/SideBar.swift @@ -16,14 +16,12 @@ struct UserProfileSidebar: View { ZStack(alignment: .topTrailing) { Button { isPresented = false - isPresented = false } label: { Image(systemName: "xmark") .foregroundColor(.white) .padding() } - VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 8) { UserImage( @@ -40,34 +38,22 @@ struct UserProfileSidebar: View { } .padding(.top, 40) - Divider().background(Color.clear) - NavigationLink { EmptyClassRoom() } label: { MenuOption(icon: "emptyclassroom", title: "Find Empty Classroom") } - NavigationLink { SettingsView() } label: { MenuOption(icon: "settings", title: "Settings") } - Divider().background(Color.clear) -// MenuOption(icon: "share", title: "Share") - MenuOption(icon: "support", title: "Support").onTapGesture { - let supportUrl = URL(string: "https://github.com/GDGVIT/vitty-ios/issues/new?template=bug_report.md") - UIApplication.shared.open(supportUrl!) - } -// MenuOption(icon: "about", title: "About") - - // MenuOption(icon: "share", title: "Share") MenuOption(icon: "support", title: "Support").onTapGesture { let supportUrl = URL(string: "https://github.com/GDGVIT/vitty-ios/issues/new?template=bug_report.md") @@ -77,7 +63,6 @@ struct UserProfileSidebar: View { Divider().background(Color.clear) - VStack(alignment: .leading, spacing: 4) { Text("Ghost Mode") .font(Font.custom("Poppins-Medium", size: 16)) @@ -102,29 +87,10 @@ struct UserProfileSidebar: View { .foregroundColor(.white) } } - - HStack { - Toggle("", isOn: $ghostMode) - .labelsHidden() - .toggleStyle(SwitchToggleStyle(tint: Color("Accent"))) - .disabled(isUpdatingGhostMode) - .padding(.top, 4) - .onChange(of: ghostMode) { oldValue, newValue in - updateGhostMode(enabled: newValue) - } - - if isUpdatingGhostMode { - ProgressView() - .scaleEffect(0.8) - .foregroundColor(.white) - } - } } - Spacer() - Button { authViewModel.signOut() do{ @@ -176,68 +142,6 @@ struct UserProfileSidebar: View { isUpdatingGhostMode = true - let endpoint = enabled ? "ghost" : "alive" - let urlString = "\(APIConstants.base_url)friends/\(endpoint)/\(username)" - - guard let url = URL(string: urlString) else { - isUpdatingGhostMode = false - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - URLSession.shared.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { - isUpdatingGhostMode = false - - if let error = error { - print("Ghost mode update failed: \(error.localizedDescription)") - - ghostMode = !enabled - return - } - - if let httpResponse = response as? HTTPURLResponse { - if httpResponse.statusCode == 200 { - - UserDefaults.standard.set(enabled, forKey: "ghostMode_\(username)") - print("Ghost mode \(enabled ? "enabled" : "disabled") successfully") - } else { - print("Ghost mode update failed with status code: \(httpResponse.statusCode)") - - ghostMode = !enabled - } - } - } - }.resume() - .transition(.move(edge: .trailing)) - } - .animation(.easeInOut(duration: 0.3), value: isPresented) - .onAppear { - loadGhostModeState() - } - } - - // MARK: - Ghost Mode Functions - - private func loadGhostModeState() { - - let username = authViewModel.loggedInBackendUser?.username ?? "" - ghostMode = UserDefaults.standard.bool(forKey: "ghostMode_\(username)") - } - - private func updateGhostMode(enabled: Bool) { - guard let username = authViewModel.loggedInBackendUser?.username, - let token = authViewModel.loggedInBackendUser?.token else { - return - } - - isUpdatingGhostMode = true - - let endpoint = enabled ? "ghost" : "alive" let urlString = "\(APIConstants.base_url)friends/\(endpoint)/\(username)" @@ -283,8 +187,6 @@ struct MenuOption: View { let title: String - - var body: some View { HStack(spacing: 16) { Image(icon) From c04133411f116cff1b0fb17836e8e0cf25fc32ed Mon Sep 17 00:00:00 2001 From: rujin2003 Date: Sun, 6 Jul 2025 01:04:46 +0545 Subject: [PATCH 2/4] fix: sat timetable craches --- .../VITTY/Auth/ViewModels/AuthViewModel.swift | 1 + VITTY/VITTY/Settings/View/SettingsView.swift | 358 ++++++++++++----- VITTY/VITTY/Shared/Constants.swift | 2 +- VITTY/VITTY/TimeTable/Models/TimeTable.swift | 101 ++--- .../ViewModel/TimeTableViewModel.swift | 370 ++++++++++++------ .../TimeTable/Views/LectureItemView.swift | 2 +- .../VITTY/TimeTable/Views/TimeTableView.swift | 135 ++++++- .../Utilities/Constants/APIConstants.swift | 2 +- VITTY/VittyWidget/Views/LargeWidget.swift | 57 +-- VITTY/VittyWidget/Views/SmallWidget.swift | 2 + .../VittyWidget/Views/WidgetComponents.swift | 1 + 11 files changed, 692 insertions(+), 339 deletions(-) diff --git a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift index 7805525..3133b69 100644 --- a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift +++ b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift @@ -338,6 +338,7 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { do { try firebaseAuth.signOut() UserDefaults.resetDefaults() + } catch { logger.error("Error Signing Out: \(error)") diff --git a/VITTY/VITTY/Settings/View/SettingsView.swift b/VITTY/VITTY/Settings/View/SettingsView.swift index 19fbd36..2392331 100644 --- a/VITTY/VITTY/Settings/View/SettingsView.swift +++ b/VITTY/VITTY/Settings/View/SettingsView.swift @@ -1,9 +1,6 @@ import SwiftUI import SwiftData - - - struct SettingsView: View { @Environment(AuthViewModel.self) private var authViewModel @Environment(\.dismiss) private var dismiss @@ -12,12 +9,12 @@ struct SettingsView: View { @StateObject private var viewModel = SettingsViewModel() - @State private var showDaySelection = false @State private var selectedDay: String? = nil @State private var showResetAlert = false + @State private var showDeleteUserAlert = false + @State private var isDeletingUser = false - private let selectedDayKey = "SelectedSaturdayDay" var body: some View { @@ -51,12 +48,14 @@ struct SettingsView: View { SettingsSectionView(title: "Class Settings") { VStack(alignment: .leading, spacing: 12) { Button { - showDaySelection.toggle() + withAnimation { + showDaySelection.toggle() + } } label: { SettingsRowView( icon: "calendar.badge.plus", title: "Saturday Class", - subtitle: selectedDay == nil ? "Select a day to copy classes to Saturday" : "Copy \(selectedDay!) classes to Saturday" + subtitle: selectedDay == nil ? "Select a day to copy to Saturday" : "Saturday classes are a copy of \(selectedDay!)" ) } .buttonStyle(PlainButtonStyle()) @@ -67,31 +66,24 @@ struct SettingsView: View { HStack(spacing: 12) { Image(systemName: selectedDay == day ? "largecircle.fill.circle" : "circle") .foregroundColor(.blue) - .font(.system(size: 16)) Text(day) .foregroundColor(.white) - .font(.system(size: 14)) Spacer() } - .padding(.leading, 16) - .padding(.vertical, 4) + .padding([.leading, .vertical], 4) .contentShape(Rectangle()) .onTapGesture { - selectedDay = day - UserDefaults.standard.set(day, forKey: selectedDayKey) copyLecturesToSaturday(from: day) - showDaySelection = false + withAnimation { + showDaySelection = false + } } } } .padding(.top, 8) - .transition(.asymmetric( - insertion: .opacity.combined(with: .scale(scale: 0.95, anchor: .top)), - removal: .opacity.combined(with: .scale(scale: 0.95, anchor: .top)) - )) + .transition(.opacity.combined(with: .scale(scale: 0.95, anchor: .top))) } - Button { showResetAlert = true } label: { @@ -102,6 +94,7 @@ struct SettingsView: View { ) } .buttonStyle(PlainButtonStyle()) + Button { if let url = URL(string: "https://vitty.dscvit.com") { UIApplication.shared.open(url) @@ -130,6 +123,20 @@ struct SettingsView: View { .toggleStyle(SwitchToggleStyle(tint: .green)) } + SettingsSectionView(title: "Account Management") { + Button { + showDeleteUserAlert = true + } label: { + SettingsRowView( + icon: "person.badge.minus", + title: "Delete Account", + subtitle: "Permanently delete your account and all data" + ) + } + .buttonStyle(PlainButtonStyle()) + .disabled(isDeletingUser) + } + SettingsSectionView(title: "About") { AboutLinkView(image: "github-icon", title: "GitHub Repository", url: URL(string: "https://github.com/GDGVIT/vitty-ios")) AboutLinkView(image: "gdsc-logo", title: "GDSC VIT", url: URL(string: "https://dscvit.com/")) @@ -138,12 +145,9 @@ struct SettingsView: View { .scrollContentBackground(.hidden) } - if showResetAlert { ResetSaturdayAlert( - onCancel: { - showResetAlert = false - }, + onCancel: { showResetAlert = false }, onReset: { resetSaturdayClasses() showResetAlert = false @@ -151,6 +155,19 @@ struct SettingsView: View { ) .zIndex(1) } + + if showDeleteUserAlert { + DeleteUserAlert( + isDeleting: isDeletingUser, + onCancel: { + showDeleteUserAlert = false + }, + onDelete: { + deleteUser() + } + ) + .zIndex(1) + } } .navigationBarBackButtonHidden(true) .interactiveDismissDisabled(true) @@ -158,8 +175,7 @@ struct SettingsView: View { viewModel.timetable = timeTables.first viewModel.checkNotificationAuthorization() loadSelectedDay() - print("Saturday before save:", timeTables[0].saturday.map { $0.name }) - + print("Saturday before save:", timeTables.first?.saturday.map { $0.name } ?? []) } .alert("Notifications Disabled", isPresented: $viewModel.showNotificationDisabledAlert) { Button("OK", role: .cancel) {} @@ -169,95 +185,193 @@ struct SettingsView: View { } } - - private func loadSelectedDay() { - selectedDay = UserDefaults.standard.string(forKey: selectedDayKey) + selectedDay = timeTables.first?.saturdaySourceDay } - private func resetSaturdayClasses() { - guard let timeTable = timeTables.first else { return } + private func deleteUser() { + guard let username = authViewModel.loggedInBackendUser?.username else { + print("No username found") + return + } - - let newTimeTable = TimeTable( - monday: timeTable.monday, - tuesday: timeTable.tuesday, - wednesday: timeTable.wednesday, - thursday: timeTable.thursday, - friday: timeTable.friday, - saturday: [], // Empty Saturday - sunday: timeTable.sunday - ) + isDeletingUser = true + + Task { + do { + try await deleteUserFromServer(username: username) + + await MainActor.run { + cleanupLocalData() + authViewModel.signOut() + showDeleteUserAlert = false + isDeletingUser = false + } + } catch { + await MainActor.run { + isDeletingUser = false + print("Failed to delete user: \(error)") + } + } + } + } + + private func deleteUserFromServer(username: String) async throws { + guard let url = URL(string: "\(APIConstants.base_url)/users/\(username)") else { + throw URLError(.badURL) + } + var request = URLRequest(url: url) + request.httpMethod = "DELETE" - modelContext.delete(timeTable) - modelContext.insert(newTimeTable) + let token = authViewModel.loggedInBackendUser?.token ?? "" - + let (_, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard 200...299 ~= httpResponse.statusCode else { + throw URLError(.badServerResponse) + } + } + + private func cleanupLocalData() { do { + try modelContext.delete(model: TimeTable.self) + try modelContext.delete(model: Remainder.self) + try modelContext.delete(model: CreateNoteModel.self) + try modelContext.delete(model: UploadedFile.self) try modelContext.save() - print("Successfully reset Saturday classes") - - - UserDefaults.standard.removeObject(forKey: selectedDayKey) - selectedDay = nil - + print("Successfully cleaned up local data") } catch { - print("Error saving context: \(error)") + print("Failed to clean up local data: \(error)") } } + private func copyLecturesToSaturday(from day: String) { - guard let timeTable = timeTables.first else { return } + guard let currentTimeTable = timeTables.first else { + print("No timetable found") + return + } + + + + + let lecturesToCopy = currentTimeTable.lectures(forDay: day) + print("Found \(lecturesToCopy.count) lectures to copy") - let lecturesToCopy: [Lecture] - switch day { - case "Monday": - lecturesToCopy = timeTable.monday - case "Tuesday": - lecturesToCopy = timeTable.tuesday - case "Wednesday": - lecturesToCopy = timeTable.wednesday - case "Thursday": - lecturesToCopy = timeTable.thursday - case "Friday": - lecturesToCopy = timeTable.friday - default: - lecturesToCopy = [] + let newSaturdayLectures = lecturesToCopy.map { originalLecture in + let newLecture = Lecture( + name: originalLecture.name, + code: originalLecture.code, + venue: originalLecture.venue, + slot: originalLecture.slot, + type: originalLecture.type, + startTime: originalLecture.startTime, + endTime: originalLecture.endTime + ) + return newLecture } - + let newTimeTable = TimeTable( - monday: timeTable.monday, - tuesday: timeTable.tuesday, - wednesday: timeTable.wednesday, - thursday: timeTable.thursday, - friday: timeTable.friday, - saturday: lecturesToCopy.map { lecture in - Lecture( - name: lecture.name, - code: lecture.code, - venue: lecture.venue, - slot: lecture.slot, - type: lecture.type, - startTime: lecture.startTime, - endTime: lecture.endTime - ) - }, - sunday: timeTable.sunday + monday: currentTimeTable.monday.map { $0.deepCopy() }, + tuesday: currentTimeTable.tuesday.map { $0.deepCopy() }, + wednesday: currentTimeTable.wednesday.map { $0.deepCopy() }, + thursday: currentTimeTable.thursday.map { $0.deepCopy() }, + friday: currentTimeTable.friday.map { $0.deepCopy() }, + saturday: newSaturdayLectures, + sunday: currentTimeTable.sunday.map { $0.deepCopy() }, + saturdaySourceDay: day ) - modelContext.delete(timeTable) - modelContext.insert(newTimeTable) + do { + print("Deleting old timetable") + modelContext.delete(currentTimeTable) + + + print("Inserting new timetable with Saturday lectures") + modelContext.insert(newTimeTable) + + + try modelContext.save() + + + self.selectedDay = day + + print("Successfully copied \(day) to Saturday using orthodox method") + print("New Saturday has \(newTimeTable.saturday.count) lectures") + + + Task { @MainActor in + NotificationCenter.default.post( + name: NSNotification.Name("TimetableDidChange"), + object: nil + ) + } + + } catch { + print("Error during orthodox copy: \(error)") + + modelContext.rollback() + } + } + + + private func resetSaturdayClasses() { + guard let currentTimeTable = timeTables.first else { + print("No timetable found") + return + } - + print("Starting orthodox reset of Saturday classes") + + + let newTimeTable = TimeTable( + monday: currentTimeTable.monday.map { $0.deepCopy() }, + tuesday: currentTimeTable.tuesday.map { $0.deepCopy() }, + wednesday: currentTimeTable.wednesday.map { $0.deepCopy() }, + thursday: currentTimeTable.thursday.map { $0.deepCopy() }, + friday: currentTimeTable.friday.map { $0.deepCopy() }, + saturday: [], + sunday: currentTimeTable.sunday.map { $0.deepCopy() }, + saturdaySourceDay: nil + ) + + do { + print("Deleting old timetable") + modelContext.delete(currentTimeTable) + + + print("Inserting new timetable with empty Saturday") + modelContext.insert(newTimeTable) + + try modelContext.save() - print("Successfully replaced timetable with copied lectures from \(day) to Saturday") + + + self.selectedDay = nil + + print("Successfully reset Saturday classes using orthodox method") + + + Task { @MainActor in + NotificationCenter.default.post( + name: NSNotification.Name("TimetableDidChange"), + object: nil + ) + } + } catch { - print("Error saving context: \(error)") + print("Error during orthodox reset: \(error)") + + modelContext.rollback() } } @@ -398,7 +512,73 @@ struct ResetSaturdayAlert: View { } .background(Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)) .onTapGesture { - + // Empty tap gesture to prevent dismissal + } + } +} + +// Custom Delete User Alert Component +struct DeleteUserAlert: View { + let isDeleting: Bool + let onCancel: () -> Void + let onDelete: () -> Void + + var body: some View { + VStack { + Spacer() + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 40)) + .foregroundColor(.red) + + Text("Delete Account?") + .font(.custom("Poppins-SemiBold", size: 18)) + .foregroundColor(.white) + + Text("This action will permanently delete your account and all associated data. This cannot be undone.") + .font(.custom("Poppins-Regular", size: 14)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + + if isDeleting { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(height: 40) + } else { + HStack(spacing: 10) { + Button(action: onCancel) { + Text("Cancel") + .font(.custom("Poppins-Regular", size: 14)) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color.gray.opacity(0.3)) + .foregroundColor(.white) + .cornerRadius(8) + } + + Button(action: onDelete) { + Text("Delete Account") + .font(.custom("Poppins-Regular", size: 14)) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color.red) + .foregroundColor(.white) + .cornerRadius(8) + } + } + } + } + .frame(minHeight: 200) + .padding(20) + .background(Color("Background")) + .cornerRadius(16) + .padding(.horizontal, 30) + .transition(.scale.combined(with: .opacity)) + Spacer() + } + .background(Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)) + .onTapGesture { + // Empty tap gesture to prevent dismissal } } } diff --git a/VITTY/VITTY/Shared/Constants.swift b/VITTY/VITTY/Shared/Constants.swift index 27d0a12..2f8ca1f 100644 --- a/VITTY/VITTY/Shared/Constants.swift +++ b/VITTY/VITTY/Shared/Constants.swift @@ -12,7 +12,7 @@ class Constants { // "https://visiting-eba-vitty-d61856bb.koyeb.app/api/v2/" - "https://9b66-2409-40e3-1ee-9039-75b8-20ad-89e9-248a.ngrok-free.app/api/v2/" + "http://localhost:80/api/v2/" // "https://f4df-2409-40e3-30a4-8539-6d49-631b-ddd8-60a3.ngrok-free.app/api/v2/" diff --git a/VITTY/VITTY/TimeTable/Models/TimeTable.swift b/VITTY/VITTY/TimeTable/Models/TimeTable.swift index da1201d..ca90c05 100644 --- a/VITTY/VITTY/TimeTable/Models/TimeTable.swift +++ b/VITTY/VITTY/TimeTable/Models/TimeTable.swift @@ -36,6 +36,9 @@ class TimeTable: Codable { var friday: [Lecture] var saturday: [Lecture] var sunday: [Lecture] + + // NEW property + var saturdaySourceDay: String? @Transient var logger = Logger( @@ -51,7 +54,8 @@ class TimeTable: Codable { thursday: [Lecture], friday: [Lecture], saturday: [Lecture], - sunday: [Lecture] + sunday: [Lecture], + saturdaySourceDay: String? = nil ) { self.monday = monday self.tuesday = tuesday @@ -60,6 +64,7 @@ class TimeTable: Codable { self.friday = friday self.saturday = saturday self.sunday = sunday + self.saturdaySourceDay = saturdaySourceDay // Set in initializer } enum CodingKeys: String, CodingKey,Codable { @@ -73,64 +78,29 @@ class TimeTable: Codable { } required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - do { - monday = try container.decode([Lecture].self, forKey: .monday) - } - catch { - logger.error("Error decoding Monday lectures: \(error)") - monday = [] - } - - do { - tuesday = try container.decode([Lecture].self, forKey: .tuesday) - } - catch { - logger.error("Error decoding Tuesday lectures: \(error)") - tuesday = [] - } - - do { - wednesday = try container.decode([Lecture].self, forKey: .wednesday) - } - catch { - logger.error("Error decoding Wednesday lectures: \(error)") - wednesday = [] - } - - do { - thursday = try container.decode([Lecture].self, forKey: .thursday) - } - catch { - logger.error("Error decoding Thursday lectures: \(error)") - thursday = [] - } - - do { - friday = try container.decode([Lecture].self, forKey: .friday) - } - catch { - logger.error("Error decoding Friday lectures: \(error)") - friday = [] - } - - do { - saturday = try container.decode([Lecture].self, forKey: .saturday) - } - catch { - logger.error("Error decoding Saturday lectures: \(error)") - saturday = [] - } - - do { - sunday = try container.decode([Lecture].self, forKey: .sunday) - } - catch { - logger.error("Error decoding Sunday lectures: \(error)") - sunday = [] - } - } + let container = try decoder.container(keyedBy: CodingKeys.self) + + monday = (try? container.decode([Lecture].self, forKey: .monday)) ?? [] + tuesday = (try? container.decode([Lecture].self, forKey: .tuesday)) ?? [] + wednesday = (try? container.decode([Lecture].self, forKey: .wednesday)) ?? [] + thursday = (try? container.decode([Lecture].self, forKey: .thursday)) ?? [] + friday = (try? container.decode([Lecture].self, forKey: .friday)) ?? [] + saturday = (try? container.decode([Lecture].self, forKey: .saturday)) ?? [] + sunday = (try? container.decode([Lecture].self, forKey: .sunday)) ?? [] + + self.saturdaySourceDay = nil + } + //MARK: NEW FUNC + func lectures(forDay day: String) -> [Lecture] { + switch day { + case "Monday": return self.monday + case "Tuesday": return self.tuesday + case "Wednesday": return self.wednesday + case "Thursday": return self.thursday + case "Friday": return self.friday + default: return [] + } + } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -185,6 +155,18 @@ class Lecture: Codable, Identifiable, Comparable { case startTime = "start_time" case endTime = "end_time" } + + func deepCopy() -> Lecture { + return Lecture( + name: self.name, + code: self.code, + venue: self.venue, + slot: self.slot, + type: self.type, + startTime: self.startTime, + endTime: self.endTime + ) + } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -296,3 +278,4 @@ extension TimeTable { sunday != other.sunday } } + diff --git a/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift b/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift index e86dcac..ec4cf2b 100644 --- a/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift +++ b/VITTY/VITTY/TimeTable/ViewModel/TimeTableViewModel.swift @@ -5,7 +5,6 @@ // Created by Chandram Dutta on 09/02/24. // - import Foundation import OSLog import SwiftData @@ -27,7 +26,9 @@ extension TimeTableView { private var hasSyncedThisSession = false private var isSyncing = false - private var currentContext: ModelContext? + + // Serial queue for database operations to prevent race conditions + private let databaseQueue = DispatchQueue(label: "com.vitty.database", qos: .userInitiated) private let logger = Logger( subsystem: Bundle.main.bundleIdentifier!, @@ -36,66 +37,111 @@ extension TimeTableView { ) ) + private var notificationObserver: NSObjectProtocol? + + init() { + setupNotificationObserver() + } + + deinit { + if let observer = notificationObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + private func setupNotificationObserver() { + notificationObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name("TimetableDidChange"), + object: nil, + queue: .main + ) { [weak self] _ in + self?.forceRefreshCurrentDay() + } + } + private func forceRefreshCurrentDay() { + logger.info("Forcing refresh of current day due to timetable change") + changeDay() + } + // NEW: Method to refresh the timetable data from the database + @MainActor + func refreshFromDatabase(_ updatedTimeTable: TimeTable?) { + guard let updatedTimeTable = updatedTimeTable else { + self.timeTable = nil + self.lectures = [] + self.stage = .error + return + } + + // Update our cached copy with the fresh data + self.timeTable = updatedTimeTable + changeDay() // Refresh the current day's lectures + + logger.info("Timetable refreshed from database") + } + func changeDay() { + guard let timeTable = timeTable else { + self.lectures = [] + return + } + switch dayNo { case 0: - self.lectures = timeTable?.monday ?? [] + self.lectures = timeTable.monday case 1: - self.lectures = timeTable?.tuesday ?? [] + self.lectures = timeTable.tuesday case 2: - self.lectures = timeTable?.wednesday ?? [] + self.lectures = timeTable.wednesday case 3: - self.lectures = timeTable?.thursday ?? [] + self.lectures = timeTable.thursday case 4: - self.lectures = timeTable?.friday ?? [] + self.lectures = timeTable.friday case 5: - self.lectures = timeTable?.saturday ?? [] + self.lectures = timeTable.saturday case 6: - self.lectures = timeTable?.sunday ?? [] + self.lectures = timeTable.sunday default: self.lectures = [] } } + @MainActor - func loadTimeTable( - existingTimeTable: TimeTable?, - username: String, - authToken: String, - context: ModelContext - ) async { - logger.info("Starting timetable loading process") - - // Store context for later use - currentContext = context - - if let existing = existingTimeTable { - logger.debug("Using existing local timetable") - timeTable = existing - changeDay() - stage = .data - print("\(existing)") - - // Start background sync if not already done - if !hasSyncedThisSession && !isSyncing { - Task { - await backgroundSync( - localTimeTable: existing, - username: username, - authToken: authToken, - context: context - ) - } - } - } else { - logger.debug("No local timetable, fetching from API") - await fetchTimeTableFromAPI( - username: username, - authToken: authToken, - context: context - ) - } - } + func loadTimeTable( + existingTimeTable: TimeTable?, + username: String, + authToken: String, + context: ModelContext + ) async { + logger.info("Starting timetable loading process") + + if let existing = existingTimeTable { + logger.debug("Using existing local timetable.") + self.timeTable = existing + changeDay() + self.stage = .data + + + if !hasSyncedThisSession && !isSyncing && !username.isEmpty && !authToken.isEmpty { + + Task { [weak self] in + await self?.backgroundSync( + localTimeTable: existing, + username: username, + authToken: authToken, + context: context + ) + } + } + } else { + logger.debug("No local timetable, fetching from API.") + await fetchTimeTableFromAPI( + username: username, + authToken: authToken, + context: context + ) + } + } private func backgroundSync( localTimeTable: TimeTable, @@ -103,154 +149,218 @@ extension TimeTableView { authToken: String, context: ModelContext ) async { - guard !isSyncing else { return } + guard !isSyncing else { + logger.info("Sync already in progress. Skipping.") + return + } - isSyncing = true - hasSyncedThisSession = true + + await MainActor.run { + isSyncing = true + } - logger.info("Starting background sync") + logger.info("Starting background sync.") + + defer { + Task { @MainActor in + isSyncing = false + } + } do { let remoteTimeTable = try await TimeTableAPIService.shared.getTimeTable( with: username, authToken: authToken ) + logger.info("Background sync: Fetched remote timetable.") + + + let mergedTimeTable = await createMergedTimeTable( + remote: remoteTimeTable, + local: localTimeTable + ) - logger.info("Background sync: Fetched remote timetable") + + await updateLocalDatabaseSafely( + with: mergedTimeTable, + oldTimeTable: localTimeTable, + context: context + ) - if shouldUpdateLocalTimeTable(local: localTimeTable, remote: remoteTimeTable) { - logger.info("Background sync: Timetables differ, updating local data") - await updateLocalTimeTableWithPersistence( - oldTimeTable: localTimeTable, - newTimeTable: remoteTimeTable, - context: context - ) - } else { - logger.info("Background sync: Timetables are identical, no update needed") + await MainActor.run { + hasSyncedThisSession = true } } catch { - logger.error("Background sync failed: \(error)") + logger.error("Background sync failed: \(error.localizedDescription)") + } - - isSyncing = false - } - - private func shouldUpdateLocalTimeTable(local: TimeTable, remote: TimeTable) -> Bool { - let daysToCompare = [ - (local.monday, remote.monday), - (local.tuesday, remote.tuesday), - (local.wednesday, remote.wednesday), - (local.thursday, remote.thursday), - (local.friday, remote.friday), - (local.saturday, remote.saturday), - (local.sunday, remote.sunday) - ] - - for (localDay, remoteDay) in daysToCompare { - if !areLectureArraysEqual(localDay, remoteDay) { - return true - } - } - - return false } - private func areLectureArraysEqual(_ local: [Lecture], _ remote: [Lecture]) -> Bool { - guard local.count == remote.count else { return false } + private func createMergedTimeTable( + remote: TimeTable, + local: TimeTable + ) async -> TimeTable { + let saturdaySourceDay = local.saturdaySourceDay - let sortedLocal = local.sorted { $0.startTime < $1.startTime } - let sortedRemote = remote.sorted { $0.startTime < $1.startTime } + let finalTimeTable = TimeTable( + monday: remote.monday.map { $0.deepCopy() }, + tuesday: remote.tuesday.map { $0.deepCopy() }, + wednesday: remote.wednesday.map { $0.deepCopy() }, + thursday: remote.thursday.map { $0.deepCopy() }, + friday: remote.friday.map { $0.deepCopy() }, + saturday: [], + sunday: remote.sunday.map { $0.deepCopy() } + ) - for (localLecture, remoteLecture) in zip(sortedLocal, sortedRemote) { - if !areLecturesEqual(localLecture, remoteLecture) { - return false - } + + if let sourceDay = saturdaySourceDay { + logger.info("Re-applying Saturday rule from source day: \(sourceDay).") + let lecturesToCopy = finalTimeTable.lectures(forDay: sourceDay) + finalTimeTable.saturday = lecturesToCopy.map { $0.deepCopy() } + finalTimeTable.saturdaySourceDay = sourceDay } - return true - } - - private func areLecturesEqual(_ local: Lecture, _ remote: Lecture) -> Bool { - return local.name == remote.name && - local.code == remote.code && - local.venue == remote.venue && - local.slot == remote.slot && - local.type == remote.type && - local.startTime == remote.startTime && - local.endTime == remote.endTime + return finalTimeTable } @MainActor - private func updateLocalTimeTableWithPersistence( + private func updateLocalDatabaseSafely( + with newTimeTable: TimeTable, oldTimeTable: TimeTable, - newTimeTable: TimeTable, context: ModelContext ) async { - logger.info("Updating local timetable with persistence") + logger.info("Updating local database with merged timetable.") + do { - // Delete the old timetable from persistent storage + context.delete(oldTimeTable) - - // Insert the new timetable context.insert(newTimeTable) - - // Save the context to persist changes try context.save() - // Update the in-memory reference - timeTable = newTimeTable + + self.timeTable = newTimeTable changeDay() - - logger.info("Local timetable successfully updated and persisted") + logger.info("Local database successfully updated and persisted.") } catch { - logger.error("Failed to update local timetable: \(error)") - // Rollback: if save fails, re-insert the old timetable - context.insert(oldTimeTable) - try? context.save() + logger.error("Failed to save merged timetable: \(error.localizedDescription)") + + do { + + context.rollback() + + + context.insert(oldTimeTable) + try context.save() + + + self.timeTable = oldTimeTable + changeDay() + logger.info("Rollback successful.") + + } catch { + logger.error("Rollback also failed: \(error.localizedDescription)") + // In this case, trigger a fresh fetch + await handleDatabaseCorruption(context: context) + } } } + @MainActor + private func handleDatabaseCorruption(context: ModelContext) async { + logger.warning("Handling potential database corruption.") + + + self.timeTable = nil + self.lectures = [] + self.stage = .error + + + hasSyncedThisSession = false + isSyncing = false + + logger.info("Database corruption handled. User will need to reload.") + } + @MainActor private func fetchTimeTableFromAPI( username: String, authToken: String, context: ModelContext ) async { - logger.info("Fetching TimeTable from API") + logger.info("Fetching TimeTable from API for initial load.") + stage = .loading + + guard !username.isEmpty && !authToken.isEmpty else { + logger.error("Username or auth token is empty") + stage = .error + return + } do { - stage = .loading - let data = try await TimeTableAPIService.shared.getTimeTable( + let remoteTimeTable = try await TimeTableAPIService.shared.getTimeTable( with: username, authToken: authToken ) + logger.info("TimeTable fetched from API.") - logger.info("TimeTable fetched from API") + + context.insert(remoteTimeTable) + try context.save() - timeTable = data + self.timeTable = remoteTimeTable changeDay() stage = .data - - context.insert(data) - try context.save() hasSyncedThisSession = true } catch { - logger.error("API fetch failed: \(error)") + logger.error("API fetch failed: \(error.localizedDescription)") stage = .error } } + func resetSyncStatus() { + hasSyncedThisSession = false + logger.debug("Sync status reset.") + } + var updatedTimeTable: TimeTable? { timeTable } - func resetSyncStatus() { + @MainActor + func forceRefresh( + username: String, + authToken: String, + context: ModelContext + ) async { + logger.info("Force refreshing timetable data.") hasSyncedThisSession = false - logger.debug("Sync status reset") + isSyncing = false + + + if let existingTimeTable = timeTable { + do { + context.delete(existingTimeTable) + try context.save() + } catch { + logger.error("Failed to delete existing timetable: \(error.localizedDescription)") + + } + } + + + self.timeTable = nil + self.lectures = [] + + + await fetchTimeTableFromAPI( + username: username, + authToken: authToken, + context: context + ) } } } diff --git a/VITTY/VITTY/TimeTable/Views/LectureItemView.swift b/VITTY/VITTY/TimeTable/Views/LectureItemView.swift index 83171b2..4673ac5 100644 --- a/VITTY/VITTY/TimeTable/Views/LectureItemView.swift +++ b/VITTY/VITTY/TimeTable/Views/LectureItemView.swift @@ -60,7 +60,7 @@ struct LectureItemView: View { .padding(.horizontal, 16) HStack { - Text("\(formatTime(time: lecture.startTime)) - \(formatTime(time: lecture.endTime))") + Text("\(formatTime(time: lecture.startTime)) - \(formatTime(time: lecture.endTime)) | \(lecture.slot)") .font(Font.custom("Poppins-Regular", size: 14)) .foregroundColor(Color("Accent")) diff --git a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift index 2795344..5b8d181 100644 --- a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift +++ b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift @@ -1,4 +1,3 @@ - import OSLog import SwiftData import SwiftUI @@ -7,24 +6,27 @@ struct TimeTableView: View { @Environment(AuthViewModel.self) private var authViewModel @Environment(\.modelContext) private var context @Environment(\.scenePhase) private var scenePhase - + private let daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - + @State private var viewModel = TimeTableViewModel() @State private var selectedLecture: Lecture? = nil + @State private var isRefreshing = false + @State private var showingRefreshAlert = false + @Query private var timetableItem: [TimeTable] @Environment(\.dismiss) private var dismiss let friend: Friend? - + var isFriendsTimeTable: Bool - + private let logger = Logger( subsystem: Bundle.main.bundleIdentifier!, category: String( describing: TimeTableView.self ) ) - + var body: some View { NavigationStack { ZStack { @@ -39,20 +41,50 @@ struct TimeTableView: View { Spacer() }.padding(8) } - + switch viewModel.stage { case .loading: VStack { Spacer() ProgressView() + .scaleEffect(1.2) + Text("Loading timetable...") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 8) Spacer() } case .error: VStack { Spacer() - Text("It's an error!\(String(describing: authViewModel.loggedInBackendUser?.username))") + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 50)) + .foregroundColor(.orange) + .padding(.bottom, 16) + + Text("Something went wrong!") .font(Font.custom("Poppins-Bold", size: 24)) + .padding(.bottom, 8) + Text("Sorry if you are late for your class!") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.bottom, 20) + + Button(action: { + showingRefreshAlert = true + }) { + HStack { + Image(systemName: "arrow.clockwise") + Text("Refresh Timetable") + } + .foregroundColor(.white) + .padding() + .background(Color("Accent")) + .cornerRadius(10) + } + .disabled(isRefreshing) + Spacer() } case .data: @@ -63,14 +95,14 @@ struct TimeTableView: View { ForEach(daysOfWeek, id: \.self) { day in Text(day) .foregroundStyle(daysOfWeek[viewModel.dayNo] == day - ? Color("Background") : Color("Accent")) + ? Color("Background") : Color("Accent")) .frame(width: 60, height: 54) .background( daysOfWeek[viewModel.dayNo] == day ? Color("Accent") : Color.clear ) .onTapGesture { - withAnimation { + withAnimation(.easeInOut(duration: 0.2)) { viewModel.dayNo = daysOfWeek.firstIndex( of: day )! @@ -85,18 +117,28 @@ struct TimeTableView: View { .background(Color("Secondary")) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) - + if viewModel.lectures.isEmpty { Spacer() - Text("No classes today!") - .font(Font.custom("Poppins-Bold", size: 24)) - Text(StringConstants.noClassQuotesOffline.randomElement()!) + VStack(spacing: 16) { + Image(systemName: "calendar.badge.exclamationmark") + .font(.system(size: 50)) + .foregroundColor(.secondary) + + Text("No classes today!") + .font(Font.custom("Poppins-Bold", size: 24)) + + Text(StringConstants.noClassQuotesOffline.randomElement() ?? "Enjoy your free time!") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + } Spacer() } else { ScrollView { VStack(spacing: 12) { ForEach(viewModel.lectures.sorted()) { lecture in - LectureItemView( lecture: lecture, selectedDayIndex: viewModel.dayNo, @@ -104,7 +146,6 @@ struct TimeTableView: View { ) { selectedLecture = lecture } - } } .padding(.horizontal) @@ -120,21 +161,58 @@ struct TimeTableView: View { .sheet(item: $selectedLecture) { lecture in LectureDetailView(lecture: lecture) } + .alert("Refresh Timetable", isPresented: $showingRefreshAlert) { + Button("Cancel", role: .cancel) { } + Button("Refresh", role: .destructive) { + Task { + await refreshTimetable() + } + } + } message: { + Text("This will clear your local timetable and fetch fresh data from the server. Continue?") + } .navigationBarBackButtonHidden(true) .onAppear { logger.debug("onAppear triggered") loadTimetable() } + .onChange(of: timetableItem) { oldValue, newValue in + logger.debug("Timetable data changed, reloading view.") + + // NEW: Check if this is a meaningful change + let oldCount = oldValue.count + let newCount = newValue.count + + // Handle different change scenarios + if oldCount != newCount { + // Data was added or removed + loadTimetable() + } else if let oldTable = oldValue.first, let newTable = newValue.first { + // Check if the actual content changed (especially Saturday) + if oldTable.isDifferentFrom(newTable) { + logger.debug("Timetable content changed, refreshing ViewModel") + // Directly refresh the ViewModel with the new data + viewModel.refreshFromDatabase(newTable) + } + } + } .onChange(of: scenePhase) { _, newPhase in - if newPhase == .active { viewModel.resetSyncStatus() + + // Check if we need to reload due to potential data corruption + if viewModel.stage == .error || viewModel.timeTable == nil { + loadTimetable() + } } } } - + private func loadTimetable() { - Task { + guard !isRefreshing else { return } + + + Task { @MainActor in await viewModel.loadTimeTable( existingTimeTable: timetableItem.first, username: friend?.username ?? (authViewModel.loggedInBackendUser?.username ?? ""), @@ -142,6 +220,23 @@ struct TimeTableView: View { context: context ) } - print("this is users token is \(authViewModel.loggedInBackendUser?.token ?? "")") + + logger.debug("User token: \(authViewModel.loggedInBackendUser?.token ?? "empty")") + } + + private func refreshTimetable() async { + await MainActor.run { + isRefreshing = true + } + + await viewModel.forceRefresh( + username: friend?.username ?? (authViewModel.loggedInBackendUser?.username ?? ""), + authToken: authViewModel.loggedInBackendUser?.token ?? "", + context: context + ) + + await MainActor.run { + isRefreshing = false + } } } diff --git a/VITTY/VITTY/Utilities/Constants/APIConstants.swift b/VITTY/VITTY/Utilities/Constants/APIConstants.swift index 1cbadeb..f55add2 100644 --- a/VITTY/VITTY/Utilities/Constants/APIConstants.swift +++ b/VITTY/VITTY/Utilities/Constants/APIConstants.swift @@ -10,7 +10,7 @@ import Foundation struct APIConstants { - static let base_url = "https://9b66-2409-40e3-1ee-9039-75b8-20ad-89e9-248a.ngrok-free.app/api/v2/" + static let base_url = "http://localhost:80/api/v2/" static let createCircle = "circles/create/" static let sendRequest = "circles/sendRequest/" static let acceptRequest = "circles/acceptRequest/" diff --git a/VITTY/VittyWidget/Views/LargeWidget.swift b/VITTY/VittyWidget/Views/LargeWidget.swift index d59f08a..9b3755c 100644 --- a/VITTY/VittyWidget/Views/LargeWidget.swift +++ b/VITTY/VittyWidget/Views/LargeWidget.swift @@ -150,7 +150,7 @@ struct ScheduleLargeWidgetView: View { ) } - let remainingCount = entry.classes.count - displayClasses.count + let remainingCount = getUpcomingClasses().count - displayClasses.count if remainingCount > 0 { Text("+\(remainingCount) More") .foregroundColor(.white.opacity(0.6)) @@ -167,14 +167,14 @@ struct ScheduleLargeWidgetView: View { .padding(.vertical, 6) } - private func getDisplayClasses() -> [Classes] { + private func getUpcomingClasses() -> [Classes] { let currentTime = Date() let calendar = Calendar.current let dateFormatter = DateFormatter() dateFormatter.dateFormat = "h:mm a" dateFormatter.locale = Locale(identifier: "en_US_POSIX") - // Sort all classes by their start time + let sortedClasses = entry.classes.sorted { class1, class2 in let time1Components = class1.time.components(separatedBy: " - ") let time2Components = class2.time.components(separatedBy: " - ") @@ -194,27 +194,15 @@ struct ScheduleLargeWidgetView: View { return startTime1 < startTime2 } - // Find the next upcoming class or current class - var currentIndex = 0 + let now = Date() - - for (index, classItem) in sortedClasses.enumerated() { + let upcomingClasses = sortedClasses.filter { classItem in let timeComponents = classItem.time.components(separatedBy: " - ") - guard timeComponents.count == 2 else { continue } + guard timeComponents.count == 2 else { return true } - let startTimeStr = timeComponents[0].trimmingCharacters(in: .whitespaces) let endTimeStr = timeComponents[1].trimmingCharacters(in: .whitespaces) + guard let endTime = dateFormatter.date(from: endTimeStr) else { return true } - guard let startTime = dateFormatter.date(from: startTimeStr), - let endTime = dateFormatter.date(from: endTimeStr) else { continue } - - // Convert to today's date - let todayStart = calendar.date( - bySettingHour: calendar.component(.hour, from: startTime), - minute: calendar.component(.minute, from: startTime), - second: 0, - of: now - ) let todayEnd = calendar.date( bySettingHour: calendar.component(.hour, from: endTime), @@ -223,29 +211,22 @@ struct ScheduleLargeWidgetView: View { of: now ) - if let todayStart = todayStart, let todayEnd = todayEnd { - // If current time is before this class starts, or if we're currently in this class - if now <= todayEnd { - currentIndex = index - break - } + + if let todayEnd = todayEnd { + return now <= todayEnd } - // If we've passed all classes, start from the beginning for next day - if index == sortedClasses.count - 1 { - currentIndex = 0 - } + return true } - // Get up to 4 classes starting from the current position - let maxDisplay = min(4, sortedClasses.count) - var displayClasses: [Classes] = [] - - for i in 0.. [Classes] { + let upcomingClasses = getUpcomingClasses() - return displayClasses + + let maxDisplay = min(4, upcomingClasses.count) + return Array(upcomingClasses.prefix(maxDisplay)) } } diff --git a/VITTY/VittyWidget/Views/SmallWidget.swift b/VITTY/VittyWidget/Views/SmallWidget.swift index 7f0a2ad..6a2957f 100644 --- a/VITTY/VittyWidget/Views/SmallWidget.swift +++ b/VITTY/VittyWidget/Views/SmallWidget.swift @@ -27,6 +27,8 @@ struct DueSmallWidgetView: View { .font(.system(size: 12, weight: .bold)) .foregroundColor(.white) Spacer() + Image("widgetIcon").resizable().frame(width: 25, height: 10) + } if entry.isEmpty { diff --git a/VITTY/VittyWidget/Views/WidgetComponents.swift b/VITTY/VittyWidget/Views/WidgetComponents.swift index d832078..5a07cce 100644 --- a/VITTY/VittyWidget/Views/WidgetComponents.swift +++ b/VITTY/VittyWidget/Views/WidgetComponents.swift @@ -53,6 +53,7 @@ struct AssignmentRow: View { .foregroundColor(.white) Spacer() + } Text(assignment.subject) From 644095161accdb65de3e298df542fd3faa953769 Mon Sep 17 00:00:00 2001 From: rujin2003 Date: Sun, 6 Jul 2025 01:31:39 +0545 Subject: [PATCH 3/4] fix: logout state issue --- VITTY/ContentView.swift | 4 -- .../VITTY/Auth/ViewModels/AuthViewModel.swift | 37 ++++++++--- VITTY/VITTY/UserProfileSideBar/SideBar.swift | 61 ++++++++++++++++--- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/VITTY/ContentView.swift b/VITTY/ContentView.swift index edec9e3..0da0ef7 100644 --- a/VITTY/ContentView.swift +++ b/VITTY/ContentView.swift @@ -43,7 +43,3 @@ struct ContentView: View { } } - -#Preview { - ContentView() -} diff --git a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift index 3133b69..05a21c3 100644 --- a/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift +++ b/VITTY/VITTY/Auth/ViewModels/AuthViewModel.swift @@ -187,13 +187,7 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { } - private func firebaseUserAuthUpdate(with auth: Auth, user: User?) { - logger.info("Firebase User Auth State Updated") - DispatchQueue.main.async { - guard user != self.loggedInFirebaseUser else { return } - self.loggedInFirebaseUser = user - } - } + func login(with loginOptions: LoginOptions) async { logger.info("Loging In...") @@ -337,13 +331,38 @@ class AuthViewModel: NSObject, ASAuthorizationControllerDelegate { func signOut() { do { try firebaseAuth.signOut() + + UserDefaults.resetDefaults() - } - catch { + + DispatchQueue.main.async { + self.loggedInBackendUser = nil + self.loggedInFirebaseUser = nil + } + + + print(self.loggedInBackendUser ?? "the backend user is set to nil ") + + logger.info("User signed out successfully") + + } catch { logger.error("Error Signing Out: \(error)") } } + + + private func firebaseUserAuthUpdate(with auth: Auth, user: User?) { + logger.info("Firebase User Auth State Updated") + DispatchQueue.main.async { + self.loggedInFirebaseUser = user + + + if user == nil { + self.loggedInBackendUser = nil + } + } + } } extension UserDefaults { diff --git a/VITTY/VITTY/UserProfileSideBar/SideBar.swift b/VITTY/VITTY/UserProfileSideBar/SideBar.swift index f6fa929..77acbfd 100644 --- a/VITTY/VITTY/UserProfileSideBar/SideBar.swift +++ b/VITTY/VITTY/UserProfileSideBar/SideBar.swift @@ -11,6 +11,7 @@ struct UserProfileSidebar: View { @State private var ghostMode: Bool = false @State private var isUpdatingGhostMode: Bool = false @Environment(\.modelContext) private var modelContext + @State private var isLoggingOut: Bool = false var body: some View { ZStack(alignment: .topTrailing) { @@ -92,16 +93,22 @@ struct UserProfileSidebar: View { Spacer() Button { - authViewModel.signOut() - do{ - try modelContext.delete(model:TimeTable.self) - try modelContext.delete(model:Remainder.self) - try modelContext.delete(model:CreateNoteModel.self) - try modelContext.delete(model:UploadedFile.self) - try modelContext.save() - }catch{ - print("Failed to load data") + Task{ + await performLogout() } +// authViewModel.signOut() + +// do{ +// try modelContext.delete(model:TimeTable.self) +// try modelContext.delete(model:Remainder.self) +// try modelContext.delete(model:CreateNoteModel.self) +// try modelContext.delete(model:UploadedFile.self) +// try modelContext.save() +// }catch{ +// print("Failed to load data") +// } + + } label: { HStack { Image(systemName: "rectangle.portrait.and.arrow.right") @@ -180,6 +187,42 @@ struct UserProfileSidebar: View { } }.resume() } + private func performLogout() async { + isLoggingOut = true + + + await MainActor.run { + authViewModel.signOut() + } + + + await clearLocalData() + + + await MainActor.run { + isLoggingOut = false + isPresented = false + } + } + + private func clearLocalData() async { + do { + + await Task.detached { [modelContext] in + do { + try modelContext.delete(model: TimeTable.self) + try modelContext.delete(model: Remainder.self) + try modelContext.delete(model: CreateNoteModel.self) + try modelContext.delete(model: UploadedFile.self) + try modelContext.save() + } catch { + print("Failed to delete local data: \(error)") + } + }.value + } catch { + print("Failed to clear local data: \(error)") + } + } } struct MenuOption: View { From d6898b27269074437dca4d9d5642bcb2ce637b04 Mon Sep 17 00:00:00 2001 From: rujin2003 Date: Sun, 6 Jul 2025 11:45:13 +0545 Subject: [PATCH 4/4] fix: deeplink and join with qr impl --- VITTY/VITTY.xcodeproj/project.pbxproj | 25 --- VITTY/VITTY/Connect/Models/CircleModel.swift | 5 + .../View/Circles/Components/JoinGroup.swift | 151 +++++++------ .../View/Circles/Components/QrCode.swift | 209 ++++++++++++++---- .../Connect/View/Circles/View/Circles.swift | 2 +- .../View/Circles/View/InsideCircle.swift | 51 +++-- .../ViewModel/CommunityPageViewModel.swift | 125 +++++------ VITTY/VITTY/Info.plist | 14 +- .../ViewModel/SettingsViewModel.swift | 182 ++++++--------- VITTY/VITTYApp.swift | 53 ++--- VITTY/VittyWidget/Views/LargeWidget.swift | 8 +- VITTY/VittyWidget/Views/SmallWidget.swift | 15 +- 12 files changed, 453 insertions(+), 387 deletions(-) diff --git a/VITTY/VITTY.xcodeproj/project.pbxproj b/VITTY/VITTY.xcodeproj/project.pbxproj index 431c6b2..ae0e5f5 100644 --- a/VITTY/VITTY.xcodeproj/project.pbxproj +++ b/VITTY/VITTY.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 4B37F1E62E03D7D300DCEE5F /* ExistingHotelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37F1E52E03D7D300DCEE5F /* ExistingHotelView.swift */; }; 4B37F1E92E04173A00DCEE5F /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37F1E82E04173500DCEE5F /* SettingsViewModel.swift */; }; 4B40FE5D2E0A917F000BDD07 /* QrCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */; }; - 4B40FE5D2E0A917F000BDD07 /* QrCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */; }; 4B47CD7B2D7DCB8B00A46FEF /* CreateReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B47CD7A2D7DCB8400A46FEF /* CreateReminder.swift */; }; 4B4FCF632D317AFD002B392C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B4FCF622D317AFD002B392C /* GoogleService-Info.plist */; }; 4B5977472DF97D5C009CC224 /* RemainderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5977462DF97D5A009CC224 /* RemainderModel.swift */; }; @@ -46,11 +45,6 @@ 4B74D8772E0BF77800B390E9 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8762E0BF77400B390E9 /* Alerts.swift */; }; 4B74D8792E0BFC6000B390E9 /* FileUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */; }; 4B74D87B2E0BFC7E00B390E9 /* FileUploadHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D87A2E0BFC7900B390E9 /* FileUploadHelper.swift */; }; - 4B74D8732E0BDF2100B390E9 /* CourseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8722E0BDF1E00B390E9 /* CourseFile.swift */; }; - 4B74D8742E0BDF2100B390E9 /* CourseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8722E0BDF1E00B390E9 /* CourseFile.swift */; }; - 4B74D8772E0BF77800B390E9 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8762E0BF77400B390E9 /* Alerts.swift */; }; - 4B74D8792E0BFC6000B390E9 /* FileUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */; }; - 4B74D87B2E0BFC7E00B390E9 /* FileUploadHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74D87A2E0BFC7900B390E9 /* FileUploadHelper.swift */; }; 4B7DA5DC2D708BD3007354A3 /* LectureItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DA5DB2D708BCD007354A3 /* LectureItemView.swift */; }; 4B7DA5DF2D7094E8007354A3 /* Academics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DA5DE2D7094E3007354A3 /* Academics.swift */; }; 4B7DA5E12D70A728007354A3 /* FriendRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DA5E02D70A71C007354A3 /* FriendRow.swift */; }; @@ -204,7 +198,6 @@ 4B37F1E52E03D7D300DCEE5F /* ExistingHotelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExistingHotelView.swift; sourceTree = ""; }; 4B37F1E82E04173500DCEE5F /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCode.swift; sourceTree = ""; }; - 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCode.swift; sourceTree = ""; }; 4B47CD7A2D7DCB8400A46FEF /* CreateReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateReminder.swift; sourceTree = ""; }; 4B4FCF622D317AFD002B392C /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 4B5977462DF97D5A009CC224 /* RemainderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemainderModel.swift; sourceTree = ""; }; @@ -212,10 +205,6 @@ 4B74D8762E0BF77400B390E9 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = ""; }; 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUpload.swift; sourceTree = ""; }; 4B74D87A2E0BFC7900B390E9 /* FileUploadHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadHelper.swift; sourceTree = ""; }; - 4B74D8722E0BDF1E00B390E9 /* CourseFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseFile.swift; sourceTree = ""; }; - 4B74D8762E0BF77400B390E9 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = ""; }; - 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUpload.swift; sourceTree = ""; }; - 4B74D87A2E0BFC7900B390E9 /* FileUploadHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadHelper.swift; sourceTree = ""; }; 4B7DA5DB2D708BCD007354A3 /* LectureItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LectureItemView.swift; sourceTree = ""; }; 4B7DA5DE2D7094E3007354A3 /* Academics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Academics.swift; sourceTree = ""; }; 4B7DA5E02D70A71C007354A3 /* FriendRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendRow.swift; sourceTree = ""; }; @@ -492,19 +481,9 @@ path = Components; sourceTree = ""; }; - 4B74D8752E0BF76B00B390E9 /* Components */ = { - isa = PBXGroup; - children = ( - 4B74D87A2E0BFC7900B390E9 /* FileUploadHelper.swift */, - 4B74D8762E0BF77400B390E9 /* Alerts.swift */, - ); - path = Components; - sourceTree = ""; - }; 4B7DA5DD2D7094CA007354A3 /* Academics */ = { isa = PBXGroup; children = ( - 4B74D8752E0BF76B00B390E9 /* Components */, 4B74D8752E0BF76B00B390E9 /* Components */, 4BBB002F2D95510B003B8FE2 /* Model */, 4BBB002E2D955104003B8FE2 /* VIewModel */, @@ -533,7 +512,6 @@ 4B7DA5EB2D71E0F4007354A3 /* Components */ = { isa = PBXGroup; children = ( - 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */, 4B40FE5C2E0A9179000BDD07 /* QrCode.swift */, 4BF0C79E2D94694000016202 /* InsideCircleCards.swift */, 4B7DA5E62D71AC51007354A3 /* CirclesRow.swift */, @@ -547,7 +525,6 @@ 4B7DA5EC2D71E0FB007354A3 /* View */ = { isa = PBXGroup; children = ( - 4B2DD6942E0A702D00BC3B67 /* CircleRequests.swift */, 4B2DD6942E0A702D00BC3B67 /* CircleRequests.swift */, 4B7DA5E42D70B2C8007354A3 /* Circles.swift */, 4BF0C79C2D94680A00016202 /* InsideCircle.swift */, @@ -576,7 +553,6 @@ 4BBB002D2D9550F8003B8FE2 /* View */ = { isa = PBXGroup; children = ( - 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */, 4B74D8782E0BFC5A00B390E9 /* FileUpload.swift */, 4B183EE72D7C78B300C9D801 /* Courses.swift */, 4BF03C982D7819E00098C803 /* Notes.swift */, @@ -602,7 +578,6 @@ 4BBB002F2D95510B003B8FE2 /* Model */ = { isa = PBXGroup; children = ( - 4B74D8722E0BDF1E00B390E9 /* CourseFile.swift */, 4B74D8722E0BDF1E00B390E9 /* CourseFile.swift */, 4B5977462DF97D5A009CC224 /* RemainderModel.swift */, 4BBB00322D957A6A003B8FE2 /* NotesModel.swift */, diff --git a/VITTY/VITTY/Connect/Models/CircleModel.swift b/VITTY/VITTY/Connect/Models/CircleModel.swift index eaea8f2..44435b1 100644 --- a/VITTY/VITTY/Connect/Models/CircleModel.swift +++ b/VITTY/VITTY/Connect/Models/CircleModel.swift @@ -15,11 +15,16 @@ struct CircleModel: Decodable { let circleID: String let circleName: String let circleRole: String + let circleJoinCode: String enum CodingKeys: String, CodingKey { case circleID = "circle_id" case circleName = "circle_name" case circleRole = "circle_role" + case circleJoinCode = "circle_join_code" + + + } } diff --git a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift index 00a4f94..30fead0 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift @@ -3,6 +3,7 @@ // // Created by Rujin Devkota on 2/28/25. // + import SwiftUI import AVFoundation import UIKit @@ -156,17 +157,15 @@ struct JoinGroup: View { } .transition(.move(edge: .bottom).combined(with: .opacity)) } - }.onReceive(NotificationCenter.default.publisher(for: Notification.Name("JoinCircleFromDeepLink"))) { notification in + } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("JoinCircleFromDeepLink"))) { notification in if let userInfo = notification.userInfo, - let circleId = userInfo["circleId"] as? String, - let circleName = userInfo["circleName"] as? String { + let code = userInfo["code"] as? String { + + localGroupCode = code + groupCode = code - - localGroupCode = circleId - groupCode = circleId - self.circleName = circleName - joinCircle() } } @@ -179,9 +178,6 @@ struct JoinGroup: View { } message: { Text(alertMessage) } - .onOpenURL { url in - handleDeepLink(url) - } .onAppear { localGroupCode = groupCode } @@ -190,61 +186,49 @@ struct JoinGroup: View { } } - // MARK: - Handle Deep Link - private func handleDeepLink(_ url: URL) { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let circleId = components.queryItems?.first(where: { $0.name == "circleId" })?.value else { - return - } - - let circleName = components.queryItems?.first(where: { $0.name == "circleName" })?.value ?? "Unknown Circle" - - showJoinAlert(circleId: circleId, circleName: circleName) - } - - // MARK: - Show Join Alert - private func showJoinAlert(circleId: String, circleName: String) { - let alert = UIAlertController( - title: "Join Circle", - message: "Do you want to join '\(circleName)'?", - preferredStyle: .alert - ) - - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - alert.addAction(UIAlertAction(title: "Join", style: .default) { _ in - self.localGroupCode = circleId - self.groupCode = circleId - self.circleName = circleName - self.joinCircle() - }) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alert, animated: true) - } - } - + // MARK: - Handle Scanned Code private func handleScannedCode(_ code: String) { - if code.contains("vitty.app/invite") || code.contains("circleId=") { - if let components = URLComponents(string: code), - let circleId = components.queryItems?.first(where: { $0.name == "circleId" })?.value { - localGroupCode = circleId - groupCode = circleId - if let name = components.queryItems?.first(where: { $0.name == "circleName" })?.value { - circleName = name - } - joinCircle() + print("Scanned code: \(code)") + + + if code.contains("vitty.app/join") { + if let url = URL(string: code) { + handleDeepLink(url) } } else { + localGroupCode = code groupCode = code + + joinCircle() } + isScanning = false } + + // MARK: - Handle Deep Link + + private func handleDeepLink(_ url: URL) { + print("Deep link received in JoinGroup: \(url.absoluteString)") + + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + print("Failed to parse URL components") + return + } + + // Handle the URL format: https://vitty.app/join?code=ABC123 + + if let code = components.queryItems?.first(where: { $0.name == "code" })?.value { + localGroupCode = code + groupCode = code + + + joinCircle() + } + } // MARK: - Join Circle - private func joinCircle() { guard !localGroupCode.isEmpty, let username = authViewModel.loggedInBackendUser?.username, @@ -261,7 +245,7 @@ struct JoinGroup: View { isJoining = true UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - + let urlString = "\(APIConstants.base_url)circles/join?code=\(localGroupCode)" guard let url = URL(string: urlString) else { showToast(message: "Error: Invalid URL", isError: true) @@ -274,11 +258,15 @@ struct JoinGroup: View { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") + print("Joining circle with code: \(localGroupCode)") + print("Request URL: \(urlString)") + URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { isJoining = false if let error = error { + print("Network error: \(error.localizedDescription)") showToast(message: "Network error: \(error.localizedDescription)", isError: true) return } @@ -288,13 +276,15 @@ struct JoinGroup: View { return } + print("Response status code: \(httpResponse.statusCode)") + if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 { showToast(message: "Successfully joined the circle! 🎉", isError: false) let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() - + communityPageViewModel.fetchCircleData( from: "\(APIConstants.base_url)circles", token: token, @@ -308,28 +298,39 @@ struct JoinGroup: View { dismiss() } } else { - if let data = data, - let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let message = errorResponse["message"] as? String { - showToast(message: "Error: \(message)", isError: true) - } else { - switch httpResponse.statusCode { - case 400: - showToast(message: "Error: Invalid circle code", isError: true) - case 404: - showToast(message: "Error: Circle not found", isError: true) - case 409: - showToast(message: "Error: Already a member of this circle", isError: true) - case 403: - showToast(message: "Error: Not authorized to join this circle", isError: true) - default: - showToast(message: "Error: Failed to join circle (Code: \(httpResponse.statusCode))", isError: true) + + if let data = data { + print("Error response data: \(String(data: data, encoding: .utf8) ?? "No data")") + + if let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let message = errorResponse["message"] as? String { + showToast(message: "Error: \(message)", isError: true) + } else { + handleHTTPError(statusCode: httpResponse.statusCode) } + } else { + handleHTTPError(statusCode: httpResponse.statusCode) } } } }.resume() } + + // MARK: - Handle HTTP Errors + private func handleHTTPError(statusCode: Int) { + switch statusCode { + case 400: + showToast(message: "Error: Invalid circle code", isError: true) + case 404: + showToast(message: "Error: Circle not found", isError: true) + case 409: + showToast(message: "Error: Already a member of this circle", isError: true) + case 403: + showToast(message: "Error: Not authorized to join this circle", isError: true) + default: + showToast(message: "Error: Failed to join circle (Code: \(statusCode))", isError: true) + } + } // MARK: - Show Toast private func showToast(message: String, isError: Bool) { @@ -346,7 +347,6 @@ struct JoinGroup: View { } } - // MARK: - Toast View struct ToastView: View { let message: String @@ -377,7 +377,7 @@ struct ToastView: View { } } - +// MARK: - QR Scanner Components struct QRScannerView: UIViewControllerRepresentable { @Binding var scannedCode: String @Binding var isScanning: Bool @@ -412,7 +412,6 @@ struct QRScannerView: UIViewControllerRepresentable { } } - protocol QRScannerDelegate: AnyObject { func didScanCode(_ code: String) func didFailWithError(_ error: Error) diff --git a/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift b/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift index 05bcd18..58b7a2c 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift @@ -1,26 +1,25 @@ -// -// QrCode.swift -// VITTY -// -// Created by Rujin Devkota on 6/24/25. -// - import CoreImage.CIFilterBuiltins import SwiftUI - struct QRCodeModalView: View { let groupCode: String let circleName: String + let existingJoinCode: String let onDismiss: () -> Void @State private var showingShareSheet = false + @State private var isGeneratingCode = false + @State private var joinCode: String = "" + @State private var showError = false + @State private var errorMessage = "" + + @Environment(AuthViewModel.self) private var authViewModel + @Environment(CommunityPageViewModel.self) private var communityPageViewModel var body: some View { VStack { Spacer() VStack(spacing: 20) { - HStack { Text("Circle QR Code") .font(.custom("Poppins-SemiBold", size: 20)) @@ -33,37 +32,101 @@ struct QRCodeModalView: View { } } - VStack(spacing: 8) { Text(circleName) .font(.custom("Poppins-SemiBold", size: 18)) .foregroundColor(.white) - - } - - if let qrImage = generateQRCode(from: createInvitationLink()) { - Image(uiImage: qrImage) - .interpolation(.none) - .resizable() - .scaledToFit() + // QR Code Display + if isGeneratingCode { + Rectangle() + .fill(Color.gray.opacity(0.3)) .frame(width: 200, height: 200) - .background(Color.white) .cornerRadius(12) + .overlay( + VStack(spacing: 8) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: Color("Accent"))) + Text("Generating QR Code...") + .font(.custom("Poppins-Regular", size: 12)) + .foregroundColor(.white) + } + ) + } else if !joinCode.isEmpty { + if let qrImage = generateQRCode(from: createInvitationLink()) { + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .background(Color.white) + .cornerRadius(12) + } else { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 200, height: 200) + .cornerRadius(12) + .overlay( + VStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 30)) + .foregroundColor(.orange) + Text("QR Code\nGeneration Failed") + .font(.custom("Poppins-Regular", size: 12)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + Button("Try Again") { + generateJoinCode() + } + .font(.custom("Poppins-Regular", size: 10)) + .foregroundColor(Color("Accent")) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color("Secondary")) + .cornerRadius(4) + } + ) + } } else { Rectangle() .fill(Color.gray.opacity(0.3)) .frame(width: 200, height: 200) .cornerRadius(12) .overlay( - Text("QR Code\nGeneration Failed") - .font(.custom("Poppins-Regular", size: 12)) - .foregroundColor(.white) - .multilineTextAlignment(.center) + VStack(spacing: 8) { + Image(systemName: "qrcode") + .font(.system(size: 40)) + .foregroundColor(.white) + Text("Tap to Generate QR Code") + .font(.custom("Poppins-Regular", size: 12)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + } ) + .onTapGesture { + generateJoinCode() + } } + + if !joinCode.isEmpty { + VStack(spacing: 4) { + Text("Join Code:") + .font(.custom("Poppins-Regular", size: 12)) + .foregroundColor(.gray) + Text(joinCode) + .font(.custom("Poppins-SemiBold", size: 16)) + .foregroundColor(Color("Accent")) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color("Secondary")) + .cornerRadius(8) + .onTapGesture { + copyJoinCode() + } + } + } Text("Share this code for others to join your circle") .font(.custom("Poppins-Regular", size: 12)) @@ -71,20 +134,28 @@ struct QRCodeModalView: View { .multilineTextAlignment(.center) .padding(.horizontal) - - Button(action: { - showingShareSheet = true - }) { - HStack { - Image(systemName: "square.and.arrow.up") - Text("Share Invitation") + // Action Buttons + HStack(spacing: 12) { + + Button(action: { + if joinCode.isEmpty { + generateJoinCode() + } else { + showingShareSheet = true + } + }) { + HStack { + Image(systemName: joinCode.isEmpty ? "qrcode" : "square.and.arrow.up") + Text(joinCode.isEmpty ? "Generate QR Code" : "Share Invitation") + } + .font(.custom("Poppins-SemiBold", size: 14)) + .foregroundColor(Color("Background")) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color("Accent")) + .cornerRadius(8) } - .font(.custom("Poppins-SemiBold", size: 16)) - .foregroundColor(Color("Background")) - .padding(.horizontal, 20) - .padding(.vertical, 12) - .background(Color("Accent")) - .cornerRadius(8) + .disabled(isGeneratingCode) } } .frame(maxWidth: 300) @@ -97,15 +168,71 @@ struct QRCodeModalView: View { } .background(Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)) .sheet(isPresented: $showingShareSheet) { - ShareSheetQr(items: [createInvitationLink(), "Join my circle '\(circleName)' on VITTY!"]) + ShareSheetQr(items: [ + createInvitationLink(), + "Join my circle '\(circleName)' on VITTY! Use code: \(joinCode)" + ]) + } + .alert("Error", isPresented: $showError) { + Button("OK") { } + } message: { + Text(errorMessage) + } + .onAppear { + + if !existingJoinCode.isEmpty { + joinCode = existingJoinCode + } else { + + generateJoinCode() + } + } + .onChange(of: existingJoinCode) { oldValue, newValue in + if !newValue.isEmpty && newValue != joinCode { + joinCode = newValue + } } } + private func generateJoinCode() { + guard let token = authViewModel.loggedInBackendUser?.token else { + errorMessage = "Authentication required" + showError = true + return + } + + isGeneratingCode = true + + communityPageViewModel.generateJoinCode(circleId: groupCode, token: token) { result in + DispatchQueue.main.async { + isGeneratingCode = false + + switch result { + case .success(let code): + joinCode = code + + case .failure(let error): + errorMessage = "Failed to generate join code: \(error.localizedDescription)" + showError = true + } + } + } + } + + private func copyJoinCode() { + UIPasteboard.general.string = joinCode + + print("Join code copied to clipboard") + } + private func createInvitationLink() -> String { - - let baseURL = "https://vitty.app/invite" - let encodedCircleName = circleName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? circleName - return "\(baseURL)/circles/sendRequest/\(groupCode)&circleName=\(encodedCircleName)" + let baseURL = "https://vitty.app/join" + + guard let encodedCircleName = circleName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return "\(baseURL)?code=\(joinCode)" + } + + return "\(baseURL)?code=\(joinCode)&circleName=\(encodedCircleName)" } private func generateQRCode(from string: String) -> UIImage? { diff --git a/VITTY/VITTY/Connect/View/Circles/View/Circles.swift b/VITTY/VITTY/Connect/View/Circles/View/Circles.swift index ebe0ac1..118874c 100644 --- a/VITTY/VITTY/Connect/View/Circles/View/Circles.swift +++ b/VITTY/VITTY/Connect/View/Circles/View/Circles.swift @@ -52,7 +52,7 @@ struct CirclesView: View { VStack(spacing: 10) { ForEach(filteredCircles, id: \.circleID) { circle in - NavigationLink(destination: InsideCircle(circleName: circle.circleName, groupCode: circle.circleID)) { + NavigationLink(destination: InsideCircle(circleName: circle.circleName, circle_id:circle.circleID, circle_join_code: circle.circleJoinCode,circle_role: circle.circleRole)) { CirclesRow(circle: circle) } .buttonStyle(PlainButtonStyle()) diff --git a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift index 3056419..482eadc 100644 --- a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift +++ b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift @@ -198,7 +198,9 @@ struct GenerateJoinCodeModal: View { struct CircleMenuView: View { let circleName: String + let role: String let onLeaveGroup: () -> Void + let onDeleteGroup: () -> Void let onGroupRequests: () -> Void let onGenerateJoinCode: () -> Void @@ -227,20 +229,23 @@ struct CircleMenuView: View { Divider() .background(Color.gray.opacity(0.3)) - Button(action: { - onCancel() - onDeleteGroup() - }) { - HStack { - Image(systemName: "trash") - .foregroundColor(.red) - Text("Delete Circle") - .font(.custom("Poppins-Regular", size: 16)) - .foregroundColor(.red) - Spacer() + if role == "admin"{ + + Button(action: { + onCancel() + onDeleteGroup() + }) { + HStack { + Image(systemName: "trash") + .foregroundColor(.red) + Text("Delete Circle") + .font(.custom("Poppins-Regular", size: 16)) + .foregroundColor(.red) + Spacer() + } + .padding() + .background(Color("Background")) } - .padding() - .background(Color("Background")) } Divider() @@ -300,7 +305,9 @@ struct DualIconMenu: View { struct InsideCircle: View { var circleName : String - var groupCode: String + var circle_id: String + var circle_join_code : String + var circle_role : String @State var searchText: String = "" @State var showLeaveAlert: Bool = false @State var showDeleteAlert: Bool = false @@ -336,6 +343,7 @@ struct InsideCircle: View { } // MARK: - Filtered members for search + private var filteredMembers: [CircleUserTemp] { if searchText.isEmpty { return communityPageViewModel.circleMembers @@ -353,7 +361,7 @@ struct InsideCircle: View { let token = authViewModel.loggedInBackendUser?.token ?? "" - communityPageViewModel.generateJoinCode(circleId: groupCode, token: token) { result in + communityPageViewModel.generateJoinCode(circleId: circle_id, token: token) { result in DispatchQueue.main.async { self.isGeneratingCode = false @@ -371,10 +379,11 @@ struct InsideCircle: View { // MARK: - Copy Join Code Function private func copyJoinCode() { UIPasteboard.general.string = generatedJoinCode - // You might want to show a toast or feedback that the code was copied + } var body: some View { + VStack(spacing: 0) { HStack { Button(action: { @@ -489,7 +498,7 @@ struct InsideCircle: View { }) .onAppear { communityPageViewModel.fetchCircleMemberData( - from: "\(APIConstants.base_url)circles/\(groupCode)", + from: "\(APIConstants.base_url)circles/\(circle_id)", token: authViewModel.loggedInBackendUser?.token ?? "", loading: true ) @@ -500,7 +509,7 @@ struct InsideCircle: View { LeaveCircleAlert(circleName: "\(circleName)", onCancel: { showLeaveAlert = false }, onLeave: { - let url = "\(APIConstants.base_url)circles/\(groupCode)/leave" + let url = "\(APIConstants.base_url)circles/\(circle_id)/leave" let token = authViewModel.loggedInBackendUser?.token ?? "" communityPageViewModel.leaveCircle(from: url, token: token) @@ -516,7 +525,7 @@ struct InsideCircle: View { DeleteCircleAlert(circleName: "\(circleName)", onCancel: { showDeleteAlert = false }, onDelete: { - let url = "\(APIConstants.base_url)circles/\(groupCode)" + let url = "\(APIConstants.base_url)circles/\(circle_id)" let token = authViewModel.loggedInBackendUser?.token ?? "" communityPageViewModel.deleteCircle(from: url, token: token) @@ -549,6 +558,7 @@ struct InsideCircle: View { if showCircleMenu { CircleMenuView( circleName: circleName, + role : circle_role, onLeaveGroup: { showLeaveAlert = true }, @@ -570,8 +580,9 @@ struct InsideCircle: View { if showQRCode { QRCodeModalView( - groupCode: groupCode, + groupCode: circle_id, circleName: circleName, + existingJoinCode: circle_join_code, onDismiss: { showQRCode = false } diff --git a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift index 8ed78a1..203c45c 100644 --- a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift +++ b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift @@ -43,15 +43,12 @@ class CommunityPageViewModel { self.loadingFreinds = true } - self.errorFreinds = false print("This is the token used in the app \(token)") print("this is the url used for the endpoint \(url)") AF.request(url, method: .get, headers: ["Authorization": "Token \(token)"]) - .validate() - .responseDecodable(of: FriendRaw.self) { response in DispatchQueue.main.async { self.loadingFreinds = false @@ -63,7 +60,6 @@ class CommunityPageViewModel { case .failure(let error): self.logger.error("Error fetching friends: \(error)") - if self.friends.isEmpty { self.errorFreinds = true } @@ -75,12 +71,10 @@ class CommunityPageViewModel { //MARK: Circle DATA func fetchCircleData(from url: String, token: String, loading: Bool = false) { - if loading || circles.isEmpty { self.loadingCircle = true } - self.errorCircle = false AF.request(url, method: .get, headers: ["Authorization": "Token \(token)"]) @@ -97,7 +91,6 @@ class CommunityPageViewModel { case .failure(let error): self.logger.error("Error fetching circles: \(error)") - if self.circles.isEmpty { self.errorCircle = true } @@ -137,7 +130,6 @@ class CommunityPageViewModel { self.logger.info("Raw response: \(jsonString)") } - response self.circleRequests = [] self.errorCircleRequests = false } @@ -155,17 +147,16 @@ class CommunityPageViewModel { func acceptCircleRequest(circleId: String, token: String, completion: @escaping (Bool) -> Void) { self.loadingRequestAction = true - let url = "\(APIConstants.base_url)circles/acceptRequest/\(circleId)" - // Debug logging to see the actual URL being called + logger.info("Attempting to accept circle request with URL: \(url)") logger.info("Circle ID: \(circleId)") logger.info("Token: \(token.prefix(10))...") AF.request(url, method: .post, headers: ["Authorization": "Token \(token)"]) .validate() - .responseData { response in // Changed to responseData to get more details + .responseData { response in DispatchQueue.main.async { self.loadingRequestAction = false @@ -178,10 +169,10 @@ class CommunityPageViewModel { self.logger.info("Response: \(responseString)") } - // Remove the accepted request from the list + self.circleRequests.removeAll { $0.circle_id == circleId } - // Refresh circles data to show the newly joined circle + self.fetchCircleData( from: "\(APIConstants.base_url)circles", token: token, @@ -193,7 +184,7 @@ class CommunityPageViewModel { case .failure(let error): self.logger.error("Error accepting circle request: \(error)") - // Log more details about the error + if let data = response.data, let errorString = String(data: data, encoding: .utf8) { self.logger.error("Error response: \(errorString)") } @@ -314,10 +305,8 @@ class CommunityPageViewModel { self.loadingCircleMembers = false switch response.result { - case .success(let value): - if let json = value as? [String: Any], let detail = json["detail"] as? String { - self.logger.info("Success: \(detail)") - } + case .success: + self.logger.info("Successfully left circle") case .failure(let error): self.logger.error("Error leaving circle: \(error)") @@ -339,21 +328,15 @@ class CommunityPageViewModel { self.loadingCircleMembers = false switch response.result { - case .success(let value): - if let json = value as? [String: Any], let detail = json["detail"] as? String { - self.logger.info("Successfully deleted circle: \(detail)") - } else { - self.logger.info("Successfully deleted circle") - } - + case .success: + self.logger.info("Successfully deleted circle") - self.fetchCircleData( - from: "\(APIConstants.base_url)circles", - token: token, - loading: false - ) - - + + self.fetchCircleData( + from: "\(APIConstants.base_url)circles", + token: token, + loading: false + ) case .failure(let error): self.logger.error("Error deleting circle: \(error)") @@ -379,7 +362,12 @@ class CommunityPageViewModel { } // MARK: - Group Creation - + + + struct CreateCircleResponse: Codable { + let detail: String + } + func createCircle(name: String, token: String, completion: @escaping (Result) -> Void) { guard let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { @@ -392,30 +380,20 @@ class CommunityPageViewModel { AF.request(url, method: .post, headers: ["Authorization": "Token \(token)"]) .validate() - .responseJSON { response in + .responseDecodable(of: CreateCircleResponse.self) { response in DispatchQueue.main.async { switch response.result { case .success(let data): - if let json = data as? [String: Any], - let detail = json["detail"] as? String { - - - if detail.lowercased().contains("successfully") { - self.logger.info("Successfully created circle: \(name)") - - completion(.success(name)) - } else { - - let error = NSError(domain: "CreateCircleError", code: 1, userInfo: [NSLocalizedDescriptionKey: detail]) - self.logger.error("Error creating circle: \(detail)") - completion(.failure(error)) - } + if data.detail.lowercased().contains("successfully") { + self.logger.info("Successfully created circle: \(name)") + completion(.success(name)) } else { - let error = NSError(domain: "CreateCircleError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) + let error = NSError(domain: "CreateCircleError", code: 1, userInfo: [NSLocalizedDescriptionKey: data.detail]) + self.logger.error("Error creating circle: \(data.detail)") completion(.failure(error)) } - + self.fetchCircleData( from: "\(APIConstants.base_url)circles", token: token, @@ -434,6 +412,7 @@ class CommunityPageViewModel { let url = "\(APIConstants.base_url)circles/sendRequest/\(circleId)/\(username)" print("this is the endpoint \(url)") + AF.request(url, method: .post, headers: ["Authorization": "Token \(token)"]) .validate() .response { response in @@ -472,52 +451,54 @@ class CommunityPageViewModel { // MARK: - Refresh Methods func refreshAllData(token: String, username: String) { - fetchFriendsData( from: "\(APIConstants.base_url)friends/\(username)/", token: token, loading: false ) - fetchCircleData( from: "\(APIConstants.base_url)circles", token: token, loading: false ) - fetchCircleRequests(token: token, loading: false) } + + struct JoinCodeResponse: Codable { + + let joinCode: String? + let detail: String? + + enum CodingKeys: String, CodingKey { + case joinCode = "join_code" + case detail + } + } + func generateJoinCode(circleId: String, token: String, completion: @escaping (Result) -> Void) { let url = "\(APIConstants.base_url)circles/\(circleId)/generateJoinCode" - print("Generating join code for circle: \(circleId)") - print("Request URL: \(url)") + print("Generating join code for circle: \(circleId)") + print("Request URL: \(url)") AF.request(url, method: .post, headers: ["Authorization": "Token \(token)"]) .validate() - .responseJSON { response in + .responseDecodable(of: JoinCodeResponse.self) { response in DispatchQueue.main.async { switch response.result { case .success(let data): - if let json = data as? [String: Any] { - if let joinCode = json["joinCode"] as? String { - print("Successfully generated join code: \(joinCode)") - completion(.success(joinCode)) - } else if let detail = json["detail"] as? String { - // Handle error case where detail contains error message - print("Error generating join code: \(detail)") - let error = NSError(domain: "GenerateJoinCodeError", code: 1, userInfo: [NSLocalizedDescriptionKey: detail]) - completion(.failure(error)) - } else { - // Handle unexpected response format - print("Unexpected response format") - let error = NSError(domain: "GenerateJoinCodeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) - completion(.failure(error)) - } + if let joinCode = data.joinCode { + print("Successfully generated join code: \(joinCode)") + completion(.success(joinCode)) + } else if let detail = data.detail { + print("Error generating join code: \(detail)") + let error = NSError(domain: "GenerateJoinCodeError", code: 1, userInfo: [NSLocalizedDescriptionKey: detail]) + completion(.failure(error)) } else { + print("Unexpected response format") let error = NSError(domain: "GenerateJoinCodeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) completion(.failure(error)) } @@ -530,5 +511,3 @@ class CommunityPageViewModel { } } } - - diff --git a/VITTY/VITTY/Info.plist b/VITTY/VITTY/Info.plist index 3193b8a..5299857 100644 --- a/VITTY/VITTY/Info.plist +++ b/VITTY/VITTY/Info.plist @@ -16,6 +16,16 @@ com.googleusercontent.apps.272763363329-i8n51oo9m30h9it7qq9ufmd0lahnmm63 + + CFBundleTypeRole + Editor + CFBundleURLName + com.gdscvit.vittyios + CFBundleURLSchemes + + vitty + + FirebaseAppDelegateProxyEnabled @@ -26,6 +36,8 @@ comgooglemaps maps + NSUserNotificationUsageDescription + We use notifications to remind you about your academic events UIAppFonts Poppins-Medium.ttf @@ -47,7 +59,5 @@ UIViewControllerBasedStatusBarAppearance - NSUserNotificationUsageDescription - We use notifications to remind you about your academic events diff --git a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift index ae92f18..3873306 100644 --- a/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift +++ b/VITTY/VITTY/Settings/ViewModel/SettingsViewModel.swift @@ -2,16 +2,17 @@ import Foundation import SwiftUI import UserNotifications -class SettingsViewModel : ObservableObject{ - @Published var notificationsEnabled: Bool = false { +class SettingsViewModel: ObservableObject { + @Published var notificationsEnabled: Bool = UserDefaults.standard.bool(forKey: "notificationsEnabled") { didSet { UserDefaults.standard.set(notificationsEnabled, forKey: "notificationsEnabled") if notificationsEnabled { - if let timetable = self.timetable { - self.scheduleAllNotifications(from: timetable) - } + + requestPermissionAndSchedule() } else { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + print("All pending notifications have been cleared.") showNotificationDisabledAlert = true } } @@ -22,11 +23,11 @@ class SettingsViewModel : ObservableObject{ init(timetable: TimeTable? = nil) { self.timetable = timetable - - self.notificationsEnabled = UserDefaults.standard.bool(forKey: "notificationsEnabled") + checkNotificationAuthorization() } + func checkNotificationAuthorization() { UNUserNotificationCenter.current().getNotificationSettings { settings in DispatchQueue.main.async { @@ -37,147 +38,108 @@ class SettingsViewModel : ObservableObject{ } } - func requestNotificationPermission() { - UNUserNotificationCenter.current().getNotificationSettings { settings in + + func requestPermissionAndSchedule() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in DispatchQueue.main.async { - if settings.authorizationStatus == .authorized { + if granted { + print("Notification permission granted.") if let timetable = self.timetable { self.scheduleAllNotifications(from: timetable) } } else { + print("Notification permission denied.") + self.notificationsEnabled = false } } } } + func scheduleAllNotifications(from timetable: TimeTable) { - // Clear existing notifications first + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - + let weekdays: [(Int, [Lecture])] = [ - (2, timetable.monday), // Monday = 2 - (3, timetable.tuesday), // Tuesday = 3 - (4, timetable.wednesday), // Wednesday = 4 - (5, timetable.thursday), // Thursday = 5 - (6, timetable.friday), // Friday = 6 - (7, timetable.saturday), // Saturday = 7 - (1, timetable.sunday) // Sunday = 1 + (1, timetable.sunday), (2, timetable.monday), (3, timetable.tuesday), + (4, timetable.wednesday), (5, timetable.thursday), (6, timetable.friday), + (7, timetable.saturday) ] for (weekday, lectures) in weekdays { for lecture in lectures { - guard let startDate = parseLectureTime(lecture.startTime, weekday: weekday) else { - print("Failed to parse time for lecture: \(lecture.name) with time: \(lecture.startTime)") - continue - } - - scheduleNotification(for: lecture.name, at: startDate, title: "Class Starting", minutesBefore: 0) - - - scheduleNotification(for: lecture.name, at: startDate, title: "Upcoming Class", minutesBefore: 10) + scheduleNotificationForNextOccurence(lecture: lecture, weekday: weekday) } } - - print("Scheduled notifications for all lectures") + print("Scheduled all notifications for the next 7 days.") } - private func scheduleNotification(for lectureName: String, at date: Date, title: String, minutesBefore: Int) { - let triggerDate = Calendar.current.date(byAdding: .minute, value: -minutesBefore, to: date) ?? date + + private func scheduleNotificationForNextOccurence(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 + + + guard let nextTriggerDate = Calendar.current.nextDate(after: Date(), matching: dateComponents, matchingPolicy: .nextTime) else { return } + + + scheduleNotification( + lectureName: lecture.name, + date: nextTriggerDate, + title: "Upcoming Class", + body: "\(lecture.name) starts in 10 minutes.", + minutesBefore: 10 + ) + + + scheduleNotification( + lectureName: lecture.name, + date: nextTriggerDate, + title: "Class Starting!", + body: "\(lecture.name) is starting now.", + minutesBefore: 0 + ) + } + + + private func scheduleNotification(lectureName: String, date: Date, title: String, body: String, minutesBefore: Int) { let content = UNMutableNotificationContent() content.title = title - content.body = "\(lectureName) is starting soon." + content.body = body content.sound = .default - let triggerComponents = Calendar.current.dateComponents([.weekday, .hour, .minute], from: triggerDate) - let trigger = UNCalendarNotificationTrigger(dateMatching: triggerComponents, repeats: true) + + 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: triggerComponents, repeats: false) - let identifier = "\(lectureName)-\(title)-\(minutesBefore)min-weekday\(triggerComponents.weekday ?? 0)" - let request = UNNotificationRequest( - identifier: identifier, - content: content, - trigger: trigger - ) + 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: \(error)") + print("Error scheduling notification for \(lectureName): \(error.localizedDescription)") } else { - print("Successfully scheduled notification: \(identifier)") + print("Successfully scheduled notification: '\(title)' for \(lectureName)") } } } - - private func parseLectureTime(_ timeString: String, weekday: Int) -> Date? { - - let formattedTimeString = formatTime(time: timeString) - - - if formattedTimeString == "Failed to parse the time string." { - return nil - } - - let timeFormatter = DateFormatter() - timeFormatter.dateFormat = "h:mm a" - timeFormatter.locale = Locale(identifier: "en_US_POSIX") - - guard let timeDate = timeFormatter.date(from: formattedTimeString) else { - print("Failed to parse formatted time: \(formattedTimeString)") - return nil - } - - - let calendar = Calendar.current - let timeComponents = calendar.dateComponents([.hour, .minute], from: timeDate) - - - let today = Date() - let currentWeekday = calendar.component(.weekday, from: today) - - - let daysFromToday = weekday - currentWeekday - let targetDate = calendar.date(byAdding: .day, value: daysFromToday, to: today) ?? today - - - var finalDateComponents = calendar.dateComponents([.year, .month, .day], from: targetDate) - finalDateComponents.hour = timeComponents.hour - finalDateComponents.minute = timeComponents.minute - finalDateComponents.second = 0 - - guard let lectureDate = calendar.date(from: finalDateComponents) else { - print("Failed to create lecture date") - return nil - } - + + private func parseTime(from timeString: String) -> Date? { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" - if weekday == currentWeekday && lectureDate < today { - return calendar.date(byAdding: .weekOfYear, value: 1, to: lectureDate) - } - - - if lectureDate < today { - return calendar.date(byAdding: .weekOfYear, value: 1, to: lectureDate) - } - - return lectureDate - } - - // Your existing formatTime function - private func formatTime(time: String) -> String { - var timeComponents = time.components(separatedBy: "T").last ?? "" - timeComponents = timeComponents.components(separatedBy: "+").first ?? "" - timeComponents = timeComponents.components(separatedBy: "Z").first ?? "" - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss" - if let date = dateFormatter.date(from: timeComponents) { - dateFormatter.dateFormat = "h:mm a" - let formattedTime = dateFormatter.string(from: date) - return formattedTime - } else { - return "Failed to parse the time string." + if let timePart = timeString.components(separatedBy: "T").last?.components(separatedBy: "+").first { + return formatter.date(from: timePart) } + return nil } } diff --git a/VITTY/VITTYApp.swift b/VITTY/VITTYApp.swift index cbfd107..b29b191 100644 --- a/VITTY/VITTYApp.swift +++ b/VITTY/VITTYApp.swift @@ -39,9 +39,8 @@ import TipKit - use // MARK: when u create a function, it helps to navigate. */ -/// Empty classrooms testing -/// empty sheet in reaminder view -/// + + @main struct VITTYApp: App { @@ -54,7 +53,7 @@ struct VITTYApp: App { @State private var deepLinkURL: URL? @State private var showJoinCircleAlert = false - @State private var pendingCircleInvite: CircleInvite? + @State private var pendingCircleInvite: (code: String, circleName: String?)? init() { setupFirebase() @@ -82,7 +81,7 @@ struct VITTYApp: App { } } message: { if let invite = pendingCircleInvite { - Text("Do you want to join '\(invite.circleName)'?") + Text("Do you want to join the circle with code '\(invite.code)'?") } } } @@ -98,68 +97,62 @@ struct VITTYApp: App { } } - +// MARK: - Deep Link Handling extension VITTYApp { - struct CircleInvite { - let circleId: String - let circleName: String - } - private func handleDeepLink(_ url: URL) { logger.info("Deep link received: \(url.absoluteString)") - if url.absoluteString.contains("vitty.app/invite") || - url.absoluteString.contains("circleId=") { - handleCircleInviteURL(url) + if url.absoluteString.contains("vitty.app/join") { + handleJoinCircleURL(url) } else { - + logger.info("Unhandled deep link type: \(url.absoluteString)") } } - private func handleCircleInviteURL(_ url: URL) { + private func handleJoinCircleURL(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { logger.error("Failed to parse URL components") return } - - guard let circleId = components.queryItems?.first(where: { $0.name == "circleId" })?.value else { - logger.error("No circleId found in URL") + + guard let code = components.queryItems?.first(where: { $0.name == "code" })?.value else { + logger.error("No code found in URL") return } - - let circleName = components.queryItems?.first(where: { $0.name == "circleName" })?.value ?? "Unknown Circle" - - pendingCircleInvite = CircleInvite(circleId: circleId, circleName: circleName) + let circleName = components.queryItems?.first(where: { $0.name == "circleName" })?.value + + // Store the invite and show alert + pendingCircleInvite = (code: code, circleName: circleName) showJoinCircleAlert = true - logger.info("Circle invite prepared: \(circleId) - \(circleName)") + logger.info("Circle join code prepared: \(code)") } - private func handleCircleInvite(_ invite: CircleInvite) { + private func handleCircleInvite(_ invite: (code: String, circleName: String?)) { NotificationCenter.default.post( name: Notification.Name("JoinCircleFromDeepLink"), object: nil, userInfo: [ - "circleId": invite.circleId, - "circleName": invite.circleName + "code": invite.code, + "circleName": invite.circleName ?? "Unknown Circle" ] ) - + pendingCircleInvite = nil - logger.info("Circle invite notification posted for: \(invite.circleId)") + logger.info("Circle join notification posted for code: \(invite.code)") } } - +// MARK: - Firebase Setup extension VITTYApp { private func setupFirebase() { self.logger.info("Configuring Firebase Started") diff --git a/VITTY/VittyWidget/Views/LargeWidget.swift b/VITTY/VittyWidget/Views/LargeWidget.swift index 9b3755c..68fa3ed 100644 --- a/VITTY/VittyWidget/Views/LargeWidget.swift +++ b/VITTY/VittyWidget/Views/LargeWidget.swift @@ -75,7 +75,7 @@ struct ScheduleLargeWidgetView: View { HStack(alignment: .top) { Spacer().frame(width: 2) VStack(alignment: .leading, spacing: 15) { - Spacer().frame(height: 5) + WidgetTitle(title: "Today's Schedule", fontSize: 18) Spacer().frame(height: 5) @@ -92,7 +92,7 @@ struct ScheduleLargeWidgetView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else if entry.completed == entry.total { - // Center the CircleProgressView + VStack { Spacer() CircleProgressView( @@ -123,7 +123,7 @@ struct ScheduleLargeWidgetView: View { } .frame(maxWidth: .infinity) } else { - // Center the CircleProgressView + VStack { Spacer() CircleProgressView( @@ -164,7 +164,7 @@ struct ScheduleLargeWidgetView: View { Spacer() } .padding(.horizontal, 4) - .padding(.vertical, 6) + .padding(.vertical, 6).ignoresSafeArea() } private func getUpcomingClasses() -> [Classes] { diff --git a/VITTY/VittyWidget/Views/SmallWidget.swift b/VITTY/VittyWidget/Views/SmallWidget.swift index 6a2957f..e322f8c 100644 --- a/VITTY/VittyWidget/Views/SmallWidget.swift +++ b/VITTY/VittyWidget/Views/SmallWidget.swift @@ -89,7 +89,7 @@ struct ScheduleSmallWidgetView: View { var body: some View { VStack(alignment: .leading) { - Spacer().frame(height: 10) + WidgetTitle(title: "Schedule", fontSize: 12.0) Spacer().frame(height: 15) @@ -121,13 +121,18 @@ struct ScheduleSmallWidgetView: View { .foregroundColor(Color(.accentBlue)) } } else { - Text("No more classes") - .font(.system(size: 10, weight: .medium)) - .foregroundColor(.white) + HStack{ + Spacer() + Text("No more classes") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.white) + Spacer() + + } } Spacer() - } + }.ignoresSafeArea() }