From cecac56effdc9223f75850457a036cd3f2b16357 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sun, 23 Feb 2025 19:48:03 -0500 Subject: [PATCH 01/21] Group models --- CanvasPlusPlayground.xcodeproj/project.pbxproj | 12 ++++++++++-- .../Grades/{ => Models}/APIGradingPeriod.swift | 0 .../Grades/{ => Models}/APIGradingSchemeEntry.swift | 0 3 files changed, 10 insertions(+), 2 deletions(-) rename CanvasPlusPlayground/Features/Grades/{ => Models}/APIGradingPeriod.swift (100%) rename CanvasPlusPlayground/Features/Grades/{ => Models}/APIGradingSchemeEntry.swift (100%) diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index f1fa87ae..daa7c48a 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -572,10 +572,9 @@ A324BA5F2D0798A1005F53FA /* Grades */ = { isa = PBXGroup; children = ( + B7460A8F2D6BF9320069CF5B /* Models */, 7F8535732C98DE3C0023E384 /* CourseGradeView.swift */, A31A81E62D0CCB69003C37EB /* GradesViewModel.swift */, - A373DC0C2D19F65700215019 /* APIGradingPeriod.swift */, - A373DC222D1D454000215019 /* APIGradingSchemeEntry.swift */, ); path = Grades; sourceTree = ""; @@ -700,6 +699,15 @@ children = ( B5894F0D2D6EBD8C00E8F527 /* Page.swift */, B5894F0E2D6EBD8C00E8F527 /* PageAPI.swift */, + ); + path = Models; + sourceTree = ""; + }; + B7460A8F2D6BF9320069CF5B /* Models */ = { + isa = PBXGroup; + children = ( + A373DC0C2D19F65700215019 /* APIGradingPeriod.swift */, + A373DC222D1D454000215019 /* APIGradingSchemeEntry.swift */, ); path = Models; sourceTree = ""; diff --git a/CanvasPlusPlayground/Features/Grades/APIGradingPeriod.swift b/CanvasPlusPlayground/Features/Grades/Models/APIGradingPeriod.swift similarity index 100% rename from CanvasPlusPlayground/Features/Grades/APIGradingPeriod.swift rename to CanvasPlusPlayground/Features/Grades/Models/APIGradingPeriod.swift diff --git a/CanvasPlusPlayground/Features/Grades/APIGradingSchemeEntry.swift b/CanvasPlusPlayground/Features/Grades/Models/APIGradingSchemeEntry.swift similarity index 100% rename from CanvasPlusPlayground/Features/Grades/APIGradingSchemeEntry.swift rename to CanvasPlusPlayground/Features/Grades/Models/APIGradingSchemeEntry.swift From a492c41aab97d47a1ff13d7a735a9a79b10a0228 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sun, 23 Feb 2025 19:52:42 -0500 Subject: [PATCH 02/21] remove context button --- .../Assignments/CourseAssignmentsView.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index 6166558d..e577d3de 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -39,18 +39,22 @@ struct CourseAssignmentsView: View { let assignmentModel = assignment.createModel() AssignmentRow(assignment: assignmentModel, showGrades: showGrades) .contextMenu { - PinButton( - itemID: assignmentModel.id, - courseID: course.id, - type: .assignment - ) + if !showGrades { + PinButton( + itemID: assignmentModel.id, + courseID: course.id, + type: .assignment + ) + } } .swipeActions(edge: .leading) { - PinButton( - itemID: assignmentModel.id, - courseID: course.id, - type: .assignment - ) + if !showGrades { + PinButton( + itemID: assignmentModel.id, + courseID: course.id, + type: .assignment + ) + } } } } From 7e98491f7ea8765e614a3b1e8a471e10ade6cd49 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sun, 23 Feb 2025 23:47:38 -0500 Subject: [PATCH 03/21] Grade Calc logic --- .../project.pbxproj | 12 ++ .../GradeCalculatorViewModel.swift | 122 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index daa7c48a..4e1297e8 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ B5894F192D6EC56000E8F527 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5894F182D6EC56000E8F527 /* PageView.swift */; }; B72EA5702D5689B20013070E /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B72EA56F2D5689B20013070E /* QuickLookPreview.swift */; }; B73AE0BD2D4FB68B007094A8 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73AE0BC2D4FB68B007094A8 /* Profile.swift */; }; + B7460A942D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */; }; B74688A32D63DB59007A27FD /* Assignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74688A22D63DB59007A27FD /* Assignment.swift */; }; B76454FE2C8DF61B002DF00E /* Course.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454ED2C8DF61B002DF00E /* Course.swift */; }; B76454FF2C8DF61B002DF00E /* Enrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454EE2C8DF61B002DF00E /* Enrollment.swift */; }; @@ -277,6 +278,7 @@ B5894F182D6EC56000E8F527 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = ""; }; B72EA56F2D5689B20013070E /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; B73AE0BC2D4FB68B007094A8 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; + B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorViewModel.swift; sourceTree = ""; }; B74688A22D63DB59007A27FD /* Assignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assignment.swift; sourceTree = ""; }; B76454642C8BBC7F002DF00E /* CanvasPlusPlayground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CanvasPlusPlayground.app; sourceTree = BUILT_PRODUCTS_DIR; }; B76454ED2C8DF61B002DF00E /* Course.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Course.swift; sourceTree = ""; }; @@ -572,6 +574,7 @@ A324BA5F2D0798A1005F53FA /* Grades */ = { isa = PBXGroup; children = ( + B7460A902D6BFB050069CF5B /* GradeCalculator */, B7460A8F2D6BF9320069CF5B /* Models */, 7F8535732C98DE3C0023E384 /* CourseGradeView.swift */, A31A81E62D0CCB69003C37EB /* GradesViewModel.swift */, @@ -712,6 +715,14 @@ path = Models; sourceTree = ""; }; + B7460A902D6BFB050069CF5B /* GradeCalculator */ = { + isa = PBXGroup; + children = ( + B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */, + ); + path = GradeCalculator; + sourceTree = ""; + }; B764545B2C8BBC7F002DF00E = { isa = PBXGroup; children = ( @@ -1019,6 +1030,7 @@ B7D7512B2D3D5D8000F7B8B8 /* AllAnnouncementsView.swift in Sources */, A3E7F3892C954E0500DC4300 /* CanvasRequest.swift in Sources */, A352AD1A2D3EF1C1007EE6FC /* GetCourseUsersRequest.swift in Sources */, + B7460A942D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift in Sources */, B77FD0072D5309340049AA5E /* ProfilePicture.swift in Sources */, A35191412D283589001E415F /* ModulesViewModel.swift in Sources */, A3E7F3912C99317100DC4300 /* CourseTabsManager.swift in Sources */, diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift new file mode 100644 index 00000000..27f120b8 --- /dev/null +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift @@ -0,0 +1,122 @@ +// +// GradeCalculatorViewModel.swift +// CanvasPlusPlayground +// +// Created by Rahul on 2/23/25. +// + +import SwiftUI + +@Observable +class GradeCalculatorViewModel { + struct GradeAssignment: Identifiable { + let id: String + let name: String + var pointsEarned: Double? + let pointsPossible: Double? + + var percentage: Double? { + guard let pointsEarned, let pointsPossible, pointsPossible > 0 else { + return nil + } + return (pointsEarned / pointsPossible) * 100 + } + } + + struct GradeGroup: Identifiable { + let id: String + let name: String + var weight: Double + var assignments: [GradeAssignment] + + var weightedScore: Double? { + guard !assignments.isEmpty, assignments + .contains( where: { $0.pointsEarned != nil }) else { + return nil + } + + let totalPossible = assignments.reduce(0.0) { + guard $1.pointsEarned != nil else { return $0 } + + guard let pointsPossible = $1.pointsPossible else { return $0 } + + return $0 + pointsPossible + } + + let totalEarned: Double = assignments.reduce(0.0) { + guard let pointsEarned = $1.pointsEarned else { return $0 } + + return $0 + pointsEarned + } + + guard totalPossible > 0 else { return nil } + + return (totalEarned / totalPossible) * weight + } + } + + var gradeGroups: [GradeGroup] = [] + var totalGrade: Double = 0.0 + + private func calculateTotalGrade() { + let totalWeight = gradeGroups.reduce(0.0) { + $0 + $1.weight + } + + if totalWeight > 0 { + var usedWeightage = 0.0 + + let weightedTotal = gradeGroups.reduce(0.0) { sum, group in + guard let weightedScore = group.weightedScore else { return sum } + + usedWeightage += group.weight + + print("Group: \(group.name), Weight: \(group.weight), Score: \(weightedScore)") + + return sum + weightedScore + } + + print("Weighted Total: \(weightedTotal), Used Weightage: \(usedWeightage)") + + totalGrade = (weightedTotal / usedWeightage) * 100 + } else { + var totalPoints = 0.0 + var totalPossible = 0.0 + + for group in gradeGroups { + for assignment in group.assignments { + if let earned = assignment.pointsEarned, + let possible = assignment.pointsPossible, + possible > 0 { + totalPoints += earned + totalPossible += possible + } + } + } + + totalGrade = totalPossible > 0 ? (totalPoints / totalPossible) * 100 : 0.0 + } + } + + init(assignmentGroups: [AssignmentGroup]) { + self.gradeGroups = assignmentGroups.map { group in + let assignments = group.assignments?.map { $0.createModel() }.map { assignment in + GradeAssignment( + id: assignment.id, + name: assignment.name, + pointsEarned: assignment.submission?.score, + pointsPossible: assignment.pointsPossible ?? 0.0 + ) + } ?? [] + + return GradeGroup( + id: group.id, + name: group.name, + weight: group.groupWeight ?? 0.0, + assignments: assignments + ) + } + + calculateTotalGrade() + } +} From 163dcef907f9fc45770beb2f4e48ed74556de4a0 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Mon, 24 Feb 2025 19:08:25 -0500 Subject: [PATCH 04/21] Grade Calc logic --- .../Assignments/Models/AssignmentGroup.swift | 12 +--- .../Models/AssignmentGroupAPI.swift | 13 ++++- .../GradeCalculatorViewModel.swift | 57 ++++++++++++++++--- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroup.swift b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroup.swift index 7cdfd626..cc84964d 100644 --- a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroup.swift +++ b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroup.swift @@ -19,9 +19,7 @@ class AssignmentGroup: Cacheable { var assignments: [AssignmentAPI]? // Rules - var dropHighest: Int? - var dropLowest: Int? - var neverDrop: [Int]? + var rules: AssignmentGroupRules? // MARK: Custom Properties var tag: String @@ -33,9 +31,7 @@ class AssignmentGroup: Cacheable { self.groupWeight = groupAPI.group_weight self.assignments = groupAPI.assignments - self.dropHighest = groupAPI.rules?.drop_highest - self.dropLowest = groupAPI.rules?.drop_lowest - self.neverDrop = groupAPI.rules?.never_drop + self.rules = groupAPI.rules self.tag = "" } @@ -44,9 +40,7 @@ class AssignmentGroup: Cacheable { self.name = other.name self.position = other.position self.groupWeight = other.groupWeight - self.dropHighest = other.dropHighest - self.dropLowest = other.dropLowest - self.neverDrop = other.neverDrop + self.rules = other.rules self.assignments = other.assignments } } diff --git a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift index 317c8b2f..a4040fcd 100644 --- a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift +++ b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift @@ -21,9 +21,16 @@ struct AssignmentGroupAPI: APIResponse { AssignmentGroup(from: self) } } +// swiftlint:enable identifier_name struct AssignmentGroupRules: Codable { - let drop_highest: Int? - let drop_lowest: Int? - let never_drop: [Int]? + let dropHighest: Int? + let dropLowest: Int? + let neverDrop: [Int]? + + enum CodingKeys: String, CodingKey { + case dropHighest = "drop_highest" + case dropLowest = "drop_lowest" + case neverDrop = "never_drop" + } } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift index 27f120b8..2fde906e 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift @@ -29,13 +29,48 @@ class GradeCalculatorViewModel { var weight: Double var assignments: [GradeAssignment] + var rules: AssignmentGroupRules? + var weightedScore: Double? { - guard !assignments.isEmpty, assignments + guard weight > 0.0, !assignments.isEmpty, assignments .contains( where: { $0.pointsEarned != nil }) else { return nil } - let totalPossible = assignments.reduce(0.0) { + var consideredAssignments = assignments + + var neverDropAssignments = consideredAssignments.filter { + guard let idAsInt = $0.id.asInt, let neverDrop = rules?.neverDrop else { + return false + } + + return neverDrop.contains(idAsInt) + } + + consideredAssignments = consideredAssignments.filter { + guard let idAsInt = $0.id.asInt, let neverDrop = rules?.neverDrop else { + return true + } + + return !neverDrop.contains(idAsInt) + } + + if let dropLowest = rules?.dropLowest, dropLowest > 0 { + consideredAssignments = Array( + consideredAssignments + .dropFirst(min(dropLowest, consideredAssignments.count)) + ) + } + + consideredAssignments.sort { ($0.pointsEarned ?? 0.0) > ($1.pointsEarned ?? 0.0) } + + if let dropHighest = rules?.dropHighest, dropHighest > 0 { + consideredAssignments = Array(consideredAssignments.dropFirst(min(dropHighest, consideredAssignments.count))) + } + + consideredAssignments += neverDropAssignments + + let totalPossible = consideredAssignments.reduce(0.0) { guard $1.pointsEarned != nil else { return $0 } guard let pointsPossible = $1.pointsPossible else { return $0 } @@ -43,7 +78,7 @@ class GradeCalculatorViewModel { return $0 + pointsPossible } - let totalEarned: Double = assignments.reduce(0.0) { + let totalEarned: Double = consideredAssignments.reduce(0.0) { guard let pointsEarned = $1.pointsEarned else { return $0 } return $0 + pointsEarned @@ -66,17 +101,21 @@ class GradeCalculatorViewModel { if totalWeight > 0 { var usedWeightage = 0.0 - let weightedTotal = gradeGroups.reduce(0.0) { sum, group in + let weightedTotal = gradeGroups.reduce(0.0) {sum, group in guard let weightedScore = group.weightedScore else { return sum } usedWeightage += group.weight - print("Group: \(group.name), Weight: \(group.weight), Score: \(weightedScore)") + LoggerService.main.debug( + "Group: \(group.name), Weight: \(group.weight), Score: \(weightedScore)" + ) return sum + weightedScore } - print("Weighted Total: \(weightedTotal), Used Weightage: \(usedWeightage)") + LoggerService.main.debug( + "Weighted Total: \(weightedTotal), Used Weightage: \(usedWeightage)" + ) totalGrade = (weightedTotal / usedWeightage) * 100 } else { @@ -86,8 +125,7 @@ class GradeCalculatorViewModel { for group in gradeGroups { for assignment in group.assignments { if let earned = assignment.pointsEarned, - let possible = assignment.pointsPossible, - possible > 0 { + let possible = assignment.pointsPossible { totalPoints += earned totalPossible += possible } @@ -113,7 +151,8 @@ class GradeCalculatorViewModel { id: group.id, name: group.name, weight: group.groupWeight ?? 0.0, - assignments: assignments + assignments: assignments, + rules: group.rules ) } From 6552ae5c6788704cd6fb5ea8e56ba62322fbf214 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Mon, 24 Feb 2025 19:09:48 -0500 Subject: [PATCH 05/21] grade calc view --- .../project.pbxproj | 4 + .../Assignments/CourseAssignmentsView.swift | 19 +++ .../Models/AssignmentGroupAPI.swift | 2 +- .../GradeCalculator/GradeCalculatorView.swift | 120 ++++++++++++++++++ .../GradeCalculatorViewModel.swift | 19 ++- 5 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index 4e1297e8..faa2bc1e 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ B72EA5702D5689B20013070E /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B72EA56F2D5689B20013070E /* QuickLookPreview.swift */; }; B73AE0BD2D4FB68B007094A8 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73AE0BC2D4FB68B007094A8 /* Profile.swift */; }; B7460A942D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */; }; + B7460A962D6CCD730069CF5B /* GradeCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */; }; B74688A32D63DB59007A27FD /* Assignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74688A22D63DB59007A27FD /* Assignment.swift */; }; B76454FE2C8DF61B002DF00E /* Course.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454ED2C8DF61B002DF00E /* Course.swift */; }; B76454FF2C8DF61B002DF00E /* Enrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454EE2C8DF61B002DF00E /* Enrollment.swift */; }; @@ -279,6 +280,7 @@ B72EA56F2D5689B20013070E /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; B73AE0BC2D4FB68B007094A8 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorViewModel.swift; sourceTree = ""; }; + B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorView.swift; sourceTree = ""; }; B74688A22D63DB59007A27FD /* Assignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assignment.swift; sourceTree = ""; }; B76454642C8BBC7F002DF00E /* CanvasPlusPlayground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CanvasPlusPlayground.app; sourceTree = BUILT_PRODUCTS_DIR; }; B76454ED2C8DF61B002DF00E /* Course.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Course.swift; sourceTree = ""; }; @@ -718,6 +720,7 @@ B7460A902D6BFB050069CF5B /* GradeCalculator */ = { isa = PBXGroup; children = ( + B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */, B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */, ); path = GradeCalculator; @@ -972,6 +975,7 @@ B76455022C8DF61B002DF00E /* CourseFilesView.swift in Sources */, B76455032C8DF61B002DF00E /* HomeView.swift in Sources */, A3269E8E2CD5533F006F7D14 /* CanvasRepository.swift in Sources */, + B7460A962D6CCD730069CF5B /* GradeCalculatorView.swift in Sources */, A3049B5B2D0E5C18002F3166 /* QuizPermissions.swift in Sources */, B7C0A3C52D2F4C19003E5A36 /* GetAssignmentRequest.swift in Sources */, A3FFD03E2CE0065A006BAB51 /* NetworkError.swift in Sources */, diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index e577d3de..50518a4f 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -13,6 +13,7 @@ struct CourseAssignmentsView: View { @State private var assignmentManager: CourseAssignmentManager @State private var isLoadingAssignments = true + @State private var showingGradeCalculator = false init(course: Course, showGrades: Bool = false) { self.course = course @@ -62,6 +63,16 @@ struct CourseAssignmentsView: View { sectionHeader(for: assignmentGroup) } } + .toolbar { + if showGrades { + ToolbarItem(placement: .automatic) { + Button("Calculate Grades") { + showingGradeCalculator = true + } + .disabled(isLoadingAssignments) + } + } + } .task { await loadAssignments() } @@ -73,6 +84,14 @@ struct CourseAssignmentsView: View { .navigationDestination(for: Assignment.self) { assignment in AssignmentDetailView(assignment: assignment) } + .sheet(isPresented: $showingGradeCalculator) { + NavigationStack { + GradeCalculatorView( + assignmentGroups: assignmentManager.assignmentGroups + ) + } + .frame(width: 450, height: 600) + } } private func loadAssignments() async { diff --git a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift index a4040fcd..d093b3e3 100644 --- a/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift +++ b/CanvasPlusPlayground/Features/Assignments/Models/AssignmentGroupAPI.swift @@ -23,7 +23,7 @@ struct AssignmentGroupAPI: APIResponse { } // swiftlint:enable identifier_name -struct AssignmentGroupRules: Codable { +struct AssignmentGroupRules: Codable, Hashable { let dropHighest: Int? let dropLowest: Int? let neverDrop: [Int]? diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift new file mode 100644 index 00000000..257ba714 --- /dev/null +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -0,0 +1,120 @@ +// +// GradeCalculatorView.swift +// CanvasPlusPlayground +// +// Created by Rahul on 2/24/25. +// + +import SwiftUI + +struct GradeCalculatorView: View { + @Environment(\.dismiss) private var dismiss + + @State private var calculator: GradeCalculatorViewModel + @FocusState private var assignmentRowFocus: GradeCalculatorViewModel.GradeAssignment? + + init(assignmentGroups: [AssignmentGroup]) { + self._calculator = .init( + initialValue: .init(assignmentGroups: assignmentGroups) + ) + } + + var body: some View { + List { + ForEach($calculator.gradeGroups, id: \.id) { $group in + DisclosureGroup( + isExpanded: isExpanded(for: group) + ) { + ForEach($group.assignments, id: \.id) { $assignment in + assignmentRow(for: $assignment) + } + .onMove { + group.assignments.move(fromOffsets: $0, toOffset: $1) + } + } label: { + groupHeader(for: group) + } + } + .onMove { + calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) + } + } + .navigationTitle("Calculate Grades") + .toolbar { + ToolbarItem(placement: .destructiveAction) { + Text("Total: \(calculator.totalGrade)") + .contentTransition(.numericText()) + .animation(.default, value: calculator.totalGrade) + } + + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + #if os(macOS) + Text("Done") + #else + Image(systemName: "xmark") + #endif + } + .keyboardShortcut(assignmentRowFocus == nil ? .defaultAction : .none) + } + + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Done") { + assignmentRowFocus = nil + } + .bold() + } + } + } + + private func groupHeader(for group: GradeCalculatorViewModel.GradeGroup) -> some View { + HStack { + Text(group.name) + Spacer() + Text("\(group.weight.truncatingTrailingZeros)%") + } + .bold() + .padding(4) + } + + private func assignmentRow(for assignment: Binding) -> some View { + HStack { + Text(assignment.wrappedValue.name) + + Spacer() + + TextField( + "Score", + value: assignment.pointsEarned, + format: .number + ) + .focused($assignmentRowFocus, equals: assignment.wrappedValue) + .fixedSize() + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.tint) + #if os(iOS) + .keyboardType(.numberPad) + #endif + + Text( + " / " + + "\(assignment.wrappedValue.pointsPossible?.truncatingTrailingZeros ?? "-")" + ) + } + .padding(.vertical, 4) + } + + private func isExpanded( + for group: GradeCalculatorViewModel.GradeGroup + ) -> Binding { + .init { + calculator.expandedAssignmentGroups[group, default: true] + } set: { newValue in + calculator.expandedAssignmentGroups[group] = newValue + } + } +} diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift index 2fde906e..6e12a44b 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift @@ -9,11 +9,11 @@ import SwiftUI @Observable class GradeCalculatorViewModel { - struct GradeAssignment: Identifiable { + struct GradeAssignment: Identifiable, Hashable { let id: String let name: String var pointsEarned: Double? - let pointsPossible: Double? + var pointsPossible: Double? var percentage: Double? { guard let pointsEarned, let pointsPossible, pointsPossible > 0 else { @@ -23,7 +23,7 @@ class GradeCalculatorViewModel { } } - struct GradeGroup: Identifiable { + struct GradeGroup: Identifiable, Hashable { let id: String let name: String var weight: Double @@ -39,7 +39,7 @@ class GradeCalculatorViewModel { var consideredAssignments = assignments - var neverDropAssignments = consideredAssignments.filter { + let neverDropAssignments = consideredAssignments.filter { guard let idAsInt = $0.id.asInt, let neverDrop = rules?.neverDrop else { return false } @@ -90,8 +90,13 @@ class GradeCalculatorViewModel { } } - var gradeGroups: [GradeGroup] = [] + var gradeGroups: [GradeGroup] = [] { + didSet { + calculateTotalGrade() + } + } var totalGrade: Double = 0.0 + var expandedAssignmentGroups: [GradeGroup: Bool] = [:] private func calculateTotalGrade() { let totalWeight = gradeGroups.reduce(0.0) { @@ -156,6 +161,10 @@ class GradeCalculatorViewModel { ) } + expandedAssignmentGroups = Dictionary( + uniqueKeysWithValues: gradeGroups.lazy.map { ($0, true) } + ) + calculateTotalGrade() } } From 5070d929a3e66be828a39dd39d063c097f3fcb2e Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Mon, 24 Feb 2025 19:06:50 -0500 Subject: [PATCH 06/21] Show assignments not included in calculations --- .../Assignments/CourseAssignmentsView.swift | 33 +++++++++++++-- .../GradeCalculator/GradeCalculatorView.swift | 13 +++--- .../GradeCalculatorViewModel.swift | 40 +++++++++++-------- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index 50518a4f..7fe10d2e 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -10,7 +10,9 @@ import SwiftUI struct CourseAssignmentsView: View { let course: Course let showGrades: Bool + @State private var assignmentManager: CourseAssignmentManager + @State private var gradeCalculator: GradeCalculatorViewModel @State private var isLoadingAssignments = true @State private var showingGradeCalculator = false @@ -18,7 +20,15 @@ struct CourseAssignmentsView: View { init(course: Course, showGrades: Bool = false) { self.course = course self.showGrades = showGrades - _assignmentManager = .init(initialValue: CourseAssignmentManager(courseID: course.id)) + + let manager = CourseAssignmentManager(courseID: course.id) + _assignmentManager = .init(initialValue: manager) + + _gradeCalculator = .init( + initialValue: .init( + assignmentGroups: manager.assignmentGroups + ) + ) } var body: some View { @@ -86,17 +96,18 @@ struct CourseAssignmentsView: View { } .sheet(isPresented: $showingGradeCalculator) { NavigationStack { - GradeCalculatorView( - assignmentGroups: assignmentManager.assignmentGroups - ) + GradeCalculatorView() } .frame(width: 450, height: 600) + .environment(gradeCalculator) } + .environment(gradeCalculator) } private func loadAssignments() async { isLoadingAssignments = true await assignmentManager.fetchAssignmentGroups() + gradeCalculator.resetGroups(assignmentManager.assignmentGroups) isLoadingAssignments = false } @@ -114,9 +125,18 @@ struct CourseAssignmentsView: View { } struct AssignmentRow: View { + @Environment(GradeCalculatorViewModel.self) private var calculator + let assignment: Assignment let showGrades: Bool + var isDropped: Bool { + !calculator.gradeGroups + .flatMap(\.consideredAssignments) + .map(\.id) + .contains(assignment.id) + } + var body: some View { if !showGrades { NavigationLink(value: assignment) { @@ -154,6 +174,11 @@ struct AssignmentRow: View { Spacer() if showGrades { + if isDropped, !calculator.gradeGroups.isEmpty { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.secondary) + } + Text(assignment.formattedGrade) .bold() + diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 257ba714..d0bea082 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -8,18 +8,14 @@ import SwiftUI struct GradeCalculatorView: View { + @Environment(GradeCalculatorViewModel.self) private var calculator @Environment(\.dismiss) private var dismiss - @State private var calculator: GradeCalculatorViewModel @FocusState private var assignmentRowFocus: GradeCalculatorViewModel.GradeAssignment? - init(assignmentGroups: [AssignmentGroup]) { - self._calculator = .init( - initialValue: .init(assignmentGroups: assignmentGroups) - ) - } - var body: some View { + @Bindable var calculator = calculator + List { ForEach($calculator.gradeGroups, id: \.id) { $group in DisclosureGroup( @@ -42,7 +38,8 @@ struct GradeCalculatorView: View { .navigationTitle("Calculate Grades") .toolbar { ToolbarItem(placement: .destructiveAction) { - Text("Total: \(calculator.totalGrade)") + Text("Total: \(calculator.totalGrade.truncatingTrailingZeros)%") + .bold() .contentTransition(.numericText()) .animation(.default, value: calculator.totalGrade) } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift index 6e12a44b..77f0cc4a 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift @@ -31,15 +31,10 @@ class GradeCalculatorViewModel { var rules: AssignmentGroupRules? - var weightedScore: Double? { - guard weight > 0.0, !assignments.isEmpty, assignments - .contains( where: { $0.pointsEarned != nil }) else { - return nil - } + var consideredAssignments: [GradeAssignment] { + var retValue = assignments - var consideredAssignments = assignments - - let neverDropAssignments = consideredAssignments.filter { + let neverDropAssignments = retValue.filter { guard let idAsInt = $0.id.asInt, let neverDrop = rules?.neverDrop else { return false } @@ -47,7 +42,7 @@ class GradeCalculatorViewModel { return neverDrop.contains(idAsInt) } - consideredAssignments = consideredAssignments.filter { + retValue = retValue.filter { guard let idAsInt = $0.id.asInt, let neverDrop = rules?.neverDrop else { return true } @@ -56,19 +51,28 @@ class GradeCalculatorViewModel { } if let dropLowest = rules?.dropLowest, dropLowest > 0 { - consideredAssignments = Array( - consideredAssignments - .dropFirst(min(dropLowest, consideredAssignments.count)) + retValue = Array( + retValue + .dropFirst(min(dropLowest, retValue.count)) ) } - consideredAssignments.sort { ($0.pointsEarned ?? 0.0) > ($1.pointsEarned ?? 0.0) } + retValue.sort { ($0.pointsEarned ?? 0.0) > ($1.pointsEarned ?? 0.0) } if let dropHighest = rules?.dropHighest, dropHighest > 0 { - consideredAssignments = Array(consideredAssignments.dropFirst(min(dropHighest, consideredAssignments.count))) + retValue = Array(retValue.dropFirst(min(dropHighest, retValue.count))) } - consideredAssignments += neverDropAssignments + retValue += neverDropAssignments + + return retValue + } + + var weightedScore: Double? { + guard weight > 0.0, !assignments.isEmpty, assignments + .contains( where: { $0.pointsEarned != nil }) else { + return nil + } let totalPossible = consideredAssignments.reduce(0.0) { guard $1.pointsEarned != nil else { return $0 } @@ -142,6 +146,10 @@ class GradeCalculatorViewModel { } init(assignmentGroups: [AssignmentGroup]) { + resetGroups(assignmentGroups) + } + + func resetGroups(_ assignmentGroups: [AssignmentGroup]) { self.gradeGroups = assignmentGroups.map { group in let assignments = group.assignments?.map { $0.createModel() }.map { assignment in GradeAssignment( @@ -164,7 +172,5 @@ class GradeCalculatorViewModel { expandedAssignmentGroups = Dictionary( uniqueKeysWithValues: gradeGroups.lazy.map { ($0, true) } ) - - calculateTotalGrade() } } From 4121bc46d0f92fda37ae19069e5b34ecbd452207 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Tue, 25 Feb 2025 13:43:38 -0500 Subject: [PATCH 07/21] popover header --- .../Assignments/CourseAssignmentsView.swift | 101 +++++++++++++++--- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index 7fe10d2e..fee81ec0 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -70,7 +70,10 @@ struct CourseAssignmentsView: View { } } } header: { - sectionHeader(for: assignmentGroup) + GroupHeader( + assignmentGroup: assignmentGroup, + showGrades: showGrades + ) } } .toolbar { @@ -110,21 +113,9 @@ struct CourseAssignmentsView: View { gradeCalculator.resetGroups(assignmentManager.assignmentGroups) isLoadingAssignments = false } - - private func sectionHeader(for assignmentGroup: AssignmentGroup) -> some View { - HStack { - Text(assignmentGroup.name) - Spacer() - if let groupWeight = assignmentGroup.groupWeight { - Text(String(format: "%.1f%%", groupWeight)) - } else { - Text("--%") - } - } - } } -struct AssignmentRow: View { +private struct AssignmentRow: View { @Environment(GradeCalculatorViewModel.self) private var calculator let assignment: Assignment @@ -176,7 +167,7 @@ struct AssignmentRow: View { if showGrades { if isDropped, !calculator.gradeGroups.isEmpty { Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) + .foregroundStyle(.separator) } Text(assignment.formattedGrade) @@ -187,3 +178,83 @@ struct AssignmentRow: View { } } } + +private struct GroupHeader: View { + let assignmentGroup: AssignmentGroup + let showGrades: Bool + + @State private var showingInfo = false + + private var showsInfoButton: Bool { + assignmentGroup.rules?.dropLowest != nil || + assignmentGroup.rules?.dropHighest != nil || + !(assignmentGroup.rules?.neverDrop?.isEmpty ?? true) + } + + var body: some View { + HStack { + Text(assignmentGroup.name) + + Spacer() + + if let groupWeight = assignmentGroup.groupWeight { + Text(String(format: "%.1f%%", groupWeight)) + } else { + Text("--%") + } + + if showGrades, showsInfoButton { + Button("Show Rules", systemImage: "info.circle") { + showingInfo = true + } + .popover(isPresented: $showingInfo) { + infoGrid + .presentationCompactAdaptation(.popover) + .presentationBackground(.thinMaterial) + } + .buttonStyle(.plain) + .labelStyle(.iconOnly) + } + } + } + + private var infoGrid: some View { + VStack { + Text("Rules").fontWeight(.heavy) + + Spacer() + + Grid { + if let dropLowest = assignmentGroup.rules?.dropLowest { + GridRow { + Text("Drop Lowest:") + .fontWeight(.light) + + Spacer() + + Text( + dropLowest, + format: .number + ) + } + } + + if let dropHighest = assignmentGroup.rules?.dropHighest { + GridRow { + Text("Drop Highest:") + .fontWeight(.light) + + Spacer() + + Text( + dropHighest, + format: .number + ) + .bold() + } + } + } + } + .padding(16) + } +} From 5a3522020a10692c987b4bfee8e179c55e349885 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Tue, 25 Feb 2025 23:30:25 -0500 Subject: [PATCH 08/21] rename grade calc --- CanvasPlusPlayground.xcodeproj/project.pbxproj | 8 ++++---- .../Features/Assignments/CourseAssignmentsView.swift | 4 ++-- ...lculatorViewModel.swift => GradeCalculator.swift} | 2 +- .../Grades/GradeCalculator/GradeCalculatorView.swift | 12 +++++++----- 4 files changed, 14 insertions(+), 12 deletions(-) rename CanvasPlusPlayground/Features/Grades/GradeCalculator/{GradeCalculatorViewModel.swift => GradeCalculator.swift} (99%) diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index faa2bc1e..2da5b9f4 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -115,7 +115,7 @@ B5894F192D6EC56000E8F527 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5894F182D6EC56000E8F527 /* PageView.swift */; }; B72EA5702D5689B20013070E /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B72EA56F2D5689B20013070E /* QuickLookPreview.swift */; }; B73AE0BD2D4FB68B007094A8 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73AE0BC2D4FB68B007094A8 /* Profile.swift */; }; - B7460A942D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */; }; + B7460A942D6BFB1A0069CF5B /* GradeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A932D6BFB1A0069CF5B /* GradeCalculator.swift */; }; B7460A962D6CCD730069CF5B /* GradeCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */; }; B74688A32D63DB59007A27FD /* Assignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74688A22D63DB59007A27FD /* Assignment.swift */; }; B76454FE2C8DF61B002DF00E /* Course.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454ED2C8DF61B002DF00E /* Course.swift */; }; @@ -279,7 +279,7 @@ B5894F182D6EC56000E8F527 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = ""; }; B72EA56F2D5689B20013070E /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; B73AE0BC2D4FB68B007094A8 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; - B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorViewModel.swift; sourceTree = ""; }; + B7460A932D6BFB1A0069CF5B /* GradeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculator.swift; sourceTree = ""; }; B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorView.swift; sourceTree = ""; }; B74688A22D63DB59007A27FD /* Assignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assignment.swift; sourceTree = ""; }; B76454642C8BBC7F002DF00E /* CanvasPlusPlayground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CanvasPlusPlayground.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -721,7 +721,7 @@ isa = PBXGroup; children = ( B7460A952D6CCD6E0069CF5B /* GradeCalculatorView.swift */, - B7460A932D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift */, + B7460A932D6BFB1A0069CF5B /* GradeCalculator.swift */, ); path = GradeCalculator; sourceTree = ""; @@ -1034,7 +1034,7 @@ B7D7512B2D3D5D8000F7B8B8 /* AllAnnouncementsView.swift in Sources */, A3E7F3892C954E0500DC4300 /* CanvasRequest.swift in Sources */, A352AD1A2D3EF1C1007EE6FC /* GetCourseUsersRequest.swift in Sources */, - B7460A942D6BFB1A0069CF5B /* GradeCalculatorViewModel.swift in Sources */, + B7460A942D6BFB1A0069CF5B /* GradeCalculator.swift in Sources */, B77FD0072D5309340049AA5E /* ProfilePicture.swift in Sources */, A35191412D283589001E415F /* ModulesViewModel.swift in Sources */, A3E7F3912C99317100DC4300 /* CourseTabsManager.swift in Sources */, diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index fee81ec0..948041b6 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -12,7 +12,7 @@ struct CourseAssignmentsView: View { let showGrades: Bool @State private var assignmentManager: CourseAssignmentManager - @State private var gradeCalculator: GradeCalculatorViewModel + @State private var gradeCalculator: GradeCalculator @State private var isLoadingAssignments = true @State private var showingGradeCalculator = false @@ -116,7 +116,7 @@ struct CourseAssignmentsView: View { } private struct AssignmentRow: View { - @Environment(GradeCalculatorViewModel.self) private var calculator + @Environment(GradeCalculator.self) private var calculator let assignment: Assignment let showGrades: Bool diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift similarity index 99% rename from CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift rename to CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 77f0cc4a..b7de952d 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorViewModel.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -8,7 +8,7 @@ import SwiftUI @Observable -class GradeCalculatorViewModel { +class GradeCalculator { struct GradeAssignment: Identifiable, Hashable { let id: String let name: String diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index d0bea082..1d08ea6f 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -8,10 +8,10 @@ import SwiftUI struct GradeCalculatorView: View { - @Environment(GradeCalculatorViewModel.self) private var calculator + @Environment(GradeCalculator.self) private var calculator @Environment(\.dismiss) private var dismiss - @FocusState private var assignmentRowFocus: GradeCalculatorViewModel.GradeAssignment? + @FocusState private var assignmentRowFocus: GradeCalculator.GradeAssignment? var body: some View { @Bindable var calculator = calculator @@ -27,6 +27,8 @@ struct GradeCalculatorView: View { .onMove { group.assignments.move(fromOffsets: $0, toOffset: $1) } + + } label: { groupHeader(for: group) } @@ -67,7 +69,7 @@ struct GradeCalculatorView: View { } } - private func groupHeader(for group: GradeCalculatorViewModel.GradeGroup) -> some View { + private func groupHeader(for group: GradeCalculator.GradeGroup) -> some View { HStack { Text(group.name) Spacer() @@ -77,7 +79,7 @@ struct GradeCalculatorView: View { .padding(4) } - private func assignmentRow(for assignment: Binding) -> some View { + private func assignmentRow(for assignment: Binding) -> some View { HStack { Text(assignment.wrappedValue.name) @@ -106,7 +108,7 @@ struct GradeCalculatorView: View { } private func isExpanded( - for group: GradeCalculatorViewModel.GradeGroup + for group: GradeCalculator.GradeGroup ) -> Binding { .init { calculator.expandedAssignmentGroups[group, default: true] From 0f95c17dca9f4a7b16f3291a19184c35cd666954 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Tue, 25 Feb 2025 23:43:11 -0500 Subject: [PATCH 09/21] Create Assignments and Groups --- .../GradeCalculator/GradeCalculator.swift | 85 ++++++++++++------- .../GradeCalculator/GradeCalculatorView.swift | 50 +++++++---- 2 files changed, 87 insertions(+), 48 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index b7de952d..2a0dc14b 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -11,9 +11,9 @@ import SwiftUI class GradeCalculator { struct GradeAssignment: Identifiable, Hashable { let id: String - let name: String + var name: String var pointsEarned: Double? - var pointsPossible: Double? + var pointsPossible: Double? = 0.0 var percentage: Double? { guard let pointsEarned, let pointsPossible, pointsPossible > 0 else { @@ -102,6 +102,58 @@ class GradeCalculator { var totalGrade: Double = 0.0 var expandedAssignmentGroups: [GradeGroup: Bool] = [:] + init(assignmentGroups: [AssignmentGroup]) { + resetGroups(assignmentGroups) + } + + // MARK: - User Intents + func createEmptyGroup() { + gradeGroups.append( + .init( + id: UUID().uuidString, + name: "New Group", + weight: 0.0, + assignments: [] + ) + ) + } + + func createEmptyAssignment(in group: GradeGroup) { + guard let indexOfGroup = gradeGroups.firstIndex(of: group) else { + return + } + + gradeGroups[indexOfGroup].assignments + .append(.init(id: UUID().uuidString, name: "")) + } + + // MARK: - Helpers + func resetGroups(_ assignmentGroups: [AssignmentGroup]) { + self.gradeGroups = assignmentGroups.map { group in + let assignments = group.assignments?.map { $0.createModel() }.map { assignment in + GradeAssignment( + id: assignment.id, + name: assignment.name, + pointsEarned: assignment.submission?.score, + pointsPossible: assignment.pointsPossible ?? 0.0 + ) + } ?? [] + + return GradeGroup( + id: group.id, + name: group.name, + weight: group.groupWeight ?? 0.0, + assignments: assignments, + rules: group.rules + ) + } + + expandedAssignmentGroups = Dictionary( + uniqueKeysWithValues: gradeGroups.lazy.map { ($0, true) } + ) + } + + // MARK: - Private private func calculateTotalGrade() { let totalWeight = gradeGroups.reduce(0.0) { $0 + $1.weight @@ -144,33 +196,4 @@ class GradeCalculator { totalGrade = totalPossible > 0 ? (totalPoints / totalPossible) * 100 : 0.0 } } - - init(assignmentGroups: [AssignmentGroup]) { - resetGroups(assignmentGroups) - } - - func resetGroups(_ assignmentGroups: [AssignmentGroup]) { - self.gradeGroups = assignmentGroups.map { group in - let assignments = group.assignments?.map { $0.createModel() }.map { assignment in - GradeAssignment( - id: assignment.id, - name: assignment.name, - pointsEarned: assignment.submission?.score, - pointsPossible: assignment.pointsPossible ?? 0.0 - ) - } ?? [] - - return GradeGroup( - id: group.id, - name: group.name, - weight: group.groupWeight ?? 0.0, - assignments: assignments, - rules: group.rules - ) - } - - expandedAssignmentGroups = Dictionary( - uniqueKeysWithValues: gradeGroups.lazy.map { ($0, true) } - ) - } } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 1d08ea6f..17954883 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -21,14 +21,21 @@ struct GradeCalculatorView: View { DisclosureGroup( isExpanded: isExpanded(for: group) ) { - ForEach($group.assignments, id: \.id) { $assignment in - assignmentRow(for: $assignment) - } - .onMove { - group.assignments.move(fromOffsets: $0, toOffset: $1) - } + Group { + ForEach($group.assignments, id: \.id) { $assignment in + assignmentRow(for: $assignment) + } + .onMove { + group.assignments.move(fromOffsets: $0, toOffset: $1) + } - + Button("Add Assignment", systemImage: "plus.circle.fill") { + calculator.createEmptyAssignment(in: group) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) } label: { groupHeader(for: group) } @@ -36,6 +43,12 @@ struct GradeCalculatorView: View { .onMove { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) } + + Button("Add Assignment Group", systemImage: "plus.circle.fill") { + calculator.createEmptyGroup() + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) } .navigationTitle("Calculate Grades") .toolbar { @@ -81,7 +94,11 @@ struct GradeCalculatorView: View { private func assignmentRow(for assignment: Binding) -> some View { HStack { - Text(assignment.wrappedValue.name) + TextField( + "Assignment Name", + text: assignment.name, + prompt: Text("Assignment Name") + ) Spacer() @@ -90,21 +107,20 @@ struct GradeCalculatorView: View { value: assignment.pointsEarned, format: .number ) - .focused($assignmentRowFocus, equals: assignment.wrappedValue) - .fixedSize() - .font(.title3) - .fontWeight(.semibold) - .foregroundStyle(.tint) - #if os(iOS) - .keyboardType(.numberPad) - #endif + .fixedSize() + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.tint) + #if os(iOS) + .keyboardType(.numberPad) + #endif Text( " / " + "\(assignment.wrappedValue.pointsPossible?.truncatingTrailingZeros ?? "-")" ) } - .padding(.vertical, 4) + .focused($assignmentRowFocus, equals: assignment.wrappedValue) } private func isExpanded( From 8d7888b90ffc1c7dbcb76d48e8949bb8d3104784 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Wed, 26 Feb 2025 10:51:22 -0500 Subject: [PATCH 10/21] Refinements --- .../Assignments/CourseAssignmentsView.swift | 4 +++- .../Grades/GradeCalculator/GradeCalculator.swift | 14 +++++++++----- .../GradeCalculator/GradeCalculatorView.swift | 14 +++++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index 948041b6..79188bb1 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -101,7 +101,9 @@ struct CourseAssignmentsView: View { NavigationStack { GradeCalculatorView() } - .frame(width: 450, height: 600) + #if os(macOS) + .frame(width: 550, height: 650) + #endif .environment(gradeCalculator) } .environment(gradeCalculator) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 2a0dc14b..4dfe8634 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -118,13 +118,16 @@ class GradeCalculator { ) } - func createEmptyAssignment(in group: GradeGroup) { + @discardableResult + func createEmptyAssignment(in group: GradeGroup) -> GradeAssignment? { guard let indexOfGroup = gradeGroups.firstIndex(of: group) else { - return + return nil } - gradeGroups[indexOfGroup].assignments - .append(.init(id: UUID().uuidString, name: "")) + let newAssignment = GradeAssignment(id: UUID().uuidString, name: "") + gradeGroups[indexOfGroup].assignments.append(newAssignment) + + return newAssignment } // MARK: - Helpers @@ -149,7 +152,8 @@ class GradeCalculator { } expandedAssignmentGroups = Dictionary( - uniqueKeysWithValues: gradeGroups.lazy.map { ($0, true) } + uniqueKeysWithValues: gradeGroups.lazy + .map { ($0, !$0.assignments.isEmpty) } ) } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 17954883..4ac090d7 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -30,7 +30,8 @@ struct GradeCalculatorView: View { } Button("Add Assignment", systemImage: "plus.circle.fill") { - calculator.createEmptyAssignment(in: group) + let newAssignment = calculator.createEmptyAssignment(in: group) + assignmentRowFocus = newAssignment } .buttonStyle(.borderless) .foregroundStyle(.secondary) @@ -44,12 +45,23 @@ struct GradeCalculatorView: View { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) } + #if os(macOS) + Divider() + #endif + Button("Add Assignment Group", systemImage: "plus.circle.fill") { calculator.createEmptyGroup() } .buttonStyle(.borderless) .foregroundStyle(.secondary) } + #if os(macOS) + .listStyle(.sidebar) + #else + .listStyle(.inset) + #endif + .scrollContentBackground(.hidden) + .background(.background) .navigationTitle("Calculate Grades") .toolbar { ToolbarItem(placement: .destructiveAction) { From 4d0f5e78753dbc92a8be0273629ec549340971b2 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Wed, 26 Feb 2025 11:23:34 -0500 Subject: [PATCH 11/21] more changes --- .../GradeCalculator/GradeCalculator.swift | 21 ++++++----- .../GradeCalculator/GradeCalculatorView.swift | 35 +++++++++++++++---- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 4dfe8634..858217c7 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -25,7 +25,7 @@ class GradeCalculator { struct GradeGroup: Identifiable, Hashable { let id: String - let name: String + var name: String var weight: Double var assignments: [GradeAssignment] @@ -107,15 +107,18 @@ class GradeCalculator { } // MARK: - User Intents - func createEmptyGroup() { - gradeGroups.append( - .init( - id: UUID().uuidString, - name: "New Group", - weight: 0.0, - assignments: [] - ) + @discardableResult + func createEmptyGroup() -> GradeGroup { + let newGroup = GradeGroup( + id: UUID().uuidString, + name: "New Group", + weight: 0.0, + assignments: [] ) + + gradeGroups.append(newGroup) + + return newGroup } @discardableResult diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 4ac090d7..16a012ed 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -12,6 +12,7 @@ struct GradeCalculatorView: View { @Environment(\.dismiss) private var dismiss @FocusState private var assignmentRowFocus: GradeCalculator.GradeAssignment? + @FocusState private var groupRowFocus: GradeCalculator.GradeGroup? var body: some View { @Bindable var calculator = calculator @@ -38,7 +39,7 @@ struct GradeCalculatorView: View { } .padding(.vertical, 4) } label: { - groupHeader(for: group) + groupHeader(for: $group) } } .onMove { @@ -50,7 +51,8 @@ struct GradeCalculatorView: View { #endif Button("Add Assignment Group", systemImage: "plus.circle.fill") { - calculator.createEmptyGroup() + let newGroup = calculator.createEmptyGroup() + groupRowFocus = newGroup } .buttonStyle(.borderless) .foregroundStyle(.secondary) @@ -81,7 +83,9 @@ struct GradeCalculatorView: View { Image(systemName: "xmark") #endif } - .keyboardShortcut(assignmentRowFocus == nil ? .defaultAction : .none) + .keyboardShortcut( + assignmentRowFocus == nil && groupRowFocus == nil ? .defaultAction : .none + ) } ToolbarItemGroup(placement: .keyboard) { @@ -94,14 +98,33 @@ struct GradeCalculatorView: View { } } - private func groupHeader(for group: GradeCalculator.GradeGroup) -> some View { + @ViewBuilder + private func groupHeader(for group: Binding) -> some View { + let formattedWeightBinding: Binding = .init { + group.wrappedValue.weight / 100.0 + } set: { + group.wrappedValue.weight = $0 * 100.0 + } + HStack { - Text(group.name) + TextField("Assignment Group Name", text: group.name) + Spacer() - Text("\(group.weight.truncatingTrailingZeros)%") + + TextField( + "Weight", + value: formattedWeightBinding, + format: .percent + ) + .fixedSize() + .foregroundStyle(.tint) + #if os(iOS) + .keyboardType(.numberPad) + #endif } .bold() .padding(4) + .focused($groupRowFocus, equals: group.wrappedValue) } private func assignmentRow(for assignment: Binding) -> some View { From 2c15839e678ba1dd8bb2311b96c21f5f1f07e2bd Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Wed, 26 Feb 2025 16:27:37 -0500 Subject: [PATCH 12/21] Draggable --- .../GradeCalculator/GradeCalculator.swift | 32 ++++++++++++++++++- .../GradeCalculator/GradeCalculatorView.swift | 10 ++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 858217c7..6501c3d7 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -9,7 +9,7 @@ import SwiftUI @Observable class GradeCalculator { - struct GradeAssignment: Identifiable, Hashable { + struct GradeAssignment: Identifiable, Hashable, Transferable, Codable { let id: String var name: String var pointsEarned: Double? @@ -21,6 +21,10 @@ class GradeCalculator { } return (pointsEarned / pointsPossible) * 100 } + + static var transferRepresentation: some TransferRepresentation { + CodableRepresentation(contentType: .item) + } } struct GradeGroup: Identifiable, Hashable { @@ -133,6 +137,32 @@ class GradeCalculator { return newAssignment } + func moveAssignments( + _ assignments: [GradeAssignment], + to newGroup: GradeGroup + ) -> Bool { + guard let newGroupIndex = gradeGroups.firstIndex(of: newGroup) else { + return false + } + + let assignmentsToAdd = assignments.filter { + !gradeGroups[newGroupIndex].assignments.contains($0) + } + + guard !assignmentsToAdd.isEmpty else { return false } + + gradeGroups[newGroupIndex].assignments + .append(contentsOf: assignmentsToAdd) + + for assignment in assignments { + for groupIndex in gradeGroups.indices where groupIndex != newGroupIndex { + gradeGroups[groupIndex].assignments.removeAll { $0.id == assignment.id } + } + } + + return true + } + // MARK: - Helpers func resetGroups(_ assignmentGroups: [AssignmentGroup]) { self.gradeGroups = assignmentGroups.map { group in diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 16a012ed..f45998ff 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -14,6 +14,8 @@ struct GradeCalculatorView: View { @FocusState private var assignmentRowFocus: GradeCalculator.GradeAssignment? @FocusState private var groupRowFocus: GradeCalculator.GradeGroup? + @State private var targetedGroup: GradeCalculator.GradeGroup? + var body: some View { @Bindable var calculator = calculator @@ -41,6 +43,13 @@ struct GradeCalculatorView: View { } label: { groupHeader(for: $group) } + .contentShape(.rect) + .dropDestination(for: GradeCalculator.GradeAssignment.self) { assignments, _ in + calculator.moveAssignments(assignments, to: group) + } isTargeted: { + targetedGroup = $0 ? group : nil + } + .listRowBackground(targetedGroup == group ? Color.blue : Color.clear) } .onMove { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) @@ -156,6 +165,7 @@ struct GradeCalculatorView: View { ) } .focused($assignmentRowFocus, equals: assignment.wrappedValue) + .draggable(assignment.wrappedValue) } private func isExpanded( From 342706bd53af914c6f75e817bbc0e80a03bfbb57 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Wed, 26 Feb 2025 23:45:25 -0500 Subject: [PATCH 13/21] Extra credit --- .../Features/Grades/GradeCalculator/GradeCalculator.swift | 4 +++- .../Features/Grades/GradeCalculator/GradeCalculatorView.swift | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 6501c3d7..a28b036a 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -92,7 +92,9 @@ class GradeCalculator { return $0 + pointsEarned } - guard totalPossible > 0 else { return nil } + guard totalPossible > 0 else { + return totalEarned > 0 ? totalEarned * weight : nil + } return (totalEarned / totalPossible) * weight } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index f45998ff..9332b5a6 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -49,7 +49,6 @@ struct GradeCalculatorView: View { } isTargeted: { targetedGroup = $0 ? group : nil } - .listRowBackground(targetedGroup == group ? Color.blue : Color.clear) } .onMove { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) From e23ac281d807b5ce40997d0e0c25e42fdfb5860b Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Thu, 27 Feb 2025 16:04:32 -0500 Subject: [PATCH 14/21] Update GradeCalculator and GradeCalculatorView --- .../Grades/GradeCalculator/GradeCalculator.swift | 6 ++---- .../GradeCalculator/GradeCalculatorView.swift | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index a28b036a..3cd43019 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -92,9 +92,7 @@ class GradeCalculator { return $0 + pointsEarned } - guard totalPossible > 0 else { - return totalEarned > 0 ? totalEarned * weight : nil - } + guard totalPossible > 0 else { return nil } return (totalEarned / totalPossible) * weight } @@ -217,7 +215,7 @@ class GradeCalculator { "Weighted Total: \(weightedTotal), Used Weightage: \(usedWeightage)" ) - totalGrade = (weightedTotal / usedWeightage) * 100 + totalGrade = max((weightedTotal / usedWeightage) * 100, weightedTotal) } else { var totalPoints = 0.0 var totalPossible = 0.0 diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 9332b5a6..eebfffd3 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -158,10 +158,20 @@ struct GradeCalculatorView: View { .keyboardType(.numberPad) #endif - Text( - " / " + - "\(assignment.wrappedValue.pointsPossible?.truncatingTrailingZeros ?? "-")" + Text("/") + + TextField( + "Total", + value: assignment.pointsPossible, + format: .number ) + .fixedSize() + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.tint) + #if os(iOS) + .keyboardType(.numberPad) + #endif } .focused($assignmentRowFocus, equals: assignment.wrappedValue) .draggable(assignment.wrappedValue) From fc4e2e3bbedd05d168f81aedfb2f1013fd7b4cb6 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Thu, 27 Feb 2025 23:01:53 -0500 Subject: [PATCH 15/21] Refactor --- .../GradeCalculator/GradeCalculatorView.swift | 107 +++++++++++------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index eebfffd3..bde9ee3b 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -21,28 +21,11 @@ struct GradeCalculatorView: View { List { ForEach($calculator.gradeGroups, id: \.id) { $group in - DisclosureGroup( - isExpanded: isExpanded(for: group) - ) { - Group { - ForEach($group.assignments, id: \.id) { $assignment in - assignmentRow(for: $assignment) - } - .onMove { - group.assignments.move(fromOffsets: $0, toOffset: $1) - } - - Button("Add Assignment", systemImage: "plus.circle.fill") { - let newAssignment = calculator.createEmptyAssignment(in: group) - assignmentRowFocus = newAssignment - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - } - .padding(.vertical, 4) - } label: { - groupHeader(for: $group) - } + GradeGroupSection( + group: $group, + assignmentRowFocus: _assignmentRowFocus, + groupRowFocus: _groupRowFocus + ) .contentShape(.rect) .dropDestination(for: GradeCalculator.GradeAssignment.self) { assignments, _ in calculator.moveAssignments(assignments, to: group) @@ -105,17 +88,57 @@ struct GradeCalculatorView: View { } } } +} + +private struct GradeGroupSection: View { + @Environment(GradeCalculator.self) private var calculator + @Binding var group: GradeCalculator.GradeGroup + @FocusState var assignmentRowFocus: GradeCalculator.GradeAssignment? + @FocusState var groupRowFocus: GradeCalculator.GradeGroup? + + var body: some View { + DisclosureGroup(isExpanded: isExpanded) { + ForEach($group.assignments, id: \.id) { $assignment in + GradeAssignmentRow(assignment: $assignment, assignmentRowFocus: _assignmentRowFocus) + } + .onMove { + group.assignments.move(fromOffsets: $0, toOffset: $1) + } + + Button("Add Assignment", systemImage: "plus.circle.fill") { + let newAssignment = calculator.createEmptyAssignment(in: group) + assignmentRowFocus = newAssignment + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .padding(4) + } label: { + GradeGroupHeader(group: $group, groupRowFocus: _groupRowFocus) + } + } + + var isExpanded: Binding { + .init { + calculator.expandedAssignmentGroups[group, default: true] + } set: { newValue in + calculator.expandedAssignmentGroups[group] = newValue + } + } +} + +private struct GradeGroupHeader: View { + @Binding var group: GradeCalculator.GradeGroup + @FocusState var groupRowFocus: GradeCalculator.GradeGroup? - @ViewBuilder - private func groupHeader(for group: Binding) -> some View { + var body: some View { let formattedWeightBinding: Binding = .init { - group.wrappedValue.weight / 100.0 + group.weight / 100.0 } set: { - group.wrappedValue.weight = $0 * 100.0 + group.weight = $0 * 100.0 } HStack { - TextField("Assignment Group Name", text: group.name) + TextField("Assignment Group Name", text: $group.name) Spacer() @@ -132,14 +155,19 @@ struct GradeCalculatorView: View { } .bold() .padding(4) - .focused($groupRowFocus, equals: group.wrappedValue) + .focused($groupRowFocus, equals: group) } +} + +private struct GradeAssignmentRow: View { + @Binding var assignment: GradeCalculator.GradeAssignment + @FocusState var assignmentRowFocus: GradeCalculator.GradeAssignment? - private func assignmentRow(for assignment: Binding) -> some View { + var body: some View { HStack { TextField( "Assignment Name", - text: assignment.name, + text: $assignment.name, prompt: Text("Assignment Name") ) @@ -147,7 +175,7 @@ struct GradeCalculatorView: View { TextField( "Score", - value: assignment.pointsEarned, + value: $assignment.pointsEarned, format: .number ) .fixedSize() @@ -162,7 +190,7 @@ struct GradeCalculatorView: View { TextField( "Total", - value: assignment.pointsPossible, + value: $assignment.pointsPossible, format: .number ) .fixedSize() @@ -173,17 +201,8 @@ struct GradeCalculatorView: View { .keyboardType(.numberPad) #endif } - .focused($assignmentRowFocus, equals: assignment.wrappedValue) - .draggable(assignment.wrappedValue) - } - - private func isExpanded( - for group: GradeCalculator.GradeGroup - ) -> Binding { - .init { - calculator.expandedAssignmentGroups[group, default: true] - } set: { newValue in - calculator.expandedAssignmentGroups[group] = newValue - } + .focused($assignmentRowFocus, equals: assignment) + .draggable(assignment) + .padding(4) } } From 5ff80c541cd5f4d5daba38d08897108f14f49e2d Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Thu, 27 Feb 2025 23:20:31 -0500 Subject: [PATCH 16/21] more refactoring --- .../GradeCalculator/GradeCalculator.swift | 2 +- .../GradeCalculator/GradeCalculatorView.swift | 47 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index 3cd43019..e1f1a6dd 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -186,7 +186,7 @@ class GradeCalculator { expandedAssignmentGroups = Dictionary( uniqueKeysWithValues: gradeGroups.lazy - .map { ($0, !$0.assignments.isEmpty) } + .map { ($0, $0.weightedScore != nil) } ) } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index bde9ee3b..8605c5ed 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -105,19 +105,23 @@ private struct GradeGroupSection: View { group.assignments.move(fromOffsets: $0, toOffset: $1) } - Button("Add Assignment", systemImage: "plus.circle.fill") { - let newAssignment = calculator.createEmptyAssignment(in: group) - assignmentRowFocus = newAssignment - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - .padding(4) + addAssignmentButton } label: { GradeGroupHeader(group: $group, groupRowFocus: _groupRowFocus) } } - var isExpanded: Binding { + private var addAssignmentButton: some View { + Button("Add Assignment", systemImage: "plus.circle.fill") { + let newAssignment = calculator.createEmptyAssignment(in: group) + assignmentRowFocus = newAssignment + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .padding(4) + } + + private var isExpanded: Binding { .init { calculator.expandedAssignmentGroups[group, default: true] } set: { newValue in @@ -139,8 +143,7 @@ private struct GradeGroupHeader: View { HStack { TextField("Assignment Group Name", text: $group.name) - - Spacer() + .fixedSize() TextField( "Weight", @@ -156,6 +159,7 @@ private struct GradeGroupHeader: View { .bold() .padding(4) .focused($groupRowFocus, equals: group) + .foregroundStyle(group.weightedScore == nil ? .secondary : .primary) } } @@ -178,13 +182,7 @@ private struct GradeAssignmentRow: View { value: $assignment.pointsEarned, format: .number ) - .fixedSize() - .font(.title3) - .fontWeight(.semibold) - .foregroundStyle(.tint) - #if os(iOS) - .keyboardType(.numberPad) - #endif + .pointsTextField() Text("/") @@ -193,6 +191,17 @@ private struct GradeAssignmentRow: View { value: $assignment.pointsPossible, format: .number ) + .pointsTextField() + } + .focused($assignmentRowFocus, equals: assignment) + .draggable(assignment) + .padding(4) + } +} + +extension View { + fileprivate func pointsTextField() -> some View { + self .fixedSize() .font(.title3) .fontWeight(.semibold) @@ -200,9 +209,5 @@ private struct GradeAssignmentRow: View { #if os(iOS) .keyboardType(.numberPad) #endif - } - .focused($assignmentRowFocus, equals: assignment) - .draggable(assignment) - .padding(4) } } From 0bbd7594295c92083fca6adbceac836ca9e6d0f1 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Thu, 27 Feb 2025 23:51:29 -0500 Subject: [PATCH 17/21] More refinements --- .../GradeCalculator/GradeCalculatorView.swift | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 8605c5ed..ae71af7d 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -14,8 +14,6 @@ struct GradeCalculatorView: View { @FocusState private var assignmentRowFocus: GradeCalculator.GradeAssignment? @FocusState private var groupRowFocus: GradeCalculator.GradeGroup? - @State private var targetedGroup: GradeCalculator.GradeGroup? - var body: some View { @Bindable var calculator = calculator @@ -26,16 +24,11 @@ struct GradeCalculatorView: View { assignmentRowFocus: _assignmentRowFocus, groupRowFocus: _groupRowFocus ) - .contentShape(.rect) - .dropDestination(for: GradeCalculator.GradeAssignment.self) { assignments, _ in - calculator.moveAssignments(assignments, to: group) - } isTargeted: { - targetedGroup = $0 ? group : nil - } } .onMove { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) } + .listRowSeparator(.hidden) #if os(macOS) Divider() @@ -47,7 +40,9 @@ struct GradeCalculatorView: View { } .buttonStyle(.borderless) .foregroundStyle(.secondary) + .listRowSeparator(.hidden) } + .textFieldStyle(.plain) #if os(macOS) .listStyle(.sidebar) #else @@ -101,14 +96,16 @@ private struct GradeGroupSection: View { ForEach($group.assignments, id: \.id) { $assignment in GradeAssignmentRow(assignment: $assignment, assignmentRowFocus: _assignmentRowFocus) } - .onMove { - group.assignments.move(fromOffsets: $0, toOffset: $1) - } addAssignmentButton } label: { GradeGroupHeader(group: $group, groupRowFocus: _groupRowFocus) } + .disclosureGroupStyle(GradeGroupDisclosureStyle()) + .background(.secondary.opacity(0.1), in: .rect(cornerRadius: 8.0)) + .dropDestination(for: GradeCalculator.GradeAssignment.self) { assignments, _ in + calculator.moveAssignments(assignments, to: group) + } } private var addAssignmentButton: some View { @@ -143,7 +140,9 @@ private struct GradeGroupHeader: View { HStack { TextField("Assignment Group Name", text: $group.name) + #if os(macOS) .fixedSize() + #endif TextField( "Weight", @@ -199,6 +198,31 @@ private struct GradeAssignmentRow: View { } } +private struct GradeGroupDisclosureStyle: DisclosureGroupStyle { + func makeBody(configuration: Configuration) -> some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: "chevron.right") + .rotationEffect(configuration.isExpanded ? .degrees(90) : .degrees(0)) + .onTapGesture { + configuration.isExpanded.toggle() + } + + configuration.label + + Spacer() + } + .padding(.bottom, 4) + + if configuration.isExpanded { + configuration.content + } + } + .frame(maxWidth: .infinity) + .padding(6) + } +} + extension View { fileprivate func pointsTextField() -> some View { self From 077de9239971d8e0ec1272dc9af389da50d4c3b5 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Fri, 28 Feb 2025 10:05:54 -0500 Subject: [PATCH 18/21] Fixes --- .../GradeCalculator/GradeCalculatorView.swift | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index ae71af7d..7c1817ba 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -96,13 +96,14 @@ private struct GradeGroupSection: View { ForEach($group.assignments, id: \.id) { $assignment in GradeAssignmentRow(assignment: $assignment, assignmentRowFocus: _assignmentRowFocus) } + .onMove { + group.assignments.move(fromOffsets: $0, toOffset: $1) + } addAssignmentButton } label: { GradeGroupHeader(group: $group, groupRowFocus: _groupRowFocus) } - .disclosureGroupStyle(GradeGroupDisclosureStyle()) - .background(.secondary.opacity(0.1), in: .rect(cornerRadius: 8.0)) .dropDestination(for: GradeCalculator.GradeAssignment.self) { assignments, _ in calculator.moveAssignments(assignments, to: group) } @@ -198,31 +199,6 @@ private struct GradeAssignmentRow: View { } } -private struct GradeGroupDisclosureStyle: DisclosureGroupStyle { - func makeBody(configuration: Configuration) -> some View { - VStack(alignment: .leading) { - HStack { - Image(systemName: "chevron.right") - .rotationEffect(configuration.isExpanded ? .degrees(90) : .degrees(0)) - .onTapGesture { - configuration.isExpanded.toggle() - } - - configuration.label - - Spacer() - } - .padding(.bottom, 4) - - if configuration.isExpanded { - configuration.content - } - } - .frame(maxWidth: .infinity) - .padding(6) - } -} - extension View { fileprivate func pointsTextField() -> some View { self From 2f2a70a9e8468ffb37827db177eaf0f1ee355871 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sat, 1 Mar 2025 13:30:27 -0500 Subject: [PATCH 19/21] UI --- .../project.pbxproj | 4 + .../Contents.json | 12 ++ .../custom.function.capsule.svg | 113 ++++++++++++++++++ .../Common/Utilities/AccessoryBar.swift | 36 ++++++ .../Assignments/CourseAssignmentsView.swift | 5 +- .../Features/Grades/CourseGradeView.swift | 22 +--- .../GradeCalculator/GradeCalculatorView.swift | 49 ++++---- 7 files changed, 198 insertions(+), 43 deletions(-) create mode 100644 CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/Contents.json create mode 100644 CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/custom.function.capsule.svg create mode 100644 CanvasPlusPlayground/Common/Utilities/AccessoryBar.swift diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index 2da5b9f4..a6228e54 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ B76455082C8DF61B002DF00E /* CourseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76454FC2C8DF61B002DF00E /* CourseManager.swift */; }; B76455092C8DF61B002DF00E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B76454F22C8DF61B002DF00E /* Preview Assets.xcassets */; }; B764550A2C8DF61B002DF00E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B76454F92C8DF61B002DF00E /* Assets.xcassets */; }; + B7725D652D7387D600D64ED8 /* AccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7725D642D7387D300D64ED8 /* AccessoryBar.swift */; }; B77FD0022D52A0D60049AA5E /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77FD0012D52A0D20049AA5E /* ProfileView.swift */; }; B77FD0072D5309340049AA5E /* ProfilePicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77FD0062D5309340049AA5E /* ProfilePicture.swift */; }; B785BE652CF592710094BF94 /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = B785BE642CF592710094BF94 /* LLM */; }; @@ -296,6 +297,7 @@ B76454FA2C8DF61B002DF00E /* CanvasPlusPlaygroundApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasPlusPlaygroundApp.swift; sourceTree = ""; }; B76454FB2C8DF61B002DF00E /* CourseFileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseFileViewModel.swift; sourceTree = ""; }; B76454FC2C8DF61B002DF00E /* CourseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseManager.swift; sourceTree = ""; }; + B7725D642D7387D300D64ED8 /* AccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryBar.swift; sourceTree = ""; }; B77FD0012D52A0D20049AA5E /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; B77FD0062D5309340049AA5E /* ProfilePicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicture.swift; sourceTree = ""; }; B7926D152CE95CE900BFFBE1 /* RGBColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RGBColors.swift; sourceTree = ""; }; @@ -468,6 +470,7 @@ A324BA552D0796BE005F53FA /* Utilities */ = { isa = PBXGroup; children = ( + B7725D642D7387D300D64ED8 /* AccessoryBar.swift */, A373DC0E2D19F71600215019 /* TypeSafeCodable.swift */, A3CF88D22D18E6BB000ACDF3 /* ParentKeyPath.swift */, B7F950312D118EAA004BB470 /* String+StripHTML.swift */, @@ -954,6 +957,7 @@ 9B69FA4F2CC2CCCF006101F3 /* AggregatedAssignmentsListCell.swift in Sources */, B7F950372D127869004BB470 /* ProfileManager.swift in Sources */, A324BA662D07AFD5005F53FA /* FileType.swift in Sources */, + B7725D652D7387D600D64ED8 /* AccessoryBar.swift in Sources */, 192EC0482C963ACB00AF8528 /* CourseAssignmentManager.swift in Sources */, A373DC232D1D454000215019 /* APIGradingSchemeEntry.swift in Sources */, A301EE0D2D612E7100D71139 /* DiscussionTopicAPI.swift in Sources */, diff --git a/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/Contents.json b/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/Contents.json new file mode 100644 index 00000000..a2ce47ff --- /dev/null +++ b/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "custom.function.capsule.svg", + "idiom" : "universal" + } + ] +} diff --git a/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/custom.function.capsule.svg b/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/custom.function.capsule.svg new file mode 100644 index 00000000..8cb8bd41 --- /dev/null +++ b/CanvasPlusPlayground/Assets.xcassets/custom.function.capsule.symbolset/custom.function.capsule.svg @@ -0,0 +1,113 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.6.0 + Requires Xcode 16 or greater + Generated from capsule + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CanvasPlusPlayground/Common/Utilities/AccessoryBar.swift b/CanvasPlusPlayground/Common/Utilities/AccessoryBar.swift new file mode 100644 index 00000000..c913336f --- /dev/null +++ b/CanvasPlusPlayground/Common/Utilities/AccessoryBar.swift @@ -0,0 +1,36 @@ +// +// AccessoryBar.swift +// CanvasPlusPlayground +// +// Created by Rahul on 3/1/25. +// + +import SwiftUI + +struct AccessoryBar: View { + let title: String + let value: String + + var body: some View { + VStack { + Divider() + + HStack { + Text(title) + Spacer() + Text(value) + .contentTransition(.numericText()) + .foregroundStyle(.tint) + } + .fontDesign(.rounded) + .font(.title2) + .bold() + .padding(.horizontal) + .padding(.vertical, 4) + + Divider() + } + .frame(maxWidth: .infinity) + .background(.bar) + } +} diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index 79188bb1..fbae23b9 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -79,9 +79,12 @@ struct CourseAssignmentsView: View { .toolbar { if showGrades { ToolbarItem(placement: .automatic) { - Button("Calculate Grades") { + Button("Calculate Grades", image: .customFunctionCapsule) { showingGradeCalculator = true } + #if os(macOS) + .labelStyle(.titleAndIcon) + #endif .disabled(isLoadingAssignments) } } diff --git a/CanvasPlusPlayground/Features/Grades/CourseGradeView.swift b/CanvasPlusPlayground/Features/Grades/CourseGradeView.swift index 17877780..9bbdbe6c 100644 --- a/CanvasPlusPlayground/Features/Grades/CourseGradeView.swift +++ b/CanvasPlusPlayground/Features/Grades/CourseGradeView.swift @@ -38,27 +38,7 @@ struct CourseGradeView: View { } private var gradesAccessoryBar: some View { - VStack { - Divider() - - HStack { - Text("Current Score") - Spacer() - Text(gradesVM.currentScore) - .animation(.default, value: gradesVM.currentScore) - .contentTransition(.numericText()) - .foregroundStyle(.tint) - } - .fontDesign(.rounded) - .font(.title2) - .bold() - .padding(.horizontal) - .padding(.vertical, 4) - - Divider() - } - .frame(maxWidth: .infinity) - .background(.bar) + AccessoryBar(title: "Current Score", value: gradesVM.currentScore) } private func loadGrades() async { diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 7c1817ba..d9c9561e 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -28,11 +28,6 @@ struct GradeCalculatorView: View { .onMove { calculator.gradeGroups.move(fromOffsets: $0, toOffset: $1) } - .listRowSeparator(.hidden) - - #if os(macOS) - Divider() - #endif Button("Add Assignment Group", systemImage: "plus.circle.fill") { let newGroup = calculator.createEmptyGroup() @@ -40,24 +35,21 @@ struct GradeCalculatorView: View { } .buttonStyle(.borderless) .foregroundStyle(.secondary) - .listRowSeparator(.hidden) } .textFieldStyle(.plain) - #if os(macOS) - .listStyle(.sidebar) - #else - .listStyle(.inset) - #endif - .scrollContentBackground(.hidden) - .background(.background) .navigationTitle("Calculate Grades") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .safeAreaInset(edge: .top) { + totalGradeView + } + #endif .toolbar { + #if os(macOS) ToolbarItem(placement: .destructiveAction) { - Text("Total: \(calculator.totalGrade.truncatingTrailingZeros)%") - .bold() - .contentTransition(.numericText()) - .animation(.default, value: calculator.totalGrade) + totalGradeView } + #endif ToolbarItem(placement: .cancellationAction) { Button { @@ -83,6 +75,18 @@ struct GradeCalculatorView: View { } } } + + private var totalGradeView: some View { + #if os(iOS) + AccessoryBar(title: "Total", value: "\(calculator.totalGrade.truncatingTrailingZeros)%") + .animation(.default, value: calculator.totalGrade) + #else + Text("Total: \(calculator.totalGrade.truncatingTrailingZeros)%") + .bold() + .contentTransition(.numericText()) + .animation(.default, value: calculator.totalGrade) + #endif + } } private struct GradeGroupSection: View { @@ -180,7 +184,8 @@ private struct GradeAssignmentRow: View { TextField( "Score", value: $assignment.pointsEarned, - format: .number + format: .number, + prompt: Text("-") ) .pointsTextField() @@ -189,7 +194,8 @@ private struct GradeAssignmentRow: View { TextField( "Total", value: $assignment.pointsPossible, - format: .number + format: .number, + prompt: Text("-") ) .pointsTextField() } @@ -199,10 +205,11 @@ private struct GradeAssignmentRow: View { } } -extension View { +extension TextField { fileprivate func pointsTextField() -> some View { self - .fixedSize() + .frame(width: 30) + .multilineTextAlignment(.trailing) .font(.title3) .fontWeight(.semibold) .foregroundStyle(.tint) From 6ab0082a5726e8be216e1ee4d9e40d40554bd0a9 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sun, 2 Mar 2025 12:46:56 -0500 Subject: [PATCH 20/21] Ethan fixes --- .../Assignments/CourseAssignmentsView.swift | 5 +++-- .../Grades/GradeCalculator/GradeCalculator.swift | 8 +++++--- .../GradeCalculator/GradeCalculatorView.swift | 16 ++++++++++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift index fbae23b9..92237a81 100644 --- a/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift +++ b/CanvasPlusPlayground/Features/Assignments/CourseAssignmentsView.swift @@ -102,12 +102,13 @@ struct CourseAssignmentsView: View { } .sheet(isPresented: $showingGradeCalculator) { NavigationStack { - GradeCalculatorView() + GradeCalculatorView( + assignmentGroups: assignmentManager.assignmentGroups + ) } #if os(macOS) .frame(width: 550, height: 650) #endif - .environment(gradeCalculator) } .environment(gradeCalculator) } diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift index e1f1a6dd..1bb5ae9b 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculator.swift @@ -115,7 +115,7 @@ class GradeCalculator { func createEmptyGroup() -> GradeGroup { let newGroup = GradeGroup( id: UUID().uuidString, - name: "New Group", + name: "", weight: 0.0, assignments: [] ) @@ -199,8 +199,10 @@ class GradeCalculator { if totalWeight > 0 { var usedWeightage = 0.0 - let weightedTotal = gradeGroups.reduce(0.0) {sum, group in - guard let weightedScore = group.weightedScore else { return sum } + let weightedTotal = gradeGroups.reduce(0.0) { sum, group in + guard let weightedScore = group.weightedScore else { + return sum + } usedWeightage += group.weight diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index d9c9561e..8f5e66d9 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -8,12 +8,19 @@ import SwiftUI struct GradeCalculatorView: View { - @Environment(GradeCalculator.self) private var calculator @Environment(\.dismiss) private var dismiss + @State private var calculator: GradeCalculator + @FocusState private var assignmentRowFocus: GradeCalculator.GradeAssignment? @FocusState private var groupRowFocus: GradeCalculator.GradeGroup? + init(assignmentGroups: [AssignmentGroup]) { + _calculator = .init( + initialValue: .init(assignmentGroups: assignmentGroups) + ) + } + var body: some View { @Bindable var calculator = calculator @@ -70,10 +77,12 @@ struct GradeCalculatorView: View { Spacer() Button("Done") { assignmentRowFocus = nil + groupRowFocus = nil } .bold() } } + .environment(calculator) } private var totalGradeView: some View { @@ -103,6 +112,9 @@ private struct GradeGroupSection: View { .onMove { group.assignments.move(fromOffsets: $0, toOffset: $1) } + .onDelete { + group.assignments.remove(atOffsets: $0) + } addAssignmentButton } label: { @@ -208,7 +220,7 @@ private struct GradeAssignmentRow: View { extension TextField { fileprivate func pointsTextField() -> some View { self - .frame(width: 30) + .fixedSize() .multilineTextAlignment(.trailing) .font(.title3) .fontWeight(.semibold) From ff79be2e548cfb1d76df78333f9797a0e2717f55 Mon Sep 17 00:00:00 2001 From: Rahul Narayanan Date: Sun, 2 Mar 2025 17:53:05 -0500 Subject: [PATCH 21/21] Max's Fixes --- .../GradeCalculator/GradeCalculatorView.swift | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift index 8f5e66d9..78f1de40 100644 --- a/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift +++ b/CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift @@ -43,7 +43,6 @@ struct GradeCalculatorView: View { .buttonStyle(.borderless) .foregroundStyle(.secondary) } - .textFieldStyle(.plain) .navigationTitle("Calculate Grades") #if os(iOS) .navigationBarTitleDisplayMode(.inline) @@ -150,9 +149,9 @@ private struct GradeGroupHeader: View { var body: some View { let formattedWeightBinding: Binding = .init { - group.weight / 100.0 + group.weight } set: { - group.weight = $0 * 100.0 + group.weight = $0 } HStack { @@ -161,16 +160,20 @@ private struct GradeGroupHeader: View { .fixedSize() #endif - TextField( - "Weight", - value: formattedWeightBinding, - format: .percent - ) - .fixedSize() + HStack(spacing: 0) { + TextField( + "--", + value: formattedWeightBinding, + format: .number + ) + .fixedSize() + #if os(iOS) + .keyboardType(.numberPad) + #endif + + Text("%") + } .foregroundStyle(.tint) - #if os(iOS) - .keyboardType(.numberPad) - #endif } .bold() .padding(4) @@ -197,7 +200,7 @@ private struct GradeAssignmentRow: View { "Score", value: $assignment.pointsEarned, format: .number, - prompt: Text("-") + prompt: Text("--") ) .pointsTextField() @@ -207,7 +210,7 @@ private struct GradeAssignmentRow: View { "Total", value: $assignment.pointsPossible, format: .number, - prompt: Text("-") + prompt: Text("--") ) .pointsTextField() }