From 45ed6870962dd1c3bb97d1a36bb5706593e0b52e Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 31 May 2024 16:12:27 +0300 Subject: [PATCH 01/11] feat: course home navigation --- Core/Core.xcodeproj/project.pbxproj | 4 - .../deleteDownloading.imageset/Contents.json | 2 +- .../deleteDownloading.imageset/Frame-17.svg | 13 - .../deleteDownloading.svg | 3 + .../startDownloading.imageset/Contents.json | 5 +- .../startDownloading.imageset/Frame-16.svg | 12 - .../startDownloading.svg | 3 + .../chevron_right.imageset/Contents.json | 15 + .../chevron_right.imageset/chevron_right.svg | 15 + .../finished_sequence.imageset/Contents.json | 12 + .../finished_sequence.svg | 3 + .../Config/UIComponentsConfig.swift | 6 +- Core/Core/Domain/Model/CourseBlockModel.swift | 38 +- Core/Core/Extensions/DateExtension.swift | 14 + Core/Core/Extensions/Notification.swift | 1 + Core/Core/SwiftGen/Assets.swift | 2 + Core/Core/SwiftGen/Strings.swift | 6 + .../View/Base/CustomDisclosureGroup.swift | 49 --- Core/Core/View/Base/DownloadView.swift | 6 +- Core/Core/en.lproj/Localizable.strings | 4 + Course/Course.xcodeproj/project.pbxproj | 38 +- Course/Course/Data/CourseRepository.swift | 30 +- .../Model/Data_CourseOutlineResponse.swift | 52 ++- .../Course/Data/Network/CourseEndpoint.swift | 2 +- .../CourseCoreModel.xcdatamodel/contents | 10 +- Course/Course/Domain/CourseInteractor.swift | 11 +- .../Container/CourseContainerViewModel.swift | 12 +- .../Outline/ContinueWithView.swift | 2 + .../Outline/CourseOutlineView.swift | 39 +- .../CourseStructureNestedListView.swift | 237 ----------- .../CourseStructure/CourseStructureView.swift | 134 ------- .../CourseVerticalImageView.swift | 7 +- .../CourseVertical/CourseVerticalView.swift | 9 +- .../Subviews/CourseProgressView.swift | 55 +++ .../Subviews/CustomDisclosureGroup.swift | 377 ++++++++++++++++++ .../Presentation/Unit/CourseUnitView.swift | 23 +- .../DropdownList/CourseUnitDropDownCell.swift | 1 + .../DropdownList/CourseUnitDropDownList.swift | 4 + .../CourseUnitVerticalsDropdownView.swift | 4 + Course/Course/SwiftGen/Strings.swift | 18 + Course/Course/en.lproj/Localizable.strings | 5 + .../Course/en.lproj/Localizable.stringsdict | 42 ++ Course/Course/uk.lproj/Localizable.strings | 5 + .../Course/uk.lproj/Localizable.stringsdict | 42 ++ .../CourseContainerViewModelTests.swift | 70 +++- .../Unit/CourseDateViewModelTests.swift | 3 +- .../Unit/CourseUnitViewModelTests.swift | 16 +- OpenEdX/DI/AppAssembly.swift | 3 +- OpenEdX/Data/CoursePersistence.swift | 28 +- OpenEdX/Managers/PipManager.swift | 7 +- OpenEdX/Router.swift | 7 +- 51 files changed, 959 insertions(+), 547 deletions(-) delete mode 100644 Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg create mode 100644 Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg delete mode 100644 Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg create mode 100644 Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg create mode 100644 Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg create mode 100644 Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg delete mode 100644 Core/Core/View/Base/CustomDisclosureGroup.swift delete mode 100644 Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift delete mode 100644 Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift create mode 100644 Course/Course/Presentation/Subviews/CourseProgressView.swift create mode 100644 Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift create mode 100644 Course/Course/en.lproj/Localizable.stringsdict create mode 100644 Course/Course/uk.lproj/Localizable.stringsdict diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index f718ae857..df8f80f4b 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -157,7 +157,6 @@ BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */; }; BA981BCE2B8F5C49005707C2 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA981BCD2B8F5C49005707C2 /* Sequence+Extensions.swift */; }; BA981BD02B91ED50005707C2 /* FullScreenProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */; }; - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; }; BAD9CA2F2B289B3500DE790A /* ajaxHandler.js in Resources */ = {isa = PBXBuildFile; fileRef = BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */; }; BAD9CA332B28A8F300DE790A /* AjaxProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */; }; BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */; }; @@ -347,7 +346,6 @@ BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; BA981BCD2B8F5C49005707C2 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = ""; }; BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenProgressView.swift; sourceTree = ""; }; - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ajaxHandler.js; sourceTree = ""; }; BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AjaxProvider.swift; sourceTree = ""; }; BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfigTests.swift; sourceTree = ""; }; @@ -731,7 +729,6 @@ 023A1137291432FD00D0D354 /* FieldConfiguration.swift */, BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */, BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */, - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */, BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, 02E93F862AEBAED4006C4750 /* AppReview */, BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */, @@ -1191,7 +1188,6 @@ 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */, 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */, 064987982B4D69FF0071642A /* CSSInjectionProtocol.swift in Sources */, - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */, BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */, 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, 023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */, diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json index d1927cffc..41ea480fe 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Frame-17.svg", + "filename" : "deleteDownloading.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg deleted file mode 100644 index 0ae948676..000000000 --- a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg new file mode 100644 index 000000000..ed2659aab --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json index 866687bad..0c0e4a7c2 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Frame-16.svg", + "filename" : "startDownloading.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg deleted file mode 100644 index 24d291489..000000000 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg new file mode 100644 index 000000000..8a29b74a2 --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json new file mode 100644 index 000000000..a21ea6e5d --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chevron_right.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg new file mode 100644 index 000000000..e951c4282 --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json b/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json new file mode 100644 index 000000000..9d0a51bf3 --- /dev/null +++ b/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "finished_sequence.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg b/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg new file mode 100644 index 000000000..51ed61934 --- /dev/null +++ b/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Configuration/Config/UIComponentsConfig.swift b/Core/Core/Configuration/Config/UIComponentsConfig.swift index cb5ce3e68..1b8cf788e 100644 --- a/Core/Core/Configuration/Config/UIComponentsConfig.swift +++ b/Core/Core/Configuration/Config/UIComponentsConfig.swift @@ -8,16 +8,16 @@ import Foundation private enum Keys: String, RawStringExtractable { - case courseNestedListEnabled = "COURSE_NESTED_LIST_ENABLED" + case courseDropDownNavigationEnabled = "COURSE_DROPDOWN_NAVIGATION_ENABLED" case courseUnitProgressEnabled = "COURSE_UNIT_PROGRESS_ENABLED" } public class UIComponentsConfig: NSObject { - public var courseNestedListEnabled: Bool + public var courseDropDownNavigationEnabled: Bool public var courseUnitProgressEnabled: Bool init(dictionary: [String: Any]) { - courseNestedListEnabled = dictionary[Keys.courseNestedListEnabled] as? Bool ?? false + courseDropDownNavigationEnabled = dictionary[Keys.courseDropDownNavigationEnabled] as? Bool ?? false courseUnitProgressEnabled = dictionary[Keys.courseUnitProgressEnabled] as? Bool ?? false super.init() } diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index 406bea3ed..96ef3ccde 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -24,6 +24,7 @@ public struct CourseStructure: Equatable { public let certificate: Certificate? public let org: String public let isSelfPaced: Bool + public let courseProgress: CourseProgress? public init( id: String, @@ -37,7 +38,8 @@ public struct CourseStructure: Equatable { media: DataLayer.CourseMedia, certificate: Certificate?, org: String, - isSelfPaced: Bool + isSelfPaced: Bool, + courseProgress: CourseProgress? ) { self.id = id self.graded = graded @@ -51,6 +53,7 @@ public struct CourseStructure: Equatable { self.certificate = certificate self.org = org self.isSelfPaced = isSelfPaced + self.courseProgress = courseProgress } public func totalVideosSizeInBytes(downloadQuality: DownloadQuality) -> Int { @@ -78,6 +81,16 @@ public struct CourseStructure: Equatable { } } +public struct CourseProgress { + public let totalAssignmentsCount: Int? + public let assignmentsCompleted: Int? + + public init(totalAssignmentsCount: Int, assignmentsCompleted: Int) { + self.totalAssignmentsCount = totalAssignmentsCount + self.assignmentsCompleted = assignmentsCompleted + } +} + public struct CourseChapter: Identifiable { public let blockId: String @@ -109,6 +122,8 @@ public struct CourseSequential: Identifiable { public let type: BlockType public let completion: Double public var childs: [CourseVertical] + public let sequentialProgress: SequentialProgress? + public let due: Date? public var isDownloadable: Bool { return childs.first(where: { $0.isDownloadable }) != nil @@ -120,7 +135,9 @@ public struct CourseSequential: Identifiable { displayName: String, type: BlockType, completion: Double, - childs: [CourseVertical] + childs: [CourseVertical], + sequentialProgress: SequentialProgress?, + due: Date? ) { self.blockId = blockId self.id = id @@ -128,6 +145,8 @@ public struct CourseSequential: Identifiable { self.type = type self.completion = completion self.childs = childs + self.sequentialProgress = sequentialProgress + self.due = due } } @@ -177,6 +196,18 @@ public struct SubtitleUrl: Equatable { } } +public struct SequentialProgress { + public let assignmentType: String? + public let numPointsEarned: Int? + public let numPointsPossible: Int? + + public init(assignmentType: String?, numPointsEarned: Int?, numPointsPossible: Int?) { + self.assignmentType = assignmentType + self.numPointsEarned = numPointsEarned + self.numPointsPossible = numPointsPossible + } +} + public struct CourseBlock: Hashable, Identifiable { public static func == (lhs: CourseBlock, rhs: CourseBlock) -> Bool { lhs.id == rhs.id && @@ -193,6 +224,7 @@ public struct CourseBlock: Hashable, Identifiable { public let courseId: String public let topicId: String? public let graded: Bool + public let due: Date? public var completion: Double public let type: BlockType public let displayName: String @@ -212,6 +244,7 @@ public struct CourseBlock: Hashable, Identifiable { courseId: String, topicId: String? = nil, graded: Bool, + due: Date?, completion: Double, type: BlockType, displayName: String, @@ -226,6 +259,7 @@ public struct CourseBlock: Hashable, Identifiable { self.courseId = courseId self.topicId = topicId self.graded = graded + self.due = due self.completion = completion self.type = type self.displayName = displayName diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index bbdb6834b..7be0c84ec 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -75,6 +75,8 @@ public extension Date { } public enum DateStringStyle { + case courseStartsMonthDDYear + case courseEndsMonthDDYear case startDDMonthYear case endedMonthDay case mmddyy @@ -103,6 +105,10 @@ public extension Date { dateFormatter.locale = Locale(identifier: "en_US_POSIX") switch style { + case .courseStartsMonthDDYear: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy + case .courseEndsMonthDDYear: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy case .endedMonthDay: dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd case .mmddyy: @@ -122,6 +128,14 @@ public extension Date { let date = dateFormatter.string(from: self) switch style { + case .courseStartsMonthDDYear: + return CoreLocalization.Date.courseStarts + " " + date + case .courseEndsMonthDDYear: + if Date() < self { + return CoreLocalization.Date.courseEnds + " " + date + } else { + return CoreLocalization.Date.courseEnded + " " + date + } case .endedMonthDay: return CoreLocalization.Date.ended + " " + date case .mmddyy, .monthYear: diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index 9f792fb2a..d8e99731b 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -9,6 +9,7 @@ import Foundation public extension Notification.Name { static let onCourseEnrolled = Notification.Name("onCourseEnrolled") + static let onblockCompletionRequested = Notification.Name("onblockCompletionRequested") static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed") static let onActualVersionReceived = Notification.Name("onActualVersionReceived") static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped") diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index a6cb7b057..d878fbcec 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -97,9 +97,11 @@ public enum CoreAssets { public static let certificateBadge = ImageAsset(name: "certificateBadge") public static let check = ImageAsset(name: "check") public static let checkEmail = ImageAsset(name: "checkEmail") + public static let chevronRight = ImageAsset(name: "chevron_right") public static let clearInput = ImageAsset(name: "clearInput") public static let edit = ImageAsset(name: "edit") public static let favorite = ImageAsset(name: "favorite") + public static let finishedSequence = ImageAsset(name: "finished_sequence") public static let goodWork = ImageAsset(name: "goodWork") public static let airmail = ImageAsset(name: "airmail") public static let defaultMail = ImageAsset(name: "defaultMail") diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index a5782d497..50f2b1755 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -79,6 +79,12 @@ public enum CoreLocalization { } } public enum Date { + /// Course Ended + public static let courseEnded = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDED", fallback: "Course Ended") + /// Course Ends + public static let courseEnds = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDS", fallback: "Course Ends") + /// Course Starts + public static let courseStarts = CoreLocalization.tr("Localizable", "DATE.COURSE_STARTS", fallback: "Course Starts") /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") /// Just now diff --git a/Core/Core/View/Base/CustomDisclosureGroup.swift b/Core/Core/View/Base/CustomDisclosureGroup.swift deleted file mode 100644 index c4a023ed3..000000000 --- a/Core/Core/View/Base/CustomDisclosureGroup.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CustomDisclosureGroup.swift -// Core -// -// Created by Eugene Yatsenko on 09.11.2023. -// - -import SwiftUI - -public struct CustomDisclosureGroup: View { - - @Binding var isExpanded: Bool - - private var onClick: () -> Void - private var animation: Animation? - private let header: Header - private let content: Content - - public init( - animation: Animation?, - isExpanded: Binding, - onClick: @escaping () -> Void, - header: (_ isExpanded: Bool) -> Header, - content: () -> Content - ) { - self.onClick = onClick - self._isExpanded = isExpanded - self.animation = animation - self.header = header(isExpanded.wrappedValue) - self.content = content() - } - - public var body: some View { - VStack(spacing: 0) { - Button { - withAnimation(animation) { - onClick() - } - } label: { - header - .contentShape(Rectangle()) - } - if isExpanded { - content - } - } - .clipped() - } -} diff --git a/Core/Core/View/Base/DownloadView.swift b/Core/Core/View/Base/DownloadView.swift index 37f63e41d..1329d14e0 100644 --- a/Core/Core/View/Base/DownloadView.swift +++ b/Core/Core/View/Base/DownloadView.swift @@ -24,7 +24,7 @@ public struct DownloadAvailableView: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(Theme.Colors.accentColor) } .frame(width: 30, height: 30) } @@ -41,6 +41,7 @@ public struct DownloadProgressView: View { .resizable() .scaledToFit() .frame(width: 20, height: 20) + .foregroundStyle(Theme.Colors.snackbarErrorColor) .foregroundColor(Theme.Colors.textPrimary) } } @@ -52,11 +53,10 @@ public struct DownloadFinishedView: View { public var body: some View { VStack(spacing: 0) { - CoreAssets.deleteDownloading.swiftUIImage.renderingMode(.template) + CoreAssets.deleteDownloading.swiftUIImage .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) } .frame(width: 30, height: 30) } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 27196487a..5ba0d8c79 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -44,6 +44,10 @@ "ERROR.RELOAD" = "Reload"; +"DATE.COURSE_STARTS" = "Course Starts"; +"DATE.COURSE_ENDS" = "Course Ends"; +"DATE.COURSE_ENDED" = "Course Ended"; + "DATE.ENDED" = "Ended"; "DATE.START" = "Start"; "DATE.STARTED" = "Started"; diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 6ecf45e64..ba6899bde 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -45,7 +45,10 @@ 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */; }; 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */; }; 02B6B3C928E1E68100232911 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02B6B3C828E1E68100232911 /* Core.framework */; }; + 02BB20182BFCE7B200364948 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */; }; + 02C355392C08DCD700501342 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 02C355372C08DCD700501342 /* Localizable.stringsdict */; }; 02D4FC2E2BBD7C9C00C47748 /* MessageSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D4FC2D2BBD7C9C00C47748 /* MessageSectionView.swift */; }; + 02E3803E2BFF9F0A00815AFA /* CourseProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */; }; 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0144E28F46474002E513D /* CourseContainerView.swift */; }; 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */; }; 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F066E729DC71750073E13B /* SubtittlesView.swift */; }; @@ -78,12 +81,10 @@ BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF5C2B3D804D005B102E /* CourseStorage.swift */; }; BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */; }; BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */; }; - BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */; }; BAC0E0D82B32EF03006B68A9 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */; }; BAC0E0DB2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */; }; BAC0E0DE2B32F0F3006B68A9 /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */; }; BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */; }; - BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */; }; BAD9CA4A2B2C88E000DE790A /* CourseVideoDownloadBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */; }; DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */; }; DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */; }; @@ -143,7 +144,11 @@ 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEndpoint.swift; sourceTree = ""; }; 02B6B3C228E1DCD100232911 /* CourseDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetails.swift; sourceTree = ""; }; 02B6B3C828E1E68100232911 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; + 02C355382C08DCD700501342 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 02C3553A2C08DCE000501342 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 02D4FC2D2BBD7C9C00C47748 /* MessageSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSectionView.swift; sourceTree = ""; }; + 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseProgressView.swift; sourceTree = ""; }; 02ED50CF29A64BB6008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F0144E28F46474002E513D /* CourseContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerView.swift; sourceTree = ""; }; 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModel.swift; sourceTree = ""; }; @@ -191,12 +196,10 @@ BA58CF5C2B3D804D005B102E /* CourseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStorage.swift; sourceTree = ""; }; BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityBarView.swift; sourceTree = ""; }; BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityContainerView.swift; sourceTree = ""; }; - BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureNestedListView.swift; sourceTree = ""; }; BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarViewModel.swift; sourceTree = ""; }; BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = ""; }; BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonLineProgressView.swift; sourceTree = ""; }; - BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureView.swift; sourceTree = ""; }; BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarView.swift; sourceTree = ""; }; DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateViewModelTests.swift; sourceTree = ""; }; DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDatesView.swift; sourceTree = ""; }; @@ -301,6 +304,7 @@ 97EA4D822B84EFA900663F58 /* Managers */, 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, + 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); path = Course; sourceTree = ""; @@ -428,7 +432,6 @@ isa = PBXGroup; children = ( BAD9CA462B2C888600DE790A /* CourseVertical */, - BAD9CA472B2C88AA00DE790A /* CourseStructure */, 02635AC62A24F181008062F2 /* ContinueWithView.swift */, 0270210128E736E700F54332 /* CourseOutlineView.swift */, ); @@ -562,15 +565,6 @@ path = CourseVertical; sourceTree = ""; }; - BAD9CA472B2C88AA00DE790A /* CourseStructure */ = { - isa = PBXGroup; - children = ( - BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */, - BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */, - ); - path = CourseStructure; - sourceTree = ""; - }; BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( @@ -578,6 +572,8 @@ BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */, + 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */, + 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */, ); path = Subviews; sourceTree = ""; @@ -728,6 +724,7 @@ files = ( 0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */, 02B6B3B228E1C49400232911 /* Localizable.strings in Resources */, + 02C355392C08DCD700501342 /* Localizable.stringsdict in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -865,13 +862,11 @@ 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */, 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */, 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */, - BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */, 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */, 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */, 073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */, 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */, 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */, - BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */, 02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */, 97E7DF0F2B7C852A00A2A09B /* DatesStatusInfoView.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, @@ -885,6 +880,7 @@ DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */, 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */, 07DE59862BECB868001CBFBC /* CourseAnalytics.swift in Sources */, + 02BB20182BFCE7B200364948 /* CustomDisclosureGroup.swift in Sources */, 022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */, BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */, BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */, @@ -900,6 +896,7 @@ 02FCB2B32BBEB36600373180 /* CourseHeaderView.swift in Sources */, DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */, 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, + 02E3803E2BFF9F0A00815AFA /* CourseProgressView.swift in Sources */, 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */, 97EA4D862B85034D00663F58 /* CalendarManager.swift in Sources */, @@ -927,6 +924,15 @@ name = Localizable.strings; sourceTree = ""; }; + 02C355372C08DCD700501342 /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 02C355382C08DCD700501342 /* en */, + 02C3553A2C08DCE000501342 /* uk */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 19073d2de..612273fe5 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -139,7 +139,11 @@ public class CourseRepository: CourseRepositoryProtocol { media: course.media, certificate: course.certificate?.domain, org: course.org ?? "", - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } @@ -173,7 +177,13 @@ public class CourseRepository: CourseRepositoryProtocol { displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + sequentialProgress: SequentialProgress( + assignmentType: sequential.assignmentProgress?.assignmentType, + numPointsEarned: Int(sequential.assignmentProgress?.numPointsEarned ?? 0), + numPointsPossible: Int(sequential.assignmentProgress?.numPointsPossible ?? 0) + ), + due: sequential.due == nil ? nil : Date(iso8601: sequential.due!) ) } @@ -211,6 +221,7 @@ public class CourseRepository: CourseRepositoryProtocol { courseId: courseId, topicId: block.userViewData?.topicID, graded: block.graded, + due: block.due == nil ? nil : Date(iso8601: block.due!), completion: block.completion ?? 0, type: BlockType(rawValue: block.type) ?? .unknown, displayName: block.displayName, @@ -350,7 +361,11 @@ And there are various ways of describing it-- call it oral poetry or media: course.media, certificate: course.certificate?.domain, org: course.org ?? "", - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } @@ -385,7 +400,13 @@ And there are various ways of describing it-- call it oral poetry or displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + sequentialProgress: SequentialProgress( + assignmentType: sequential.assignmentProgress?.assignmentType, + numPointsEarned: Int(sequential.assignmentProgress?.numPointsEarned ?? 0), + numPointsPossible: Int(sequential.assignmentProgress?.numPointsPossible ?? 0) + ), + due: sequential.due == nil ? nil : Date(iso8601: sequential.due!) ) } @@ -421,6 +442,7 @@ And there are various ways of describing it-- call it oral poetry or courseId: courseId, topicId: block.userViewData?.topicID, graded: block.graded, + due: block.due == nil ? nil : Date(iso8601: block.due!), completion: block.completion ?? 0, type: BlockType(rawValue: block.type) ?? .unknown, displayName: block.displayName, diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index f2a060e13..4259c286e 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -21,6 +21,7 @@ public extension DataLayer { public let certificate: Certificate? public let org: String? public let isSelfPaced: Bool + public let courseProgress: CourseProgress? enum CodingKeys: String, CodingKey { case blocks @@ -30,6 +31,7 @@ public extension DataLayer { case certificate case org case isSelfPaced = "is_self_paced" + case courseProgress = "course_progress" } public init( @@ -39,7 +41,8 @@ public extension DataLayer { media: DataLayer.CourseMedia, certificate: Certificate?, org: String?, - isSelfPaced: Bool + isSelfPaced: Bool, + courseProgress: CourseProgress? ) { self.rootItem = rootItem self.dict = dict @@ -48,6 +51,7 @@ public extension DataLayer { self.certificate = certificate self.org = org self.isSelfPaced = isSelfPaced + self.courseProgress = courseProgress } public init(from decoder: Decoder) throws { @@ -60,6 +64,7 @@ public extension DataLayer { certificate = try values.decode(Certificate.self, forKey: .certificate) org = try values.decode(String.self, forKey: .org) isSelfPaced = try values.decode(Bool.self, forKey: .isSelfPaced) + courseProgress = try values.decode(DataLayer.CourseProgress.self, forKey: .courseProgress) } } } @@ -68,6 +73,7 @@ public extension DataLayer { public let blockId: String public let id: String public let graded: Bool + public let due: String? public let completion: Double? public let studentUrl: String public let webUrl: String @@ -77,11 +83,13 @@ public extension DataLayer { public let allSources: [String]? public let userViewData: CourseDetailUserViewData? public let multiDevice: Bool? + public let assignmentProgress: AssignmentProgress? public init( blockId: String, id: String, graded: Bool, + due: String?, completion: Double?, studentUrl: String, webUrl: String, @@ -90,11 +98,13 @@ public extension DataLayer { descendants: [String]?, allSources: [String]?, userViewData: CourseDetailUserViewData?, - multiDevice: Bool? + multiDevice: Bool?, + assignmentProgress: AssignmentProgress? ) { self.blockId = blockId self.id = id self.graded = graded + self.due = due self.completion = completion self.studentUrl = studentUrl self.webUrl = webUrl @@ -104,10 +114,11 @@ public extension DataLayer { self.allSources = allSources self.userViewData = userViewData self.multiDevice = multiDevice + self.assignmentProgress = assignmentProgress } public enum CodingKeys: String, CodingKey { - case id, type, descendants, graded, completion + case id, type, descendants, graded, completion, due case blockId = "block_id" case studentUrl = "student_view_url" case webUrl = "lms_web_url" @@ -115,9 +126,28 @@ public extension DataLayer { case userViewData = "student_view_data" case allSources = "all_sources" case multiDevice = "student_view_multi_device" + case assignmentProgress = "assignment_progress" } } + + struct AssignmentProgress: Codable { + public let assignmentType: String? + public let numPointsEarned: Double? + public let numPointsPossible: Double? + + public enum CodingKeys: String, CodingKey { + case assignmentType = "assignment_type" + case numPointsEarned = "num_points_earned" + case numPointsPossible = "num_points_possible" + } + public init(assignmentType: String?, numPointsEarned: Double?, numPointsPossible: Double?) { + self.assignmentType = assignmentType + self.numPointsEarned = numPointsEarned + self.numPointsPossible = numPointsPossible + } + } + struct Transcripts: Codable { public let en: String? @@ -202,6 +232,20 @@ public extension DataLayer { case fileSize = "file_size" case streamPriority = "stream_priority" } - + } + + struct CourseProgress: Codable { + public let totalAssignmentsCount: Int? + public let assignmentsCompleted: Int? + + public init(totalAssignmentsCount: Int, assignmentsCompleted: Int) { + self.totalAssignmentsCount = totalAssignmentsCount + self.assignmentsCompleted = assignmentsCompleted + } + + enum CodingKeys: String, CodingKey { + case totalAssignmentsCount = "total_assignments_count" + case assignmentsCompleted = "assignments_completed" + } } } diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseEndpoint.swift index 2abafa14a..6ce7a048a 100644 --- a/Course/Course/Data/Network/CourseEndpoint.swift +++ b/Course/Course/Data/Network/CourseEndpoint.swift @@ -86,7 +86,7 @@ enum CourseEndpoint: EndPointType { "nav_depth": "4", "requested_fields": """ contains_gated_content,show_gated_sections,special_exam_info,graded, - format,student_view_multi_device,due,completion + format,student_view_multi_device,due,completion,assignment_progress """, "block_counts": "video" ] diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents index dda82f3ae..92d3a8165 100644 --- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents +++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents @@ -1,15 +1,19 @@ - + + + + + @@ -77,6 +81,7 @@ + @@ -85,6 +90,7 @@ + @@ -101,4 +107,4 @@ - + \ No newline at end of file diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index 1b01e7a59..f76d5ed2e 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -55,7 +55,11 @@ public class CourseInteractor: CourseInteractorProtocol { media: course.media, certificate: course.certificate, org: course.org, - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } @@ -64,6 +68,7 @@ public class CourseInteractor: CourseInteractorProtocol { } public func blockCompletionRequest(courseID: String, blockID: String) async throws { + NotificationCenter.default.post(name: .onblockCompletionRequested, object: courseID) return try await repository.blockCompletionRequest(courseID: courseID, blockID: blockID) } @@ -127,7 +132,9 @@ public class CourseInteractor: CourseInteractorProtocol { displayName: sequential.displayName, type: sequential.type, completion: sequential.completion, - childs: newChilds + childs: newChilds, + sequentialProgress: sequential.sequentialProgress, + due: sequential.due ) } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index e8ce69707..c6c150bd3 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -17,8 +17,8 @@ public enum CourseTab: Int, CaseIterable, Identifiable { case course case videos - case discussion case dates + case discussion case handounds public var title: String { @@ -67,6 +67,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { @Published var userSettings: UserSettings? @Published var isInternetAvaliable: Bool = true @Published var dueDatesShifted: Bool = false + @Published var updateCourseProgress: Bool = false + + let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) var errorMessage: String? { didSet { @@ -565,6 +568,13 @@ public class CourseContainerViewModel: BaseCourseViewModel { selector: #selector(handleShiftDueDates), name: .shiftCourseDates, object: nil ) + + completionPublisher + .sink { [weak self] _ in + guard let self = self else { return } + updateCourseProgress = true + } + .store(in: &cancellables) } deinit { diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 1c05c575b..45271c4ae 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -83,6 +83,7 @@ struct ContinueWithView_Previews: PreviewProvider { id: "1", courseId: "123", graded: true, + due: Date(), completion: 0, type: .html, displayName: "Continue lesson", @@ -96,6 +97,7 @@ struct ContinueWithView_Previews: PreviewProvider { id: "2", courseId: "123", graded: true, + due: Date(), completion: 0, type: .html, displayName: "Continue lesson", diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 7a72f6fa0..3401f87f1 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -30,6 +30,8 @@ public struct CourseOutlineView: View { @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @State private var expandedChapters: [String: Bool] = [:] + public init( viewModel: CourseContainerViewModel, title: String, @@ -41,7 +43,7 @@ public struct CourseOutlineView: View { dateTabIndex: Int ) { self.title = title - self.viewModel = viewModel//StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID self.isVideo = isVideo self._selection = selection @@ -52,10 +54,8 @@ public struct CourseOutlineView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page name GeometryReader { proxy in VStack(alignment: .center) { - // MARK: - Page Body RefreshableScrollViewCompat(action: { await withTaskGroup(of: Void.self) { group in group.addTask { @@ -95,7 +95,6 @@ public struct CourseOutlineView: View { let sequential = chapter.childs[continueWith.sequentialIndex] let continueUnit = sequential.childs[continueWith.verticalIndex] - // MARK: - ContinueWith button ContinueWithView( data: continueWith, courseContinueUnit: continueUnit @@ -129,21 +128,19 @@ public struct CourseOutlineView: View { ? viewModel.courseVideosStructure : viewModel.courseStructure { - // MARK: - Sections - if viewModel.config.uiComponents.courseNestedListEnabled { - CourseStructureNestedListView( - proxy: proxy, - course: course, - viewModel: viewModel - ) - } else { - CourseStructureView( - proxy: proxy, - course: course, - viewModel: viewModel - ) + if !isVideo, let progress = course.courseProgress { + CourseProgressView(progress: progress) + .padding(.horizontal, 24) + .padding(.top, 16) + .padding(.bottom, 8) } + // MARK: - Sections + CustomDisclosureGroup( + course: course, + proxy: proxy, + viewModel: viewModel + ) } else { if let courseStart = viewModel.courseStart { Text(courseStart > Date() ? CourseLocalization.Outline.courseHasntStarted : "") @@ -211,6 +208,14 @@ public struct CourseOutlineView: View { } } } + .onAppear { + if viewModel.updateCourseProgress { + Task { + await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) + } + viewModel.updateCourseProgress = false + } + } .background( Theme.Colors.background .ignoresSafeArea() diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift deleted file mode 100644 index 6e8ad3927..000000000 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// CourseStructureNestedListView.swift -// Course -// -// Created by Eugene Yatsenko on 09.11.2023. -// - -import SwiftUI -import Core -import Kingfisher -import Theme - -struct CourseStructureNestedListView: View { - - private let proxy: GeometryProxy - private let course: CourseStructure - private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - @State private var isExpandedIds: [String] = [] - - init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { - self.proxy = proxy - self.course = course - self.viewModel = viewModel - } - - var body: some View { - ForEach(course.childs, content: disclosureGroup) - } - - private func disclosureGroup(chapter: CourseChapter) -> some View { - CustomDisclosureGroup( - animation: .easeInOut(duration: 0.2), - isExpanded: .constant(isExpandedIds.contains(where: { $0 == chapter.id })), - onClick: { onHeaderClick(chapter: chapter) }, - header: { isExpanded in header(chapter: chapter, isExpanded: isExpanded) }, - content: { section(chapter: chapter) } - ) - } - - private func header( - chapter: CourseChapter, - isExpanded: Bool - ) -> some View { - HStack { - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - .foregroundColor(Theme.Colors.textPrimary) - Spacer() - Image(systemName: "chevron.down").renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - .dropdownArrowRotationAnimation(value: isExpanded) - } - .padding(.horizontal, 30) - .padding(.vertical, 15) - } - - private func section(chapter: CourseChapter) -> some View { - ForEach(chapter.childs) { sequential in - VStack(spacing: 0) { - sequentialLabel( - sequential: sequential, - chapter: chapter, - isExpanded: false - ) - } - } - } - - @ViewBuilder - private func sequentialLabel( - sequential: CourseSequential, - chapter: CourseChapter, - isExpanded: Bool - ) -> some View { - HStack { - Button { - onLabelClick(sequential: sequential, chapter: chapter) - } label: { - HStack(spacing: 0) { - Group { - if sequential.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - } else { - sequential.type.image - } - Text(sequential.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - } - .foregroundColor(Theme.Colors.textPrimary) - Spacer() - } - } - downloadButton( - sequential: sequential, - chapter: chapter - ) - - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(sequential.displayName) - .padding(.leading, 40) - .padding(.trailing, 28) - .padding(.vertical, 14) - } - - @ViewBuilder - private func downloadButton( - sequential: CourseSequential, - chapter: CourseChapter - ) -> some View { - if let state = viewModel.sequentialsDownloadState[sequential.id] { - switch state { - case .available: - if viewModel.isInternetAvaliable { - Button { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: sequential.id, - state: state - ) - } - } label: { - DownloadAvailableView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.download) - } - } - case .downloading: - if viewModel.isInternetAvaliable { - Button { - viewModel.router.showDownloads( - downloads: viewModel.getTasks(sequential: sequential), - manager: viewModel.manager - ) - } label: { - ProgressBar(size: 30, lineWidth: 1.75) - } - } - case .finished: - Button { - viewModel.router.presentAlert( - alertTitle: "Warning", - alertMessage: "\(CourseLocalization.Alert.deleteVideos) \"\(sequential.displayName)\"?", - positiveAction: CoreLocalization.Alert.delete, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: sequential.id, - state: state - ) - } - viewModel.router.dismiss(animated: true) - }, - type: .deleteVideo - ) - } label: { - DownloadFinishedView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) - } - } - downloadCount(sequential: sequential) - } - } - - @ViewBuilder - private func downloadCount(sequential: CourseSequential) -> some View { - let downloadable = viewModel.verticalsBlocksDownloadable(by: sequential) - if !downloadable.isEmpty { - Text(String(downloadable.count)) - .foregroundColor(Color(UIColor.label)) - } - } - - private func onHeaderClick(chapter: CourseChapter) { - if let index = isExpandedIds.firstIndex(where: {$0 == chapter.id}) { - isExpandedIds.remove(at: index) - } else { - isExpandedIds.append(chapter.id) - } - } - - private func onLabelClick( - sequential: CourseSequential, - chapter: CourseChapter - ) { - guard let chapterIndex = course.childs.firstIndex( - where: { $0.id == chapter.id } - ) else { - return - } - - guard let sequentialIndex = chapter.childs.firstIndex( - where: { $0.id == sequential.id } - ) else { - return - } - - guard let courseVertical = sequential.childs.first else { - return - } - - guard let block = courseVertical.childs.first else { - return - } - - viewModel.trackVerticalClicked( - courseId: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - vertical: courseVertical - ) - viewModel.router.showCourseUnit( - courseName: viewModel.courseStructure?.displayName ?? "", - blockId: block.id, - courseID: viewModel.courseStructure?.id ?? "", - verticalIndex: 0, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - - } - -} diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift deleted file mode 100644 index e2dd8646d..000000000 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// CourseStructureView.swift -// Course -// -// Created by Eugene Yatsenko on 15.12.2023. -// - -import SwiftUI -import Core -import Theme - -struct CourseStructureView: View { - - private let proxy: GeometryProxy - private let course: CourseStructure - private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { - self.proxy = proxy - self.course = course - self.viewModel = viewModel - } - - var body: some View { - let chapters = course.childs - ForEach(chapters, id: \.id) { chapter in - let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id }) - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.horizontal, 24) - .padding(.top, 40) - ForEach(chapter.childs, id: \.id) { child in - let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id }) - VStack(alignment: .leading) { - HStack { - Button { - if let chapterIndex, let sequentialIndex { - viewModel.trackSequentialClicked(child) - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: child.displayName, - chapters: chapters, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - } label: { - Group { - if child.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - } else { - child.type.image - } - Text(child.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - .frame( - maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading - ) - } - .foregroundColor(Theme.Colors.textPrimary) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(child.displayName) - Spacer() - if let state = viewModel.sequentialsDownloadState[child.id] { - switch state { - case .available: - DownloadAvailableView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.download) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - case .downloading: - DownloadProgressView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - case .finished: - DownloadFinishedView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - } - } - Image(systemName: "chevron.right") - .foregroundColor(Theme.Colors.accentColor) - } - .padding(.horizontal, 36) - .padding(.vertical, 20) - if chapterIndex != chapters.count - 1 { - Divider() - .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) - .padding(.horizontal, 24) - } - } - } - } - } -} diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift index 5d33fbd69..bc3722da1 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift @@ -34,7 +34,8 @@ struct CourseVerticalImageView_Previews: PreviewProvider { id: "1", courseId: "123", topicId: "1", - graded: false, + graded: false, + due: Date(), completion: 1, type: .video, displayName: "Block 1", @@ -52,6 +53,7 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .problem, displayName: "Block 1", @@ -68,6 +70,7 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .discussion, displayName: "Block 1", @@ -84,6 +87,7 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .html, displayName: "Block 1", @@ -100,6 +104,7 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .unknown, displayName: "Block 1", diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index d16095762..ccfc6f9b7 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -203,7 +203,14 @@ struct CourseVerticalView_Previews: PreviewProvider { type: .vertical, completion: 0, childs: []) - ]) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) ]) ] diff --git a/Course/Course/Presentation/Subviews/CourseProgressView.swift b/Course/Course/Presentation/Subviews/CourseProgressView.swift new file mode 100644 index 000000000..05ad004b4 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseProgressView.swift @@ -0,0 +1,55 @@ +// +// CourseProgressView.swift +// Course +// +// Created by  Stepanok Ivan on 23.05.2024. +// + +import SwiftUI +import Theme +import Core + +public struct CourseProgressView: View { + private var progress: CourseProgress + + public init(progress: CourseProgress) { + self.progress = progress + } + + public var body: some View { + VStack(alignment: .leading) { + ZStack(alignment: .leading) { + GeometryReader { geometry in + RoundedRectangle(cornerRadius: 10) + .fill(Theme.Colors.textSecondary.opacity(0.5)) + .frame(width: geometry.size.width, height: 10) + + if let total = progress.totalAssignmentsCount, + let completed = progress.assignmentsCompleted { + RoundedCorners(tl: 5, tr: 0, bl: 5, br: 0) + .fill(Theme.Colors.accentColor) + .frame(width: geometry.size.width * CGFloat(completed) / CGFloat(total), height: 10) + } + } + .frame(height: 10) + } + + .cornerRadius(10) + + if let total = progress.totalAssignmentsCount, + let completed = progress.assignmentsCompleted { + Text(CourseLocalization.Course.progressCompleted(completed, total)) + .foregroundColor(Theme.Colors.textPrimary) + .font(Theme.Fonts.labelSmall) + .padding(.top, 4) + } + } + } +} + +struct CourseProgressView_Previews: PreviewProvider { + static var previews: some View { + CourseProgressView(progress: CourseProgress(totalAssignmentsCount: 20, assignmentsCompleted: 12)) + .padding() + } +} diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift new file mode 100644 index 000000000..7cb1dcb74 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -0,0 +1,377 @@ +// +// CustomDisclosureGroup.swift +// Course +// +// Created by  Stepanok Ivan on 21.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct CustomDisclosureGroup: View { + @State private var expandedSections: [String: Bool] = [:] + + private let proxy: GeometryProxy + private let course: CourseStructure + private let viewModel: CourseContainerViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + init(course: CourseStructure, proxy: GeometryProxy, viewModel: CourseContainerViewModel) { + self.course = course + self.proxy = proxy + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + ForEach(course.childs) { chapter in + let chapterIndex = course.childs.firstIndex(where: { $0.id == chapter.id }) + VStack(alignment: .leading) { + Button(action: { + withAnimation(.linear(duration: 0.2)) { + expandedSections[chapter.id, default: false].toggle() + } + }, label: { + HStack { + CoreAssets.chevronRight.swiftUIImage + .rotationEffect(.degrees(expandedSections[chapter.id] ?? false ? 90 : -90)) + .foregroundColor(Theme.Colors.textPrimary) + if chapter.childs.allSatisfy({ $0.completion == 1 }) { + CoreAssets.finishedSequence.swiftUIImage + } + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .lineLimit(1) + Spacer() + if canDownloadAllSections(in: chapter), + let state = downloadAllButtonState(for: chapter) { + Button(action: { + downloadAllSubsections(in: chapter, state: state) + }, label: { + switch state { + case .available: + DownloadAvailableView() + case .downloading: + DownloadProgressView() + case .finished: + DownloadFinishedView() + } + + }) + } + } + }) + if expandedSections[chapter.id] ?? false { + VStack(alignment: .leading) { + ForEach(chapter.childs) { sequential in + let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == sequential.id }) + VStack(alignment: .leading) { + HStack { + Button(action: { + if let chapterIndex, let sequentialIndex { + viewModel.trackSequentialClicked(sequential) + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } + }, label: { + VStack(alignment: .leading) { + HStack { + if sequential.completion == 1 { + CoreAssets.finishedSequence.swiftUIImage + .resizable() + .frame(width: 20, height: 20) + } else { + sequential.type.image + } + Text(sequential.displayName) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .frame( + maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading + ) + } + if let sequentialProgress = sequential.sequentialProgress, + let assignmentType = sequentialProgress.assignmentType, + let numPointsEarned = sequentialProgress.numPointsEarned, + let numPointsPossible = sequentialProgress.numPointsPossible, + let due = sequential.due { + let daysRemaining = getAssignmentStatus(for: due) + Text("\(assignmentType) - \(daysRemaining) - \(numPointsEarned) / \(numPointsPossible)") + .font(Theme.Fonts.bodySmall) + .multilineTextAlignment(.leading) + .lineLimit(2) + } + } + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(sequential.displayName) + + }) + Spacer() + if sequential.sequentialProgress?.assignmentType != nil { + CoreAssets.chevronRight.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) + } + } + .padding(.vertical, 4) + } + } + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Theme.Colors.cardViewBackground) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(Theme.Colors.cardViewStroke) + ) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 8) + .onFirstAppear { + for chapter in course.childs { + expandedSections[chapter.id] = false + } + } + } + + func getAssignmentStatus(for date: Date) -> String { + let calendar = Calendar.current + let today = Date() + + if calendar.isDateInToday(date) { + return CourseLocalization.Course.dueToday + } else if calendar.isDateInTomorrow(date) { + return CourseLocalization.Course.dueTomorrow + } else if let daysUntil = calendar.dateComponents([.day], from: today, to: date).day, daysUntil > 0 { + return CourseLocalization.dueIn(daysUntil) + } else if let daysAgo = calendar.dateComponents([.day], from: date, to: today).day, daysAgo > 0 { + return CourseLocalization.pastDue(daysAgo) + } else { + return "" + } + } + + private func canDownloadAllSections(in chapter: CourseChapter) -> Bool { + for sequential in chapter.childs { + if let state = viewModel.sequentialsDownloadState[sequential.id] { + return true + } + } + return false + } + + private func downloadAllSubsections(in chapter: CourseChapter, state: DownloadViewState) { + Task { + for sequential in chapter.childs { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: sequential.id, + state: state + ) + } + } + } + + private func downloadAllButtonState(for chapter: CourseChapter) -> DownloadViewState? { + if canDownloadAllSections(in: chapter) { + let downloads = chapter.childs.filter({ viewModel.sequentialsDownloadState[$0.id] != nil }) + + if downloads.contains(where: { viewModel.sequentialsDownloadState[$0.id] == .downloading }) { + return .downloading + } else if downloads.allSatisfy({ viewModel.sequentialsDownloadState[$0.id] == .finished }) { + return .finished + } else { + return .available + } + } + return nil + } + +} + +#if DEBUG +struct CustomDisclosureGroup_Previews: PreviewProvider { + + static var previews: some View { + + // Sample data models + let sampleCourseChapters: [CourseChapter] = [ + CourseChapter( + blockId: "1", + id: "1", + displayName: "Chapter 1", + type: .chapter, + childs: [ + CourseSequential( + blockId: "1-1", + id: "1-1", + displayName: "Sequential 1", + type: .sequential, + completion: 0, + childs: [ + CourseVertical( + blockId: "1-1-1", + id: "1-1-1", + courseId: "1", + displayName: "Vertical 1", + type: .vertical, + completion: 0, + childs: [] + ), + CourseVertical( + blockId: "1-1-2", + id: "1-1-2", + courseId: "1", + displayName: "Vertical 2", + type: .vertical, + completion: 1.0, + childs: [] + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ), + CourseSequential( + blockId: "1-2", + id: "1-2", + displayName: "Sequential 2", + type: .sequential, + completion: 1.0, + childs: [ + CourseVertical( + blockId: "1-2-1", + id: "1-2-1", + courseId: "1", + displayName: "Vertical 3", + type: .vertical, + completion: 1.0, + childs: [] + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Basic Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) + ] + ), + CourseChapter( + blockId: "2", + id: "2", + displayName: "Chapter 2", + type: .chapter, + childs: [ + CourseSequential( + blockId: "2-1", + id: "2-1", + displayName: "Sequential 3", + type: .sequential, + completion: 1.0, + childs: [ + CourseVertical( + blockId: "2-1-1", + id: "2-1-1", + courseId: "2", + displayName: "Vertical 4", + type: .vertical, + completion: 1.0, + childs: [] + ), + CourseVertical( + blockId: "2-1-2", + id: "2-1-2", + courseId: "2", + displayName: "Vertical 5", + type: .vertical, + completion: 1.0, + childs: [] + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) + ] + ) + ] + + let viewModel = CourseContainerViewModel( + interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, + router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), + isActive: true, + courseStart: Date(), + courseEnd: nil, + enrollmentStart: Date(), + enrollmentEnd: nil, + coreAnalytics: CoreAnalyticsMock() + ) + Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + await viewModel.getCourseBlocks(courseID: "courseId") + } + group.addTask { + await viewModel.getCourseDeadlineInfo(courseID: "courseId") + } + } + } + + return GeometryReader { proxy in + ScrollView { + CustomDisclosureGroup( + course: CourseStructure( + id: "Id", + graded: false, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "Course", + childs: sampleCourseChapters, + media: DataLayer.CourseMedia.init(image: DataLayer.Image(raw: "", small: "", large: "")), + certificate: nil, + org: "org", + isSelfPaced: false, + courseProgress: nil + ), + proxy: proxy, + viewModel: viewModel + ) + } + } + } +} +#endif diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index dff0a0ea9..44f1e4a1b 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -442,6 +442,7 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", @@ -456,6 +457,7 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", @@ -470,6 +472,7 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", @@ -484,6 +487,7 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", @@ -517,10 +521,17 @@ struct CourseUnitView_Previews: PreviewProvider { completion: 0, childs: blocks ) - ] + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() ) - ]), + ] + ), CourseChapter( blockId: "2", id: "2", @@ -543,7 +554,13 @@ struct CourseUnitView_Previews: PreviewProvider { completion: 0, childs: blocks ) - ] + ], + sequentialProgress: SequentialProgress( + assignmentType: "Basic Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() ) ]) diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift index 397bf332a..7b7310fb4 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift @@ -77,6 +77,7 @@ struct CourseUnitDropDownCell_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .video, displayName: "Lesson 1", diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift index f338a202f..fea9801f3 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift @@ -51,6 +51,7 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", @@ -65,6 +66,7 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", @@ -79,6 +81,7 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", @@ -93,6 +96,7 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift index 805c709a1..dd7ddbc75 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift @@ -64,6 +64,7 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", @@ -79,6 +80,7 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", @@ -94,6 +96,7 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", @@ -109,6 +112,7 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index a66bfae07..1cada7b12 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -10,6 +10,14 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum CourseLocalization { + /// Plural format key: "%#@due_in@" + public static func dueIn(_ p1: Int) -> String { + return CourseLocalization.tr("Localizable", "due_in", p1, fallback: "Plural format key: \"%#@due_in@\"") + } + /// Plural format key: "%#@past_due@" + public static func pastDue(_ p1: Int) -> String { + return CourseLocalization.tr("Localizable", "past_due", p1, fallback: "Plural format key: \"%#@past_due@\"") + } public enum Accessibility { /// Cancel download public static let cancelDownload = CourseLocalization.tr("Localizable", "ACCESSIBILITY.CANCEL_DOWNLOAD", fallback: "Cancel download") @@ -30,6 +38,16 @@ public enum CourseLocalization { /// Turning off the switch will stop downloading and delete all downloaded videos for public static let stopDownloading = CourseLocalization.tr("Localizable", "ALERT.STOP_DOWNLOADING", fallback: "Turning off the switch will stop downloading and delete all downloaded videos for") } + public enum Course { + /// Due Today + public static let dueToday = CourseLocalization.tr("Localizable", "COURSE.DUE_TODAY", fallback: "Due Today") + /// Due Tomorrow + public static let dueTomorrow = CourseLocalization.tr("Localizable", "COURSE.DUE_TOMORROW", fallback: "Due Tomorrow") + /// %@ of %@ assignments complete + public static func progressCompleted(_ p1: Any, _ p2: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.PROGRESS_COMPLETED", String(describing: p1), String(describing: p2), fallback: "%@ of %@ assignments complete") + } + } public enum Courseware { /// Back to outline public static let backToOutline = CourseLocalization.tr("Localizable", "COURSEWARE.BACK_TO_OUTLINE", fallback: "Back to outline") diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 90c6472f3..40a4d8157 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -120,3 +120,8 @@ "COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; "COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; "COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; + +"COURSE.DUE_TODAY" = "Due Today"; +"COURSE.DUE_TOMORROW" = "Due Tomorrow"; + +"COURSE.PROGRESS_COMPLETED" = "%@ of %@ assignments complete"; diff --git a/Course/Course/en.lproj/Localizable.stringsdict b/Course/Course/en.lproj/Localizable.stringsdict new file mode 100644 index 000000000..ccfae8233 --- /dev/null +++ b/Course/Course/en.lproj/Localizable.stringsdict @@ -0,0 +1,42 @@ + + + + + due_in + + NSStringLocalizedFormatKey + %#@due_in@ + due_in + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Due in %d day + other + Due in %d days + + + past_due + + NSStringLocalizedFormatKey + %#@past_due@ + past_due + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Due %d day ago + other + Due %d days ago + + + + diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings index 59a57991a..abb9dc970 100644 --- a/Course/Course/uk.lproj/Localizable.strings +++ b/Course/Course/uk.lproj/Localizable.strings @@ -119,3 +119,8 @@ "COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; "COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; "COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; + +"COURSE.DUE_TODAY" = "Закінчується сьогодні"; +"COURSE.DUE_TOMORROW" = "Закінчується завтра"; + +"COURSE.PROGRESS_COMPLETED" = "%@ з %@ завдань виконано"; diff --git a/Course/Course/uk.lproj/Localizable.stringsdict b/Course/Course/uk.lproj/Localizable.stringsdict new file mode 100644 index 000000000..0b7ac9460 --- /dev/null +++ b/Course/Course/uk.lproj/Localizable.stringsdict @@ -0,0 +1,42 @@ + + + + + due_in + + NSStringLocalizedFormatKey + %#@due_in@ + due_in + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Прострочено на %d день + other + Прострочено на %d днів + + + past_due + + NSStringLocalizedFormatKey + %#@past_due@ + past_due + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Залишився %d день + other + Залишилося %d днів + + + + diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 435539972..d7fb25b2f 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -48,7 +48,8 @@ final class CourseContainerViewModelTests: XCTestCase { id: "", courseId: "123", topicId: "", - graded: true, + graded: true, + due: Date(), completion: 0, type: .problem, displayName: "", @@ -72,7 +73,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( blockId: "", @@ -98,7 +101,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let resumeBlock = ResumeBlock(blockID: "123") @@ -165,7 +169,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(interactor, .getLoadedCourseBlocks(courseID: .any, willReturn: courseStructure)) @@ -363,6 +368,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -396,7 +402,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -423,7 +431,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -500,6 +509,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -532,7 +542,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -559,7 +571,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -621,6 +634,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -653,7 +667,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -680,7 +696,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -743,6 +760,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -775,7 +793,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -802,7 +822,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -858,6 +879,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -890,7 +912,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -917,7 +941,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -988,6 +1013,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1020,7 +1046,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1047,7 +1075,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -1117,6 +1146,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1138,6 +1168,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1170,7 +1201,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1197,7 +1230,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift index 74d006cbc..19ae08286 100644 --- a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift @@ -46,7 +46,8 @@ final class CourseDateViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(interactor, .getCourseDates(courseID: .any, willReturn: courseDates)) diff --git a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift index 1e206e81e..abf7d2000 100644 --- a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift @@ -20,7 +20,8 @@ final class CourseUnitViewModelTests: XCTestCase { id: "1", courseId: "123", topicId: "1", - graded: false, + graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", @@ -34,6 +35,7 @@ final class CourseUnitViewModelTests: XCTestCase { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", @@ -47,6 +49,7 @@ final class CourseUnitViewModelTests: XCTestCase { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", @@ -60,6 +63,7 @@ final class CourseUnitViewModelTests: XCTestCase { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", @@ -90,7 +94,10 @@ final class CourseUnitViewModelTests: XCTestCase { type: .vertical, completion: 0, childs: blocks) - ]) + ], + sequentialProgress: nil, + due: Date() + ) ]), CourseChapter( @@ -112,7 +119,10 @@ final class CourseUnitViewModelTests: XCTestCase { type: .vertical, completion: 0, childs: blocks) - ]) + ], + sequentialProgress: nil, + due: Date() + ) ]) ] diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 12ee2d514..2bf633460 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -206,8 +206,7 @@ class AppAssembly: Assembly { PipManager( router: r.resolve(Router.self)!, discoveryInteractor: r.resolve(DiscoveryInteractorProtocol.self)!, - courseInteractor: r.resolve(CourseInteractorProtocol.self)!, - isNestedListEnabled: r.resolve(ConfigProtocol.self)?.uiComponents.courseNestedListEnabled ?? false + courseInteractor: r.resolve(CourseInteractorProtocol.self)! ) }.inObjectScope(.container) } diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index e2fc37e54..689ee3864 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -111,6 +111,7 @@ public class CoursePersistence: CoursePersistenceProtocol { blockId: $0.blockId ?? "", id: $0.id ?? "", graded: $0.graded, + due: $0.due, completion: $0.completion, studentUrl: $0.studentUrl ?? "", webUrl: $0.webUrl ?? "", @@ -119,7 +120,12 @@ public class CoursePersistence: CoursePersistenceProtocol { descendants: $0.descendants, allSources: $0.allSources, userViewData: userViewData, - multiDevice: $0.multiDevice + multiDevice: $0.multiDevice, + assignmentProgress: DataLayer.AssignmentProgress( + assignmentType: $0.assignmentType, + numPointsEarned: $0.numPointsEarned, + numPointsPossible: $0.numPointsPossible + ) ) } @@ -140,7 +146,11 @@ public class CoursePersistence: CoursePersistenceProtocol { ), certificate: DataLayer.Certificate(url: structure.certificate), org: structure.org ?? "", - isSelfPaced: structure.isSelfPaced + isSelfPaced: structure.isSelfPaced, + courseProgress: DataLayer.CourseProgress( + totalAssignmentsCount: Int(structure.totalAssignmentsCount), + assignmentsCompleted: Int(structure.assignmentsCompleted) + ) ) } @@ -154,6 +164,8 @@ public class CoursePersistence: CoursePersistenceProtocol { newStructure.id = structure.id newStructure.rootItem = structure.rootItem newStructure.isSelfPaced = structure.isSelfPaced + newStructure.totalAssignmentsCount = Int32(structure.courseProgress?.totalAssignmentsCount ?? 0) + newStructure.assignmentsCompleted = Int32(structure.courseProgress?.assignmentsCompleted ?? 0) for block in Array(structure.dict.values) { let courseDetail = CDCourseBlock(context: self.context) @@ -168,6 +180,18 @@ public class CoursePersistence: CoursePersistenceProtocol { courseDetail.type = block.type courseDetail.completion = block.completion ?? 0 courseDetail.multiDevice = block.multiDevice ?? false + if let numPointsEarned = block.assignmentProgress?.numPointsEarned { + courseDetail.numPointsEarned = numPointsEarned + } + if let numPointsPossible = block.assignmentProgress?.numPointsPossible { + courseDetail.numPointsPossible = numPointsPossible + } + if let assignmentType = block.assignmentProgress?.assignmentType { + courseDetail.assignmentType = assignmentType + } + if let due = block.due { + courseDetail.due = due + } if block.userViewData?.encodedVideo?.youTube != nil { let youTube = CDCourseBlockVideo(context: self.context) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 8720ae03f..8873e6d19 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -15,7 +15,6 @@ public class PipManager: PipManagerProtocol { let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router - let isNestedListEnabled: Bool public var isPipActive: Bool { controllerHolder != nil } @@ -26,13 +25,11 @@ public class PipManager: PipManagerProtocol { public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, - courseInteractor: CourseInteractorProtocol, - isNestedListEnabled: Bool + courseInteractor: CourseInteractorProtocol ) { self.discoveryInteractor = discoveryInteractor self.courseInteractor = courseInteractor self.router = router - self.isNestedListEnabled = isNestedListEnabled } public func holder( @@ -116,7 +113,7 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } - if !isNestedListEnabled && holder.selectedCourseTab != CourseTab.dates.rawValue { + if holder.selectedCourseTab != CourseTab.dates.rawValue { viewControllers.append(try await courseVerticalController(for: holder)) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 8e582a5f4..2c142af96 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -470,7 +470,7 @@ public class Router: AuthorizationRouter, )! let config = Container.shared.resolve(ConfigProtocol.self) - let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + let isDropdownActive = config?.uiComponents.courseDropDownNavigationEnabled ?? false let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) return UIHostingController(rootView: view) @@ -570,13 +570,10 @@ public class Router: AuthorizationRouter, chapterIndex: chapterIndex, sequentialIndex: sequentialIndex ) - - let config = Container.shared.resolve(ConfigProtocol.self) - let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false var controllers = navigationController.viewControllers - if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { + if currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { From 99edb6bd82ba654819050dbbe07751fa39e81b3c Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 31 May 2024 17:53:23 +0300 Subject: [PATCH 02/11] fix: address feedback --- .../Container/CourseContainerViewModel.swift | 9 + .../Outline/CourseOutlineView.swift | 13 +- .../Subviews/CourseProgressView.swift | 3 +- .../Subviews/CustomDisclosureGroup.swift | 176 +++++++++--------- 4 files changed, 105 insertions(+), 96 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index c6c150bd3..21259c1da 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -134,6 +134,15 @@ public class CourseContainerViewModel: BaseCourseViewModel { addObservers() } + func updateCourseIfNeeded(courseID: String) { + if updateCourseProgress { + Task { + await getCourseBlocks(courseID: courseID, withProgress: false) + } + updateCourseProgress = false + } + } + @MainActor func getCourseBlocks(courseID: String, withProgress: Bool = true) async { guard let courseStart, courseStart < Date() else { return } diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 3401f87f1..19824264b 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -13,7 +13,7 @@ import SwiftUIIntrospect public struct CourseOutlineView: View { - @ObservedObject private var viewModel: CourseContainerViewModel + @StateObject private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -43,7 +43,7 @@ public struct CourseOutlineView: View { dateTabIndex: Int ) { self.title = title - self.viewModel = viewModel + self._viewModel = StateObject(wrappedValue: { viewModel }()) self.courseID = courseID self.isVideo = isVideo self._selection = selection @@ -128,7 +128,7 @@ public struct CourseOutlineView: View { ? viewModel.courseVideosStructure : viewModel.courseStructure { - if !isVideo, let progress = course.courseProgress { + if !isVideo, let progress = course.courseProgress, progress.totalAssignmentsCount != 0 { CourseProgressView(progress: progress) .padding(.horizontal, 24) .padding(.top, 16) @@ -209,12 +209,7 @@ public struct CourseOutlineView: View { } } .onAppear { - if viewModel.updateCourseProgress { - Task { - await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) - } - viewModel.updateCourseProgress = false - } + viewModel.updateCourseIfNeeded(courseID: courseID) } .background( Theme.Colors.background diff --git a/Course/Course/Presentation/Subviews/CourseProgressView.swift b/Course/Course/Presentation/Subviews/CourseProgressView.swift index 05ad004b4..70ee1c2d8 100644 --- a/Course/Course/Presentation/Subviews/CourseProgressView.swift +++ b/Course/Course/Presentation/Subviews/CourseProgressView.swift @@ -12,7 +12,7 @@ import Core public struct CourseProgressView: View { private var progress: CourseProgress - public init(progress: CourseProgress) { + public init(progress: CourseProgress) { self.progress = progress } @@ -33,7 +33,6 @@ public struct CourseProgressView: View { } .frame(height: 10) } - .cornerRadius(10) if let total = progress.totalAssignmentsCount, diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 7cb1dcb74..4162985ec 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -28,99 +28,105 @@ struct CustomDisclosureGroup: View { ForEach(course.childs) { chapter in let chapterIndex = course.childs.firstIndex(where: { $0.id == chapter.id }) VStack(alignment: .leading) { - Button(action: { - withAnimation(.linear(duration: 0.2)) { - expandedSections[chapter.id, default: false].toggle() - } - }, label: { - HStack { - CoreAssets.chevronRight.swiftUIImage - .rotationEffect(.degrees(expandedSections[chapter.id] ?? false ? 90 : -90)) - .foregroundColor(Theme.Colors.textPrimary) - if chapter.childs.allSatisfy({ $0.completion == 1 }) { - CoreAssets.finishedSequence.swiftUIImage + Button( + action: { + withAnimation(.linear(duration: course.childs.count > 1 ? 0.2 : 0.05)) { + expandedSections[chapter.id, default: false].toggle() } - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) - .lineLimit(1) - Spacer() - if canDownloadAllSections(in: chapter), - let state = downloadAllButtonState(for: chapter) { - Button(action: { - downloadAllSubsections(in: chapter, state: state) - }, label: { - switch state { - case .available: - DownloadAvailableView() - case .downloading: - DownloadProgressView() - case .finished: - DownloadFinishedView() - } - - }) + }, label: { + HStack { + CoreAssets.chevronRight.swiftUIImage + .rotationEffect(.degrees(expandedSections[chapter.id] ?? false ? -90 : 90)) + .foregroundColor(Theme.Colors.textPrimary) + if chapter.childs.allSatisfy({ $0.completion == 1 }) { + CoreAssets.finishedSequence.swiftUIImage + } + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .lineLimit(1) + Spacer() + if canDownloadAllSections(in: chapter), + let state = downloadAllButtonState(for: chapter) { + Button( + action: { + downloadAllSubsections(in: chapter, state: state) + }, label: { + switch state { + case .available: + DownloadAvailableView() + case .downloading: + DownloadProgressView() + case .finished: + DownloadFinishedView() + } + + } + ) + } } } - }) + ) if expandedSections[chapter.id] ?? false { VStack(alignment: .leading) { ForEach(chapter.childs) { sequential in let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == sequential.id }) VStack(alignment: .leading) { HStack { - Button(action: { - if let chapterIndex, let sequentialIndex { - viewModel.trackSequentialClicked(sequential) - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: sequential.displayName, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - }, label: { - VStack(alignment: .leading) { - HStack { - if sequential.completion == 1 { - CoreAssets.finishedSequence.swiftUIImage - .resizable() - .frame(width: 20, height: 20) - } else { - sequential.type.image - } - Text(sequential.displayName) - .font(Theme.Fonts.titleSmall) - .multilineTextAlignment(.leading) - .lineLimit(1) - .frame( - maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading - ) + Button( + action: { + if let chapterIndex, let sequentialIndex { + viewModel.trackSequentialClicked(sequential) + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) } - if let sequentialProgress = sequential.sequentialProgress, - let assignmentType = sequentialProgress.assignmentType, - let numPointsEarned = sequentialProgress.numPointsEarned, - let numPointsPossible = sequentialProgress.numPointsPossible, - let due = sequential.due { - let daysRemaining = getAssignmentStatus(for: due) - Text("\(assignmentType) - \(daysRemaining) - \(numPointsEarned) / \(numPointsPossible)") - .font(Theme.Fonts.bodySmall) - .multilineTextAlignment(.leading) - .lineLimit(2) + }, label: { + VStack(alignment: .leading) { + HStack { + if sequential.completion == 1 { + CoreAssets.finishedSequence.swiftUIImage + .resizable() + .frame(width: 20, height: 20) + } else { + sequential.type.image + } + Text(sequential.displayName) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .frame( + maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading + ) + } + if let sequentialProgress = sequential.sequentialProgress, + let assignmentType = sequentialProgress.assignmentType, + let numPointsEarned = sequentialProgress.numPointsEarned, + let numPointsPossible = sequentialProgress.numPointsPossible, + let due = sequential.due { + let daysRemaining = getAssignmentStatus(for: due) + Text("\(assignmentType) - \(daysRemaining) - \(numPointsEarned) / \(numPointsPossible)") + .font(Theme.Fonts.bodySmall) + .multilineTextAlignment(.leading) + .lineLimit(2) + } } + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(sequential.displayName) + } - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityElement(children: .ignore) - .accessibilityLabel(sequential.displayName) - - }) + ) Spacer() - if sequential.sequentialProgress?.assignmentType != nil { + if sequential.due != nil { CoreAssets.chevronRight.swiftUIImage .foregroundColor(Theme.Colors.textPrimary) } @@ -135,7 +141,7 @@ struct CustomDisclosureGroup: View { .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 8) - .fill(Theme.Colors.cardViewBackground) + .fill(Theme.Colors.cardViewBackground) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -210,7 +216,7 @@ struct CustomDisclosureGroup: View { #if DEBUG struct CustomDisclosureGroup_Previews: PreviewProvider { - + static var previews: some View { // Sample data models @@ -246,7 +252,7 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { completion: 1.0, childs: [] ) - ], + ], sequentialProgress: SequentialProgress( assignmentType: "Advanced Assessment Tools", numPointsEarned: 1, @@ -349,7 +355,7 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { } } } - + return GeometryReader { proxy in ScrollView { CustomDisclosureGroup( @@ -364,7 +370,7 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { media: DataLayer.CourseMedia.init(image: DataLayer.Image(raw: "", small: "", large: "")), certificate: nil, org: "org", - isSelfPaced: false, + isSelfPaced: false, courseProgress: nil ), proxy: proxy, From 9462af1d057d0bad158f871519598c39e8d8b779 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Mon, 3 Jun 2024 12:07:40 +0300 Subject: [PATCH 03/11] fix: merge conflicts --- Core/Core/Data/Model/Data_PrimaryEnrollment.swift | 2 +- .../Data/Model/Data_CourseOutlineResponse.swift | 15 --------------- .../Subviews/CustomDisclosureGroup.swift | 3 ++- OpenEdX/Data/CoursePersistence.swift | 6 +++--- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index 1102bae78..60764c78a 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -36,7 +36,7 @@ public extension DataLayer { public let certificate: DataLayer.Certificate? public let courseModes: [CourseMode]? public let courseStatus: CourseStatus? - public let progress: CourseProgress? + public let progress: DataLayer.CourseProgress? public let courseAssignments: CourseAssignments? enum CodingKeys: String, CodingKey { diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index 4259c286e..4614736ba 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -233,19 +233,4 @@ public extension DataLayer { case streamPriority = "stream_priority" } } - - struct CourseProgress: Codable { - public let totalAssignmentsCount: Int? - public let assignmentsCompleted: Int? - - public init(totalAssignmentsCount: Int, assignmentsCompleted: Int) { - self.totalAssignmentsCount = totalAssignmentsCount - self.assignmentsCompleted = assignmentsCompleted - } - - enum CodingKeys: String, CodingKey { - case totalAssignmentsCount = "total_assignments_count" - case assignmentsCompleted = "assignments_completed" - } - } } diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 4162985ec..febd08109 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -342,7 +342,8 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), - enrollmentEnd: nil, + enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) Task { diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index c2fd84681..94ded4e9b 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -75,7 +75,7 @@ public class CoursePersistence: CoursePersistenceProtocol { let requestBlocks = CDCourseBlock.fetchRequest() requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) - + let blocks = try? context.fetch(requestBlocks).map { let userViewData = DataLayer.CourseDetailUserViewData( transcripts: $0.transcripts?.jsonStringToDictionary() as? [String: String], @@ -148,8 +148,8 @@ public class CoursePersistence: CoursePersistenceProtocol { org: structure.org ?? "", isSelfPaced: structure.isSelfPaced, courseProgress: DataLayer.CourseProgress( - totalAssignmentsCount: Int(structure.totalAssignmentsCount), - assignmentsCompleted: Int(structure.assignmentsCompleted) + assignmentsCompleted: Int(structure.assignmentsCompleted), + totalAssignmentsCount: Int(structure.totalAssignmentsCount) ) ) } From 574d5602610bfc8bac950964969d579d91383723 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Wed, 5 Jun 2024 11:52:58 +0300 Subject: [PATCH 04/11] fix: address feedback --- .../startDownloading.imageset/Contents.json | 12 +++++- ...startDownloading.svg => download_dark.svg} | 0 .../download_light.svg | 3 ++ Core/Core/View/Base/DownloadView.swift | 1 - .../Subviews/CustomDisclosureGroup.swift | 2 +- .../CardViewBackground.colorset/Contents.json | 6 +-- .../InfoColor.colorset copy/Contents.json | 38 ------------------- .../Contents.json | 38 ------------------- .../Colors/TabbarColor.colorset/Contents.json | 6 +-- 9 files changed, 21 insertions(+), 85 deletions(-) rename Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/{startDownloading.svg => download_dark.svg} (100%) create mode 100644 Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg delete mode 100644 Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json delete mode 100644 Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json index 0c0e4a7c2..672c958c5 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json @@ -1,7 +1,17 @@ { "images" : [ { - "filename" : "startDownloading.svg", + "filename" : "download_light.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "download_dark.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg similarity index 100% rename from Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/startDownloading.svg rename to Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg new file mode 100644 index 000000000..1f933d639 --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/View/Base/DownloadView.swift b/Core/Core/View/Base/DownloadView.swift index 1329d14e0..ede7c2912 100644 --- a/Core/Core/View/Base/DownloadView.swift +++ b/Core/Core/View/Base/DownloadView.swift @@ -24,7 +24,6 @@ public struct DownloadAvailableView: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.accentColor) } .frame(width: 30, height: 30) } diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index febd08109..917e81ac4 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -141,7 +141,7 @@ struct CustomDisclosureGroup: View { .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 8) - .fill(Theme.Colors.cardViewBackground) + .fill(Theme.Colors.tabbarColor) ) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json index 164b36790..44092279d 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json b/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json deleted file mode 100644 index 00d59cb46..000000000 --- a/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json b/Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json deleted file mode 100644 index 14e0c379b..000000000 --- a/Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.239", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.239", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json index 2d9b9cd70..7e4772ec9 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "0.984", - "green" : "0.980", - "red" : "0.976" + "blue" : "0xFA", + "green" : "0xF9", + "red" : "0xF8" } }, "idiom" : "universal" From 68a73deb958727eeabc693443188021e30a25f7b Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 6 Jun 2024 12:36:38 +0300 Subject: [PATCH 05/11] fix: address feedback --- .../Presentation/Container/CourseContainerViewModel.swift | 6 ++---- Course/Course/Presentation/Outline/CourseOutlineView.swift | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 5f519119b..51e687f3e 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -140,11 +140,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { addObservers() } - func updateCourseIfNeeded(courseID: String) { + func updateCourseIfNeeded(courseID: String) async { if updateCourseProgress { - Task { - await getCourseBlocks(courseID: courseID, withProgress: false) - } + await getCourseBlocks(courseID: courseID, withProgress: false) updateCourseProgress = false } } diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 426b9532c..cb7e1bec2 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -188,7 +188,9 @@ public struct CourseOutlineView: View { } } .onAppear { - viewModel.updateCourseIfNeeded(courseID: courseID) + Task { + await viewModel.updateCourseIfNeeded(courseID: courseID) + } } .background( Theme.Colors.background From 1a2b29893b99df0fe0c4865d23dff325b9f2151d Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 12:17:05 +0300 Subject: [PATCH 06/11] fix: address feedback --- .../Subviews/CustomDisclosureGroup.swift | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 917e81ac4..af7d062e1 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -75,18 +75,33 @@ struct CustomDisclosureGroup: View { HStack { Button( action: { - if let chapterIndex, let sequentialIndex { + if let chapterIndex, + let sequentialIndex, + let block = sequential.childs[sequentialIndex].childs.first { viewModel.trackSequentialClicked(sequential) - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: sequential.displayName, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) + if viewModel.config.uiComponents.courseDropDownNavigationEnabled { + viewModel.router.showCourseUnit( + courseName: viewModel.courseStructure?.displayName ?? "", + blockId: block.id, + courseID: viewModel.courseStructure?.id ?? "", + verticalIndex: 0, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } else { + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } } - }, label: { + }, + label: { VStack(alignment: .leading) { HStack { if sequential.completion == 1 { From 0eaecbb219a3e6a9c83e89f7a4388b69d579ce32 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 12:57:37 +0300 Subject: [PATCH 07/11] fix: address feedback --- Course/Course/Data/Model/Data_CourseOutlineResponse.swift | 2 +- .../Course/Presentation/Subviews/CustomDisclosureGroup.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index 4614736ba..5cee8c3e0 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -64,7 +64,7 @@ public extension DataLayer { certificate = try values.decode(Certificate.self, forKey: .certificate) org = try values.decode(String.self, forKey: .org) isSelfPaced = try values.decode(Bool.self, forKey: .isSelfPaced) - courseProgress = try values.decode(DataLayer.CourseProgress.self, forKey: .courseProgress) + courseProgress = try? values.decode(DataLayer.CourseProgress.self, forKey: .courseProgress) } } } diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index af7d062e1..bae4a917a 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -77,12 +77,12 @@ struct CustomDisclosureGroup: View { action: { if let chapterIndex, let sequentialIndex, - let block = sequential.childs[sequentialIndex].childs.first { + let blockId = sequential.childs.first?.blockId { viewModel.trackSequentialClicked(sequential) if viewModel.config.uiComponents.courseDropDownNavigationEnabled { viewModel.router.showCourseUnit( courseName: viewModel.courseStructure?.displayName ?? "", - blockId: block.id, + blockId: blockId, courseID: viewModel.courseStructure?.id ?? "", verticalIndex: 0, chapters: course.childs, From cc0f7d38ba188fc84a8105d3e2b09d77aa99f42a Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 14:10:28 +0300 Subject: [PATCH 08/11] fix: address feedback --- .../Subviews/CustomDisclosureGroup.swift | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index bae4a917a..75186dd91 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -75,30 +75,31 @@ struct CustomDisclosureGroup: View { HStack { Button( action: { - if let chapterIndex, - let sequentialIndex, - let blockId = sequential.childs.first?.blockId { - viewModel.trackSequentialClicked(sequential) - if viewModel.config.uiComponents.courseDropDownNavigationEnabled { - viewModel.router.showCourseUnit( - courseName: viewModel.courseStructure?.displayName ?? "", - blockId: blockId, - courseID: viewModel.courseStructure?.id ?? "", - verticalIndex: 0, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } else { - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: sequential.displayName, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } + guard let chapterIndex = chapterIndex else { return } + guard let sequentialIndex else { return } + guard let courseVertical = sequential.childs.first else { return } + guard let block = courseVertical.childs.first else { return } + + viewModel.trackSequentialClicked(sequential) + if viewModel.config.uiComponents.courseDropDownNavigationEnabled { + viewModel.router.showCourseUnit( + courseName: viewModel.courseStructure?.displayName ?? "", + blockId: block.id, + courseID: viewModel.courseStructure?.id ?? "", + verticalIndex: 0, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } else { + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) } }, label: { @@ -137,7 +138,6 @@ struct CustomDisclosureGroup: View { .foregroundColor(Theme.Colors.textPrimary) .accessibilityElement(children: .ignore) .accessibilityLabel(sequential.displayName) - } ) Spacer() @@ -150,6 +150,7 @@ struct CustomDisclosureGroup: View { } } } + } } .padding(.horizontal, 16) From e28415e01a0d10533df339270c2f9a3c5cde8967 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 14:31:57 +0300 Subject: [PATCH 09/11] fix: address feedback --- OpenEdX/DI/AppAssembly.swift | 6 ++++-- OpenEdX/DI/ScreenAssembly.swift | 2 +- OpenEdX/Managers/PipManager.swift | 7 +++++-- OpenEdX/Router.swift | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 2bf633460..6b722f8c2 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -203,10 +203,12 @@ class AppAssembly: Assembly { }.inObjectScope(.container) container.register(PipManagerProtocol.self) { r in - PipManager( + let config = r.resolve(ConfigProtocol.self)! + return PipManager( router: r.resolve(Router.self)!, discoveryInteractor: r.resolve(DiscoveryInteractorProtocol.self)!, - courseInteractor: r.resolve(CourseInteractorProtocol.self)! + courseInteractor: r.resolve(CourseInteractorProtocol.self)!, + courseDropDownNavigationEnabled: config.uiComponents.courseDropDownNavigationEnabled ) }.inObjectScope(.container) } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 44b421731..04ddc0514 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -352,7 +352,7 @@ class ScreenAssembly: Assembly { container.register( YouTubeVideoPlayerViewModel.self - ) { (r, url: URL?, blockID: String, courseID: String, languages, playerStateSubject) in + ) { (r, url: URL?, blockID: String, courseID: String, languages: [SubtitleUrl], playerStateSubject: CurrentValueSubject) in let router: Router = r.resolve(Router.self)! return YouTubeVideoPlayerViewModel( languages: languages, diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index d7221fd1c..3d81f8500 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -16,6 +16,7 @@ public class PipManager: PipManagerProtocol { let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router + let courseDropDownNavigationEnabled: Bool public var isPipActive: Bool { controllerHolder != nil } @@ -26,11 +27,13 @@ public class PipManager: PipManagerProtocol { public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, - courseInteractor: CourseInteractorProtocol + courseInteractor: CourseInteractorProtocol, + courseDropDownNavigationEnabled: Bool ) { self.discoveryInteractor = discoveryInteractor self.courseInteractor = courseInteractor self.router = router + self.courseDropDownNavigationEnabled = courseDropDownNavigationEnabled } public func holder( @@ -111,7 +114,7 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } - if holder.selectedCourseTab != CourseTab.dates.rawValue { + if !courseDropDownNavigationEnabled || holder.selectedCourseTab != CourseTab.dates.rawValue { viewControllers.append(try await courseVerticalController(for: holder)) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 0334ee6a1..d409eb282 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -592,8 +592,10 @@ public class Router: AuthorizationRouter, ) var controllers = navigationController.viewControllers + let config = Container.shared.resolve(ConfigProtocol.self)! + let courseDropDownNavigationEnabled = config.uiComponents.courseDropDownNavigationEnabled - if currentCourseTabSelection == CourseTab.dates.rawValue { + if !courseDropDownNavigationEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { From d8dbccb5f5420219c0aca4af11a31fbc62fb1628 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 14:39:22 +0300 Subject: [PATCH 10/11] fix: address feedback --- OpenEdX/Managers/PipManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 3d81f8500..94895077d 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -114,7 +114,7 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } - if !courseDropDownNavigationEnabled || holder.selectedCourseTab != CourseTab.dates.rawValue { + if !courseDropDownNavigationEnabled && holder.selectedCourseTab != CourseTab.dates.rawValue { viewControllers.append(try await courseVerticalController(for: holder)) } From 8c65f45ea90fdfc933aa746ea12356f20eb155b3 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 7 Jun 2024 14:57:05 +0300 Subject: [PATCH 11/11] fix: address feedback --- OpenEdX/Router.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index d409eb282..8dbbafa20 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -595,7 +595,7 @@ public class Router: AuthorizationRouter, let config = Container.shared.resolve(ConfigProtocol.self)! let courseDropDownNavigationEnabled = config.uiComponents.courseDropDownNavigationEnabled - if !courseDropDownNavigationEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { + if courseDropDownNavigationEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else {