From d3805511fdcfaafee587a10a06d2e5bd189c7f53 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:18:48 +0300 Subject: [PATCH] feat: [FC-0047] Course progress and collapsing sections (#446) * feat: course home navigation * fix: address feedback * fix: merge conflicts * fix: address feedback * fix: address feedback * fix: address feedback * fix: address feedback * fix: address feedback * fix: address feedback * fix: address feedback * fix: address feedback --- Core/Core.xcodeproj/project.pbxproj | 3 - .../deleteDownloading.imageset/Contents.json | 2 +- .../deleteDownloading.imageset/Frame-17.svg | 13 - .../deleteDownloading.svg | 3 + .../startDownloading.imageset/Contents.json | 15 +- .../startDownloading.imageset/Frame-16.svg | 12 - .../download_dark.svg | 3 + .../download_light.svg | 3 + .../finished_sequence.imageset/Contents.json | 12 + .../finished_sequence.svg | 3 + .../Config/UIComponentsConfig.swift | 6 +- .../Data/Model/Data_PrimaryEnrollment.swift | 2 +- Core/Core/Domain/Model/CourseBlockModel.swift | 38 +- Core/Core/SwiftGen/Assets.swift | 1 + .../View/Base/CustomDisclosureGroup.swift | 49 --- Core/Core/View/Base/DownloadView.swift | 5 +- Core/Core/en.lproj/Localizable.strings | 1 + Course/Course.xcodeproj/project.pbxproj | 38 +- Course/Course/Data/CourseRepository.swift | 30 +- .../Model/Data_CourseOutlineResponse.swift | 37 +- .../Course/Data/Network/CourseEndpoint.swift | 2 +- .../CourseCoreModel.xcdatamodel/contents | 8 +- Course/Course/Domain/CourseInteractor.swift | 10 +- .../Container/CourseContainerViewModel.swift | 19 +- .../Outline/ContinueWithView.swift | 2 + .../Outline/CourseOutlineView.swift | 38 +- .../CourseStructureNestedListView.swift | 237 ----------- .../CourseStructure/CourseStructureView.swift | 134 ------ .../CourseVerticalImageView.swift | 7 +- .../CourseVertical/CourseVerticalView.swift | 9 +- .../Subviews/CourseProgressView.swift | 54 +++ .../Subviews/CustomDisclosureGroup.swift | 400 ++++++++++++++++++ .../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 | 5 +- OpenEdX/DI/ScreenAssembly.swift | 2 +- OpenEdX/Data/CoursePersistence.swift | 30 +- OpenEdX/Managers/PipManager.swift | 8 +- OpenEdX/Router.swift | 9 +- .../CardViewBackground.colorset/Contents.json | 6 +- .../InfoColor.colorset copy/Contents.json | 38 -- .../Contents.json | 38 -- .../Colors/TabbarColor.colorset/Contents.json | 6 +- 53 files changed, 941 insertions(+), 630 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/download_dark.svg create mode 100644 Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.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 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.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 3d8d938c7..ad4325f39 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -163,7 +163,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 */; }; @@ -359,7 +358,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 = ""; }; @@ -748,7 +746,6 @@ 023A1137291432FD00D0D354 /* FieldConfiguration.swift */, BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */, BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */, - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */, BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, 02E93F862AEBAED4006C4750 /* AppReview */, BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */, 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..672c958c5 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json @@ -1,12 +1,25 @@ { "images" : [ { - "filename" : "Frame-16.svg", + "filename" : "download_light.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "download_dark.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/download_dark.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg new file mode 100644 index 000000000..8a29b74a2 --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg @@ -0,0 +1,3 @@ + + + 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/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/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/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/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index ea47e8ee4..6f38ed569 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -107,6 +107,7 @@ public enum CoreAssets { 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 learnEmpty = ImageAsset(name: "learn_empty") public static let airmail = ImageAsset(name: "airmail") 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..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.textPrimary) } .frame(width: 30, height: 30) } @@ -41,6 +40,7 @@ public struct DownloadProgressView: View { .resizable() .scaledToFit() .frame(width: 20, height: 20) + .foregroundStyle(Theme.Colors.snackbarErrorColor) .foregroundColor(Theme.Colors.textPrimary) } } @@ -52,11 +52,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 44186571a..b1fda17c5 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -48,6 +48,7 @@ "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 ed692cdd8..efc45a860 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 */; }; 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDC29252E900051930C /* CourseRouter.swift */; }; @@ -84,12 +87,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 */; }; @@ -149,7 +150,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 = ""; }; @@ -203,12 +208,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 = ""; }; @@ -313,6 +316,7 @@ 97EA4D822B84EFA900663F58 /* Managers */, 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, + 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); path = Course; sourceTree = ""; @@ -440,7 +444,6 @@ isa = PBXGroup; children = ( BAD9CA462B2C888600DE790A /* CourseVertical */, - BAD9CA472B2C88AA00DE790A /* CourseStructure */, 02635AC62A24F181008062F2 /* ContinueWithView.swift */, 0270210128E736E700F54332 /* CourseOutlineView.swift */, ); @@ -580,15 +583,6 @@ path = CourseVertical; sourceTree = ""; }; - BAD9CA472B2C88AA00DE790A /* CourseStructure */ = { - isa = PBXGroup; - children = ( - BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */, - BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */, - ); - path = CourseStructure; - sourceTree = ""; - }; BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( @@ -596,6 +590,8 @@ BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */, + 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */, + 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */, ); path = Subviews; sourceTree = ""; @@ -746,6 +742,7 @@ files = ( 0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */, 02B6B3B228E1C49400232911 /* Localizable.strings in Resources */, + 02C355392C08DCD700501342 /* Localizable.stringsdict in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -885,13 +882,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 */, 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */, @@ -907,6 +902,7 @@ DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */, 067B7B522BED339200D1768F /* SubtitlesView.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 */, @@ -924,6 +920,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 */, @@ -951,6 +948,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..5cee8c3e0 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,5 @@ public extension DataLayer { case fileSize = "file_size" case streamPriority = "stream_priority" } - } } 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 d25cbc5ad..d8e99bd3e 100644 --- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents +++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents @@ -2,14 +2,18 @@ + + + + @@ -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 dfec16d70..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 + ) ) } @@ -128,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 f2678a915..51e687f3e 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -16,8 +16,8 @@ public enum CourseTab: Int, CaseIterable, Identifiable { } case course case videos - case discussion case dates + case discussion case handounds } @@ -68,6 +68,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 { @@ -137,6 +140,13 @@ public class CourseContainerViewModel: BaseCourseViewModel { addObservers() } + func updateCourseIfNeeded(courseID: String) async { + if updateCourseProgress { + await getCourseBlocks(courseID: courseID, withProgress: false) + updateCourseProgress = false + } + } + func openLastVisitedBlock() { guard let continueWith = continueWith, let courseStructure = courseStructure else { return } @@ -607,6 +617,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 936ba835e..cb7e1bec2 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 @@ -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 = StateObject(wrappedValue: { 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 @@ -108,21 +107,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, progress.totalAssignmentsCount != 0 { + 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 : "") @@ -190,6 +187,11 @@ public struct CourseOutlineView: View { } } } + .onAppear { + Task { + await viewModel.updateCourseIfNeeded(courseID: courseID) + } + } .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..70ee1c2d8 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseProgressView.swift @@ -0,0 +1,54 @@ +// +// 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..75186dd91 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -0,0 +1,400 @@ +// +// 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: course.childs.count > 1 ? 0.2 : 0.05)) { + 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: { + 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: { + 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.due != nil { + CoreAssets.chevronRight.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) + } + } + .padding(.vertical, 4) + } + } + } + + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Theme.Colors.tabbarColor) + ) + .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, + lastVisitedBlockID: 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 cd69ebbb3..144614179 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -49,7 +49,8 @@ final class CourseContainerViewModelTests: XCTestCase { id: "", courseId: "123", topicId: "", - graded: true, + graded: true, + due: Date(), completion: 0, type: .problem, displayName: "", @@ -73,7 +74,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( blockId: "", @@ -99,7 +102,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let resumeBlock = ResumeBlock(blockID: "123") @@ -167,7 +171,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(interactor, .getLoadedCourseBlocks(courseID: .any, willReturn: courseStructure)) @@ -369,6 +374,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -402,7 +408,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -429,7 +437,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -507,6 +516,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -539,7 +549,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -566,7 +578,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -629,6 +642,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -661,7 +675,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -688,7 +704,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -752,6 +769,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -784,7 +802,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -811,7 +831,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -868,6 +889,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -900,7 +922,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -927,7 +951,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -999,6 +1024,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1031,7 +1057,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1058,7 +1086,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -1129,6 +1158,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1150,6 +1180,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1182,7 +1213,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1209,7 +1242,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..6b722f8c2 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -203,11 +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)!, - isNestedListEnabled: r.resolve(ConfigProtocol.self)?.uiComponents.courseNestedListEnabled ?? false + 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/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index 74005ca53..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], @@ -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( + assignmentsCompleted: Int(structure.assignmentsCompleted), + totalAssignmentsCount: Int(structure.totalAssignmentsCount) + ) ) } @@ -155,6 +165,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) @@ -169,6 +181,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 6de11a104..94895077d 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -16,7 +16,7 @@ public class PipManager: PipManagerProtocol { let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router - let isNestedListEnabled: Bool + let courseDropDownNavigationEnabled: Bool public var isPipActive: Bool { controllerHolder != nil } @@ -28,12 +28,12 @@ public class PipManager: PipManagerProtocol { router: Router, discoveryInteractor: DiscoveryInteractorProtocol, courseInteractor: CourseInteractorProtocol, - isNestedListEnabled: Bool + courseDropDownNavigationEnabled: Bool ) { self.discoveryInteractor = discoveryInteractor self.courseInteractor = courseInteractor self.router = router - self.isNestedListEnabled = isNestedListEnabled + self.courseDropDownNavigationEnabled = courseDropDownNavigationEnabled } public func holder( @@ -114,7 +114,7 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } - if !isNestedListEnabled && 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 f14818890..8dbbafa20 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -485,7 +485,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) @@ -590,13 +590,12 @@ 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 + let config = Container.shared.resolve(ConfigProtocol.self)! + let courseDropDownNavigationEnabled = config.uiComponents.courseDropDownNavigationEnabled - if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { + if courseDropDownNavigationEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { 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"