From e7edd623d14975f990dd7dfb777e57d06f1ab885 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 9 May 2024 15:31:06 +0300 Subject: [PATCH 01/22] feat: replace Discover page with new Learn page --- Core/Core.xcodeproj/project.pbxproj | 12 + .../learn.imageset/Contents.json | 15 + .../learn.imageset/learn filled.svg | 10 + .../chevron_right.imageset/Contents.json | 15 + .../chevron_right.imageset/chevron_right.svg | 15 + .../learn_big.imageset/Contents.json | 12 + .../learn_big.imageset/learn_big.svg | 14 + .../resumeCourse.imageset/Contents.json | 21 ++ .../resumeCourse.imageset/resumeCourse.svg | 3 + .../viewAll.imageset/Contents.json | 12 + .../viewAll.imageset/viewAll.svg | 14 + Core/Core/Configuration/Config/Config.swift | 1 + .../Config/DashboardConfig.swift | 34 ++ .../Config/DiscoveryConfig.swift | 5 + Core/Core/Data/Model/Data_Dashboard.swift | 12 +- Core/Core/Data/Model/Data_Discovery.swift | 6 +- .../Core/Data/Model/Data_MyLearnCourses.swift | 281 ++++++++++++++++ Core/Core/Domain/Model/CourseItem.swift | 25 +- Core/Core/Domain/Model/MyEnrollments.swift | 93 ++++++ Core/Core/Extensions/DateExtension.swift | 14 + Core/Core/SwiftGen/Assets.swift | 5 + Core/Core/SwiftGen/Strings.swift | 8 + Core/Core/View/Base/CourseCellView.swift | 5 +- Core/Core/en.lproj/Localizable.strings | 4 + Core/Core/uk.lproj/Localizable.strings | 1 + .../Container/CourseContainerView.swift | 5 +- .../Container/CourseContainerViewModel.swift | 78 +++-- .../Outline/CourseOutlineView.swift | 24 +- .../CourseContainerViewModelTests.swift | 13 + Dashboard/Dashboard.xcodeproj/project.pbxproj | 48 +++ .../Dashboard/Data/DashboardRepository.swift | 129 +++++++- .../Data/Network/DashboardEndpoint.swift | 23 +- .../DashboardCoreModel.xcdatamodel/contents | 44 ++- .../DashboardPersistenceProtocol.swift | 2 + .../Domain/DashboardInteractor.swift | 20 ++ .../Presentation/AllCoursesView.swift | 211 ++++++++++++ .../Presentation/AllCoursesViewModel.swift | 106 ++++++ .../Presentation/DashboardRouter.swift | 16 +- .../Presentation/DashboardView.swift | 4 +- .../Elements/CategoryFilterView.swift | 84 +++++ .../Elements/CourseCardView.swift | 125 +++++++ .../Presentation/Elements/DropDownMenu.swift | 81 +++++ .../Presentation/Elements/NoCoursesView.swift | 94 ++++++ .../Elements/PrimaryCardView.swift | 275 ++++++++++++++++ .../Elements/ProgressLineView.swift | 50 +++ .../Dashboard/Presentation/LearnView.swift | 309 ++++++++++++++++++ .../Presentation/LearnViewModel.swift | 76 +++++ Dashboard/Dashboard/SwiftGen/Strings.swift | 66 ++++ .../Dashboard/en.lproj/Localizable.strings | 32 ++ .../Dashboard/uk.lproj/Localizable.strings | 31 ++ .../DashboardMock.generated.swift | 159 +++++++++ .../DashboardViewModelTests.swift | 16 +- .../Discovery/Data/DiscoveryRepository.swift | 14 +- .../Presentation/DiscoveryRouter.swift | 8 +- .../NativeDiscovery/CourseDetailsView.swift | 4 +- .../DiscoveryWebviewViewModel.swift | 4 +- .../WebPrograms/ProgramWebviewViewModel.swift | 4 +- .../DiscoveryViewModelTests.swift | 24 +- .../Presentation/SearchViewModelTests.swift | 8 +- OpenEdX/DI/ScreenAssembly.swift | 22 +- OpenEdX/Data/CoursePersistence.swift | 9 +- OpenEdX/Data/DashboardPersistence.swift | 213 +++++++++++- OpenEdX/Data/DiscoveryPersistence.swift | 8 +- .../DeepLinkRouter/DeepLinkRouter.swift | 4 +- OpenEdX/Managers/PipManager.swift | 5 +- OpenEdX/Router.swift | 23 +- OpenEdX/View/MainScreenView.swift | 73 +++-- .../CardViewStroke.colorset/Contents.json | 6 +- .../Contents.json | 6 +- .../CourseCardShadow.colorset/Contents.json | 38 +++ Theme/Theme/SwiftGen/ThemeAssets.swift | 1 + Theme/Theme/Theme.swift | 1 + 72 files changed, 3092 insertions(+), 141 deletions(-) create mode 100644 Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg create mode 100644 Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg create mode 100644 Core/Core/Assets.xcassets/learn_big.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg create mode 100644 Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg create mode 100644 Core/Core/Assets.xcassets/viewAll.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg create mode 100644 Core/Core/Configuration/Config/DashboardConfig.swift create mode 100644 Core/Core/Data/Model/Data_MyLearnCourses.swift create mode 100644 Core/Core/Domain/Model/MyEnrollments.swift create mode 100644 Dashboard/Dashboard/Presentation/AllCoursesView.swift create mode 100644 Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift create mode 100644 Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift create mode 100644 Dashboard/Dashboard/Presentation/LearnView.swift create mode 100644 Dashboard/Dashboard/Presentation/LearnViewModel.swift create mode 100644 Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index f718ae857..9a362b53a 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */; }; 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */; }; 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F38293A452B00DE65D0 /* ResetPassword.swift */; }; + 02935B732BCECAD000B22F66 /* Data_MyLearnCourses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */; }; + 02935B752BCEE6D600B22F66 /* MyEnrollments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */; }; 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */; }; 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A463102AEA966C00331037 /* AppReviewView.swift */; }; 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */; }; @@ -75,6 +77,7 @@ 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */; }; 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */; }; 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */; }; + 02CA59842BD7DDBE00D517AA /* DashboardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */; }; 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF46C729546AA200A698EE /* NoCachedDataError.swift */; }; 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */; }; 02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D800CB29348F460099CF16 /* ImagePicker.swift */; }; @@ -245,6 +248,8 @@ 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleKeyboardInputView.swift; sourceTree = ""; }; 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_ResetPassword.swift; sourceTree = ""; }; 028F9F38293A452B00DE65D0 /* ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPassword.swift; sourceTree = ""; }; + 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_MyLearnCourses.swift; sourceTree = ""; }; + 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyEnrollments.swift; sourceTree = ""; }; 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; 02A463102AEA966C00331037 /* AppReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewView.swift; sourceTree = ""; }; 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistenceProtocol.swift; sourceTree = ""; }; @@ -257,6 +262,7 @@ 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewControllerExtension.swift; sourceTree = ""; }; 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Dashboard.swift; sourceTree = ""; }; + 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardConfig.swift; sourceTree = ""; }; 02CF46C729546AA200A698EE /* NoCachedDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCachedDataError.swift; sourceTree = ""; }; 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKStoreReviewControllerExtension.swift; sourceTree = ""; }; 02D800CB29348F460099CF16 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; @@ -586,6 +592,7 @@ 0727878428D31657002E9142 /* Data_User.swift */, 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */, 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */, + 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */, 021D924728DC860C00ACC565 /* Data_UserProfile.swift */, 0259104929C4A5B6004B5A55 /* UserSettings.swift */, 070019A428F6F17900D5FC78 /* Data_Media.swift */, @@ -611,6 +618,7 @@ children = ( 0727878828D31734002E9142 /* User.swift */, 0284DBFD28D48C5300830893 /* CourseItem.swift */, + 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */, 021D924F28DC89D100ACC565 /* UserProfile.swift */, 070019AB28F6FD0100D5FC78 /* CourseDetailBlock.swift */, 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */, @@ -839,6 +847,7 @@ BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */, BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, + 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */, A53A32342B233DEC005FE38A /* ThemeConfig.swift */, E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */, ); @@ -1076,6 +1085,7 @@ 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, 06619EAF2B973B25001FAADE /* AccessibilityInjection.swift in Sources */, BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, + 02935B732BCECAD000B22F66 /* Data_MyLearnCourses.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 06DEA4A32BBD66A700110D20 /* BackNavigationButton.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, @@ -1181,9 +1191,11 @@ 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */, 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */, 0649879A2B4D69FF0071642A /* WebViewHTML.swift in Sources */, + 02CA59842BD7DDBE00D517AA /* DashboardConfig.swift in Sources */, 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */, 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */, 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */, + 02935B752BCEE6D600B22F66 /* MyEnrollments.swift in Sources */, 020C31C9290AC3F700D6DEA2 /* PickerFields.swift in Sources */, 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */, 023A1138291432FD00D0D354 /* FieldConfiguration.swift in Sources */, diff --git a/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json new file mode 100644 index 000000000..718131171 --- /dev/null +++ b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "learn filled.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg new file mode 100644 index 000000000..c961205bc --- /dev/null +++ b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json new file mode 100644 index 000000000..a21ea6e5d --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chevron_right.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg new file mode 100644 index 000000000..e951c4282 --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/learn_big.imageset/Contents.json b/Core/Core/Assets.xcassets/learn_big.imageset/Contents.json new file mode 100644 index 000000000..f823d5953 --- /dev/null +++ b/Core/Core/Assets.xcassets/learn_big.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "learn_big.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg b/Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg new file mode 100644 index 000000000..a1874861e --- /dev/null +++ b/Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json b/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json new file mode 100644 index 000000000..1ab6cc7ba --- /dev/null +++ b/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "resumeCourse.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg b/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg new file mode 100644 index 000000000..0af03cb0c --- /dev/null +++ b/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json b/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json new file mode 100644 index 000000000..b044a6ae9 --- /dev/null +++ b/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "viewAll.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg b/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg new file mode 100644 index 000000000..da32ef8c1 --- /dev/null +++ b/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index 0c3aa5782..bd75f3f89 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -25,6 +25,7 @@ public protocol ConfigProtocol { var theme: ThemeConfig { get } var uiComponents: UIComponentsConfig { get } var discovery: DiscoveryConfig { get } + var dashboard: DashboardConfig { get } var braze: BrazeConfig { get } var branch: BranchConfig { get } var segment: SegmentConfig { get } diff --git a/Core/Core/Configuration/Config/DashboardConfig.swift b/Core/Core/Configuration/Config/DashboardConfig.swift new file mode 100644 index 000000000..3b7ac4543 --- /dev/null +++ b/Core/Core/Configuration/Config/DashboardConfig.swift @@ -0,0 +1,34 @@ +// +// DashboardConfig.swift +// Core +// +// Created by  Stepanok Ivan on 23.04.2024. +// + +import Foundation + +public enum DashboardConfigType: String { + case learn + case dashboard +} + +private enum DashboardKeys: String, RawStringExtractable { + case dashboardType = "TYPE" +} + +public class DashboardConfig: NSObject { + public let type: DashboardConfigType + + init(dictionary: [String: AnyObject]) { + type = (dictionary[DashboardKeys.dashboardType] as? String).flatMap { + DashboardConfigType(rawValue: $0) + } ?? .learn + } +} + +private let key = "DASHBOARD" +extension Config { + public var dashboard: DashboardConfig { + DashboardConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift index 884800441..708d11ae9 100644 --- a/Core/Core/Configuration/Config/DiscoveryConfig.swift +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -36,6 +36,11 @@ public class DiscoveryWebviewConfig: NSObject { public class DiscoveryConfig: NSObject { public let type: DiscoveryConfigType public let webview: DiscoveryWebviewConfig + public var isWebViewConfigured: Bool { + get { + return type == .webview && webview.baseURL != nil + } + } init(dictionary: [String: AnyObject]) { type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { diff --git a/Core/Core/Data/Model/Data_Dashboard.swift b/Core/Core/Data/Model/Data_Dashboard.swift index d39d8aa2d..830447ec5 100644 --- a/Core/Core/Data/Model/Data_Dashboard.swift +++ b/Core/Core/Data/Model/Data_Dashboard.swift @@ -68,6 +68,7 @@ public extension DataLayer { public let isActive: Bool public let course: DashboardCourse public let courseModes: [CourseMode] + public let progress: Progress? enum CodingKeys: String, CodingKey { case auditAccessExpires = "audit_access_expires" @@ -76,6 +77,7 @@ public extension DataLayer { case isActive = "is_active" case course case courseModes = "course_modes" + case progress } public init( @@ -84,7 +86,8 @@ public extension DataLayer { mode: Mode, isActive: Bool, course: DashboardCourse, - courseModes: [CourseMode] + courseModes: [CourseMode], + progress: Progress? ) { self.auditAccessExpires = auditAccessExpires self.created = created @@ -92,6 +95,7 @@ public extension DataLayer { self.isActive = isActive self.course = course self.courseModes = courseModes + self.progress = progress } } @@ -244,7 +248,7 @@ public extension DataLayer.CourseEnrollments { org: course.org, shortDescription: "", imageURL: fullImageURL, - isActive: true, + isActive: course.coursewareAccess.hasAccess, courseStart: course.start != nil ? Date(iso8601: course.start!) : nil, courseEnd: course.end != nil ? Date(iso8601: course.end!) : nil, enrollmentStart: course.start != nil @@ -255,7 +259,9 @@ public extension DataLayer.CourseEnrollments { : nil, courseID: course.id, numPages: enrollments.numPages ?? 1, - coursesCount: enrollments.count ?? 0 + coursesCount: enrollments.count ?? 0, + progressEarned: 0, + progressPossible: 0 ) } } diff --git a/Core/Core/Data/Model/Data_Discovery.swift b/Core/Core/Data/Model/Data_Discovery.swift index cb8dd9be8..cd0584055 100644 --- a/Core/Core/Data/Model/Data_Discovery.swift +++ b/Core/Core/Data/Model/Data_Discovery.swift @@ -108,14 +108,16 @@ public extension DataLayer.DiscoveryResponce { CourseItem(name: $0.name, org: $0.org, shortDescription: $0.shortDescription ?? "", imageURL: $0.media.image?.small ?? "", - isActive: nil, + isActive: true, courseStart: Date(iso8601: $0.start ?? ""), courseEnd: Date(iso8601: $0.end ?? ""), enrollmentStart: Date(iso8601: $0.enrollmentStart ?? ""), enrollmentEnd: Date(iso8601: $0.enrollmentEnd ?? ""), courseID: $0.courseID ?? "", numPages: pagination.numPages, - coursesCount: pagination.count) + coursesCount: pagination.count, + progressEarned: 0, + progressPossible: 0) }) return listReady } diff --git a/Core/Core/Data/Model/Data_MyLearnCourses.swift b/Core/Core/Data/Model/Data_MyLearnCourses.swift new file mode 100644 index 000000000..26b00a606 --- /dev/null +++ b/Core/Core/Data/Model/Data_MyLearnCourses.swift @@ -0,0 +1,281 @@ +// +// Data_MyLearnCourses.swift +// Core +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import Foundation + +public extension DataLayer { + struct MyLearnCourses: Codable { + public let userTimezone: String + public let enrollments: Enrollments + public let primary: Primary? + + enum CodingKeys: String, CodingKey { + case userTimezone = "user_timezone" + case enrollments = "enrollments" + case primary = "primary" + } + + public init(userTimezone: String, enrollments: Enrollments, primary: Primary?) { + self.userTimezone = userTimezone + self.enrollments = enrollments + self.primary = primary + } + } + + // MARK: - CourseImage + struct CourseImage: Codable { + public let uri: String + public let name: String + + enum CodingKeys: String, CodingKey { + case uri = "uri" + case name = "name" + } + + public init(uri: String, name: String) { + self.uri = uri + self.name = name + } + } + + // MARK: - Primary + struct Primary: Codable { + public let auditAccessExpires: Date? + public let created: String? + public let mode: String? + public let isActive: Bool? + public let course: DashboardCourse? + public let certificate: DataLayer.Certificate? + public let courseModes: [CourseMode]? + public let courseStatus: CourseStatus? + public let progress: Progress? + public let courseAssignments: CourseAssignments? + + enum CodingKeys: String, CodingKey { + case auditAccessExpires = "audit_access_expires" + case created = "created" + case mode = "mode" + case isActive = "is_active" + case course = "course" + case certificate = "certificate" + case courseModes = "course_modes" + case courseStatus = "course_status" + case progress = "course_progress" + case courseAssignments = "course_assignments" + } + + public init( + auditAccessExpires: Date?, + created: String?, + mode: String?, + isActive: Bool?, + course: DashboardCourse?, + certificate: DataLayer.Certificate?, + courseModes: [CourseMode]?, + courseStatus: CourseStatus?, + progress: Progress?, + courseAssignments: CourseAssignments? + ) { + self.auditAccessExpires = auditAccessExpires + self.created = created + self.mode = mode + self.isActive = isActive + self.course = course + self.certificate = certificate + self.courseModes = courseModes + self.courseStatus = courseStatus + self.progress = progress + self.courseAssignments = courseAssignments + } + } + + // MARK: - CourseStatus + struct CourseStatus: Codable { + public let lastVisitedModuleID: String + public let lastVisitedModulePath: [String] + public let lastVisitedBlockID: String + public let lastVisitedUnitDisplayName: String + + enum CodingKeys: String, CodingKey { + case lastVisitedModuleID = "last_visited_module_id" + case lastVisitedModulePath = "last_visited_module_path" + case lastVisitedBlockID = "last_visited_block_id" + case lastVisitedUnitDisplayName = "last_visited_unit_display_name" + } + } + + // MARK: - CourseAssignments + struct CourseAssignments: Codable { + public let futureAssignments: [Assignment]? + public let pastAssignments: [Assignment]? + + enum CodingKeys: String, CodingKey { + case futureAssignments = "future_assignments" + case pastAssignments = "past_assignments" + } + + public init(futureAssignments: [Assignment]?, pastAssignments: [Assignment]?) { + self.futureAssignments = futureAssignments + self.pastAssignments = pastAssignments + } + } + + // MARK: - Assignment + struct Assignment: Codable { + public let assignmentType: String? + public let complete: Bool? + public let date: String? + public let dateType: String? + public let description: String? + public let learnerHasAccess: Bool? + public let link: String? + public let linkText: String? + public let title: String? + public let extraInfo: String? + public let firstComponentBlockID: String? + + enum CodingKeys: String, CodingKey { + case assignmentType = "assignment_type" + case complete = "complete" + case date = "date" + case dateType = "date_type" + case description = "description" + case learnerHasAccess = "learner_has_access" + case link = "link" + case linkText = "link_text" + case title = "title" + case extraInfo = "extra_info" + case firstComponentBlockID = "first_component_block_id" + } + + public init( + assignmentType: String?, + complete: Bool?, + date: String?, + dateType: String?, + description: String?, + learnerHasAccess: Bool?, + link: String?, + linkText: String?, + title: String?, + extraInfo: String?, + firstComponentBlockID: String? + ) { + self.assignmentType = assignmentType + self.complete = complete + self.date = date + self.dateType = dateType + self.description = description + self.learnerHasAccess = learnerHasAccess + self.link = link + self.linkText = linkText + self.title = title + self.extraInfo = extraInfo + self.firstComponentBlockID = firstComponentBlockID + } + } + + // MARK: - Progress + struct Progress: Codable { + public let assignmentsCompleted: Double + public let totalAssignmentsCount: Double + + enum CodingKeys: String, CodingKey { + case assignmentsCompleted = "assignments_completed" + case totalAssignmentsCount = "total_assignments_count" + } + + public init(assignmentsCompleted: Double, totalAssignmentsCount: Double) { + self.assignmentsCompleted = assignmentsCompleted + self.totalAssignmentsCount = totalAssignmentsCount + } + } +} + +public extension DataLayer.MyLearnCourses { + func domain(baseURL: String) -> MyEnrollments { + var primaryCourse: PrimaryCourse? + if let primary = self.primary { + let futureAssignments: [DataLayer.Assignment] = primary.courseAssignments?.futureAssignments ?? [] + let pastAssignments: [DataLayer.Assignment] = primary.courseAssignments?.pastAssignments ?? [] + + primaryCourse = PrimaryCourse( + name: primary.course?.name ?? "", + org: primary.course?.org ?? "", + courseID: primary.course?.id ?? "", + isActive: primary.course?.coursewareAccess.hasAccess ?? true, + courseStart: primary.course?.start != nil + ? Date(iso8601: primary.course?.start ?? "") + : nil, + courseEnd: primary.course?.end != nil + ? Date(iso8601: primary.course?.end ?? "") + : nil, + courseBanner: baseURL + (primary.course?.media.courseImage?.url ?? ""), + futureAssignments: futureAssignments.map { + Assignment( + type: $0.assignmentType ?? "", + title: $0.title ?? "", + description: $0.description, + date: Date(iso8601: $0.date ?? ""), + complete: $0.complete ?? false, + firstComponentBlockId: $0.firstComponentBlockID + ) + }, + pastAssignments: pastAssignments.map { + Assignment( + type: $0.assignmentType ?? "", + title: $0.title ?? "", + description: $0.description ?? "", + date: Date(iso8601: $0.date ?? ""), + complete: $0.complete ?? false, + firstComponentBlockId: $0.firstComponentBlockID + ) + }, + progressEarned: primary.progress?.assignmentsCompleted ?? 0, + progressPossible: primary.progress?.totalAssignmentsCount ?? 0, + lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, + resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName + ) + } + let courses = self.enrollments.results.map { + let imageUrl = $0.course.media.courseImage?.url ?? "" + let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let fullImageURL = baseURL + encodedUrl + return CourseItem( + name: $0.course.name, + org: $0.course.org, + shortDescription: "", + imageURL: fullImageURL, + isActive: $0.course.coursewareAccess.hasAccess, + courseStart: $0.course.start != nil + ? Date(iso8601: $0.course.start!) + : nil, + courseEnd: $0.course.end != nil + ? Date(iso8601: $0.course.end!) + : nil, + enrollmentStart: $0.course.start != nil + ? Date(iso8601: $0.course.start!) + : nil, + enrollmentEnd: $0.course.end != nil + ? Date(iso8601: $0.course.end!) + : nil, + courseID: $0.course.id, + numPages: enrollments.numPages ?? 1, + coursesCount: enrollments.count ?? 0, + progressEarned: $0.progress?.assignmentsCompleted ?? 0, + progressPossible: $0.progress?.totalAssignmentsCount ?? 0 + ) + } + + return MyEnrollments( + primaryCourse: primaryCourse, + courses: courses, + totalPages: enrollments.numPages ?? 1, + count: enrollments.count ?? 1 + ) + } +} diff --git a/Core/Core/Domain/Model/CourseItem.swift b/Core/Core/Domain/Model/CourseItem.swift index 9229417f1..f13d5dc2f 100644 --- a/Core/Core/Domain/Model/CourseItem.swift +++ b/Core/Core/Domain/Model/CourseItem.swift @@ -7,12 +7,25 @@ import Foundation +public enum CourseTab: Int, CaseIterable, Identifiable { + public var id: Int { + rawValue + } + + case course + case videos + case discussion + case dates + case handounds + +} + public struct CourseItem: Hashable { public let name: String public let org: String public let shortDescription: String public let imageURL: String - public let isActive: Bool? + public let isActive: Bool public let courseStart: Date? public let courseEnd: Date? public let enrollmentStart: Date? @@ -20,19 +33,23 @@ public struct CourseItem: Hashable { public let courseID: String public let numPages: Int public let coursesCount: Int + public let progressEarned: Double + public let progressPossible: Double public init(name: String, org: String, shortDescription: String, imageURL: String, - isActive: Bool?, + isActive: Bool, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, courseID: String, numPages: Int, - coursesCount: Int) { + coursesCount: Int, + progressEarned: Double, + progressPossible: Double) { self.name = name self.org = org self.shortDescription = shortDescription @@ -45,5 +62,7 @@ public struct CourseItem: Hashable { self.courseID = courseID self.numPages = numPages self.coursesCount = coursesCount + self.progressEarned = progressEarned + self.progressPossible = progressPossible } } diff --git a/Core/Core/Domain/Model/MyEnrollments.swift b/Core/Core/Domain/Model/MyEnrollments.swift new file mode 100644 index 000000000..5236fe370 --- /dev/null +++ b/Core/Core/Domain/Model/MyEnrollments.swift @@ -0,0 +1,93 @@ +// +// MyEnrollments.swift +// Core +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import Foundation + +public struct MyEnrollments: Hashable { + public let primaryCourse: PrimaryCourse? + public var courses: [CourseItem] + public let totalPages: Int + public let count: Int + + public init(primaryCourse: PrimaryCourse?, courses: [CourseItem], totalPages: Int, count: Int) { + self.primaryCourse = primaryCourse + self.courses = courses + self.totalPages = totalPages + self.count = count + } +} + +public struct PrimaryCourse: Hashable { + public let name: String + public let org: String + public let courseID: String + public let isActive: Bool + public let courseStart: Date? + public let courseEnd: Date? + public let courseBanner: String + public let futureAssignments: [Assignment] + public let pastAssignments: [Assignment] + public let progressEarned: Double? + public let progressPossible: Double? + public let lastVisitedBlockID: String? + public let resumeTitle: String? + + public init( + name: String, + org: String, + courseID: String, + isActive: Bool, + courseStart: Date?, + courseEnd: Date?, + courseBanner: String, + futureAssignments: [Assignment], + pastAssignments: [Assignment], + progressEarned: Double?, + progressPossible: Double?, + lastVisitedBlockID: String?, + resumeTitle: String? + ) { + self.name = name + self.org = org + self.courseID = courseID + self.isActive = isActive + self.courseStart = courseStart + self.courseEnd = courseEnd + self.courseBanner = courseBanner + self.futureAssignments = futureAssignments + self.pastAssignments = pastAssignments + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.lastVisitedBlockID = lastVisitedBlockID + self.resumeTitle = resumeTitle + } +} + +public struct Assignment: Hashable { + public let type: String + public let title: String + public let description: String? + public let date: Date + public let complete: Bool + public let firstComponentBlockId: String? + + public init( + type: String, + title: String, + description: String?, + date: Date, + complete: Bool, + firstComponentBlockId: String? + ) { + self.type = type + self.title = title + self.description = description + self.date = date + self.complete = complete + self.firstComponentBlockId = firstComponentBlockId + } +} diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 8a57079f4..f48487e46 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -76,6 +76,8 @@ public extension Date { } public enum DateStringStyle { + case courseStartsMonthDDYear + case courseEndsMonthDDYear case startDDMonthYear case endedMonthDay case mmddyy @@ -104,6 +106,10 @@ public extension Date { dateFormatter.locale = Locale(identifier: "en_US_POSIX") switch style { + case .courseStartsMonthDDYear: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy + case .courseEndsMonthDDYear: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy case .endedMonthDay: dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd case .mmddyy: @@ -123,6 +129,14 @@ public extension Date { let date = dateFormatter.string(from: self) switch style { + case .courseStartsMonthDDYear: + return CoreLocalization.Date.courseStarts + " " + date + case .courseEndsMonthDDYear: + if Date() < self { + return CoreLocalization.Date.courseEnds + " " + date + } else { + return CoreLocalization.Date.courseEnded + " " + date + } case .endedMonthDay: return CoreLocalization.Date.ended + " " + date case .mmddyy, .monthYear: diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index e25f452d0..cf2f76fe0 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -70,6 +70,7 @@ public enum CoreAssets { public static let handouts = ImageAsset(name: "handouts") public static let dashboard = ImageAsset(name: "dashboard") public static let discovery = ImageAsset(name: "discovery") + public static let learn = ImageAsset(name: "learn") public static let profile = ImageAsset(name: "profile") public static let programs = ImageAsset(name: "programs") public static let addPhoto = ImageAsset(name: "addPhoto") @@ -96,10 +97,12 @@ public enum CoreAssets { public static let certificateBadge = ImageAsset(name: "certificateBadge") public static let check = ImageAsset(name: "check") public static let checkEmail = ImageAsset(name: "checkEmail") + public static let chevronRight = ImageAsset(name: "chevron_right") public static let clearInput = ImageAsset(name: "clearInput") public static let edit = ImageAsset(name: "edit") public static let favorite = ImageAsset(name: "favorite") public static let goodWork = ImageAsset(name: "goodWork") + public static let learnBig = ImageAsset(name: "learn_big") public static let airmail = ImageAsset(name: "airmail") public static let defaultMail = ImageAsset(name: "defaultMail") public static let fastmail = ImageAsset(name: "fastmail") @@ -113,8 +116,10 @@ public enum CoreAssets { public static let noWifiMini = ImageAsset(name: "noWifiMini") public static let notAvaliable = ImageAsset(name: "notAvaliable") public static let playVideo = ImageAsset(name: "playVideo") + public static let resumeCourse = ImageAsset(name: "resumeCourse") public static let star = ImageAsset(name: "star") public static let starOutline = ImageAsset(name: "star_outline") + public static let viewAll = ImageAsset(name: "viewAll") public static let warning = ImageAsset(name: "warning") public static let warningFilled = ImageAsset(name: "warning_filled") } diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index a5782d497..faa821bc2 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -79,6 +79,12 @@ public enum CoreLocalization { } } public enum Date { + /// Course Ended + public static let courseEnded = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDED", fallback: "Course Ended") + /// Course Ends + public static let courseEnds = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDS", fallback: "Course Ends") + /// Course Starts + public static let courseStarts = CoreLocalization.tr("Localizable", "DATE.COURSE_STARTS", fallback: "Course Starts") /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") /// Just now @@ -136,6 +142,8 @@ public enum CoreLocalization { public static let discovery = CoreLocalization.tr("Localizable", "MAINSCREEN.DISCOVERY", fallback: "Discover") /// In developing public static let inDeveloping = CoreLocalization.tr("Localizable", "MAINSCREEN.IN_DEVELOPING", fallback: "In developing") + /// Learn + public static let learn = CoreLocalization.tr("Localizable", "MAINSCREEN.LEARN", fallback: "Learn") /// Profile public static let profile = CoreLocalization.tr("Localizable", "MAINSCREEN.PROFILE", fallback: "Profile") /// Programs diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index 6166b81a3..bbf5c4f8f 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -137,7 +137,10 @@ struct CourseCellView_Previews: PreviewProvider { enrollmentEnd: nil, courseID: "1", numPages: 1, - coursesCount: 10) + coursesCount: 10, + progressEarned: 4, + progressPossible: 10 + ) static var previews: some View { ZStack { diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 27196487a..ca5657132 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -11,6 +11,7 @@ "MAINSCREEN.IN_DEVELOPING" = "In developing"; "MAINSCREEN.PROGRAMS" = "Programs"; "MAINSCREEN.PROFILE" = "Profile"; +"MAINSCREEN.LEARN" = "Learn"; "VIEW.SNACKBAR.TRY_AGAIN_BTN" = "Try Again"; @@ -44,6 +45,9 @@ "ERROR.RELOAD" = "Reload"; +"DATE.COURSE_STARTS" = "Course Starts"; +"DATE.COURSE_ENDS" = "Course Ends"; +"DATE.COURSE_ENDED" = "Course Ended"; "DATE.ENDED" = "Ended"; "DATE.START" = "Start"; "DATE.STARTED" = "Started"; diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index 1da07ab04..1026b82e0 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -11,6 +11,7 @@ "MAINSCREEN.IN_DEVELOPING" = "В розробці"; "MAINSCREEN.PROGRAMS" = "Програми"; "MAINSCREEN.PROFILE" = "Профіль"; +"MAINSCREEN.LEARN" = "Навчання"; "VIEW.SNACKBAR.TRY_AGAIN_BTN" = "Спробувати ще"; diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 322b37564..135fdd764 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -311,9 +311,12 @@ struct CourseScreensView_Previews: PreviewProvider { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ), - courseID: "", title: "Title of Course") + courseID: "", + title: "Title of Course" + ) } } #endif diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 82572b60e..c9c5f83b8 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -10,17 +10,7 @@ import SwiftUI import Core import Combine -public enum CourseTab: Int, CaseIterable, Identifiable { - public var id: Int { - rawValue - } - - case course - case videos - case discussion - case dates - case handounds - +extension CourseTab { public var title: String { switch self { case .course: @@ -54,7 +44,7 @@ public enum CourseTab: Int, CaseIterable, Identifiable { public class CourseContainerViewModel: BaseCourseViewModel { - @Published public var selection: Int = CourseTab.course.rawValue + @Published public var selection: Int @Published var isShowProgress = true @Published var isShowRefresh = false @Published var courseStructure: CourseStructure? @@ -85,6 +75,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { let courseEnd: Date? let enrollmentStart: Date? let enrollmentEnd: Date? + let lastVisitedBlockID: String? var courseDownloadTasks: [DownloadDataTask] = [] private(set) var waitingDownloads: [CourseBlock]? @@ -109,7 +100,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - coreAnalytics: CoreAnalytics + lastVisitedBlockID: String?, + coreAnalytics: CoreAnalytics, + selection: CourseTab = CourseTab.course ) { self.interactor = interactor self.authInteractor = authInteractor @@ -125,12 +118,44 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.storage = storage self.userSettings = storage.userSettings self.isInternetAvaliable = connectivity.isInternetAvaliable + self.lastVisitedBlockID = lastVisitedBlockID self.coreAnalytics = coreAnalytics + self.selection = selection.rawValue super.init(manager: manager) addObservers() } + func openLastVisitedBlock() { + if let continueWith = continueWith, + let courseStructure = courseStructure { + let chapter = courseStructure.childs[continueWith.chapterIndex] + let sequential = chapter.childs[continueWith.sequentialIndex] + let continueUnit = sequential.childs[continueWith.verticalIndex] + + var continueBlock: CourseBlock? + continueUnit.childs.forEach { block in + if block.id == continueWith.lastVisitedBlockId { + continueBlock = block + } + } + + trackResumeCourseClicked( + blockId: continueBlock?.id ?? "" + ) + + router.showCourseUnit( + courseName: courseStructure.displayName, + blockId: continueBlock?.id ?? "", + courseID: courseStructure.id, + verticalIndex: continueWith.verticalIndex, + chapters: courseStructure.childs, + chapterIndex: continueWith.chapterIndex, + sequentialIndex: continueWith.sequentialIndex + ) + } + } + @MainActor func getCourseBlocks(courseID: String, withProgress: Bool = true) async { guard let courseStart, courseStart < Date() else { return } @@ -143,13 +168,10 @@ public class CourseContainerViewModel: BaseCourseViewModel { isShowProgress = false isShowRefresh = false if let courseStructure { - let continueWith = try await getResumeBlock( + try await getResumeBlock( courseID: courseID, courseStructure: courseStructure ) - withAnimation { - self.continueWith = continueWith - } } } else { courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) @@ -236,12 +258,22 @@ public class CourseContainerViewModel: BaseCourseViewModel { } @MainActor - private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> ContinueWith? { - let result = try await interactor.resumeBlock(courseID: courseID) - return findContinueVertical( - blockID: result.blockID, - courseStructure: courseStructure - ) + private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws { + if let lastVisitedBlockID { + self.continueWith = findContinueVertical( + blockID: lastVisitedBlockID, + courseStructure: courseStructure + ) + openLastVisitedBlock() + } else { + let result = try await interactor.resumeBlock(courseID: courseID) + withAnimation { + self.continueWith = findContinueVertical( + blockID: result.blockID, + courseStructure: courseStructure + ) + } + } } @MainActor diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 18575dc7a..8a97a2a23 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -100,28 +100,7 @@ public struct CourseOutlineView: View { data: continueWith, courseContinueUnit: continueUnit ) { - var continueBlock: CourseBlock? - continueUnit.childs.forEach { block in - if block.id == continueWith.lastVisitedBlockId { - continueBlock = block - } - } - - viewModel.trackResumeCourseClicked( - blockId: continueBlock?.id ?? "" - ) - - if let course = viewModel.courseStructure { - viewModel.router.showCourseUnit( - courseName: course.displayName, - blockId: continueBlock?.id ?? "", - courseID: course.id, - verticalIndex: continueWith.verticalIndex, - chapters: course.childs, - chapterIndex: continueWith.chapterIndex, - sequentialIndex: continueWith.sequentialIndex - ) - } + viewModel.openLastVisitedBlock() } } @@ -344,6 +323,7 @@ struct CourseOutlineView_Previews: PreviewProvider { courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) Task { diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 435539972..cd69ebbb3 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -40,6 +40,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -148,6 +149,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -207,6 +209,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -250,6 +253,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -290,6 +294,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -330,6 +335,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -462,6 +468,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -583,6 +590,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -704,6 +712,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -826,6 +835,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -956,6 +966,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -1086,6 +1097,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure @@ -1236,6 +1248,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 2a7b812fe..30ea18598 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -7,14 +7,24 @@ objects = { /* Begin PBXBuildFile section */ + 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */; }; + 027724202BCEA16C00C2908D /* ProgressLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */; }; 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33228D8BDBA002B6862 /* DashboardView.swift */; }; 027DB33928D8D9F6002B6862 /* DashboardEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */; }; 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */; }; 027DB33F28D8E605002B6862 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB33E28D8E605002B6862 /* Core.framework */; }; 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */; }; 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */; }; + 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028895662BE3B34E00102D8C /* NoCoursesView.swift */; }; + 02935B6F2BCEC91100B22F66 /* LearnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B6E2BCEC91100B22F66 /* LearnView.swift */; }; + 02935B712BCEC91F00B22F66 /* LearnViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */; }; + 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B762BCFB2C100B22F66 /* CourseCardView.swift */; }; 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */; }; 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */; }; + 02A98F112BD96814009B46BD /* DropDownMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F102BD96814009B46BD /* DropDownMenu.swift */; }; + 02A98F132BD96B9D009B46BD /* AllCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */; }; + 02A98F152BD96BC2009B46BD /* AllCoursesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */; }; + 02A98F172BD97886009B46BD /* CategoryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F162BD97885009B46BD /* CategoryFilterView.swift */; }; 02A9A90B2978194100B55797 /* DashboardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */; }; 02A9A90C2978194100B55797 /* Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02EF39E728D89F560058F6BD /* Dashboard.framework */; platformFilter = ios; }; 02A9A92929781A4D00B55797 /* DashboardMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */; }; @@ -39,14 +49,24 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCardView.swift; sourceTree = ""; }; + 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressLineView.swift; sourceTree = ""; }; 027DB33228D8BDBA002B6862 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEndpoint.swift; sourceTree = ""; }; 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardRepository.swift; sourceTree = ""; }; 027DB33E28D8E605002B6862 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardInteractor.swift; sourceTree = ""; }; 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; + 028895662BE3B34E00102D8C /* NoCoursesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCoursesView.swift; sourceTree = ""; }; + 02935B6E2BCEC91100B22F66 /* LearnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnView.swift; sourceTree = ""; }; + 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnViewModel.swift; sourceTree = ""; }; + 02935B762BCFB2C100B22F66 /* CourseCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCardView.swift; sourceTree = ""; }; 02A48B17295ACE200033D5E0 /* DashboardCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DashboardCoreModel.xcdatamodel; sourceTree = ""; }; 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistenceProtocol.swift; sourceTree = ""; }; + 02A98F102BD96814009B46BD /* DropDownMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownMenu.swift; sourceTree = ""; }; + 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCoursesView.swift; sourceTree = ""; }; + 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCoursesViewModel.swift; sourceTree = ""; }; + 02A98F162BD97885009B46BD /* CategoryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFilterView.swift; sourceTree = ""; }; 02A9A9082978194100B55797 /* DashboardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DashboardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModelTests.swift; sourceTree = ""; }; 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardMock.generated.swift; sourceTree = ""; }; @@ -109,6 +129,19 @@ path = Persistence; sourceTree = ""; }; + 0277241C2BCE9DF300C2908D /* Elements */ = { + isa = PBXGroup; + children = ( + 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */, + 02A98F102BD96814009B46BD /* DropDownMenu.swift */, + 02A98F162BD97885009B46BD /* CategoryFilterView.swift */, + 02935B762BCFB2C100B22F66 /* CourseCardView.swift */, + 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */, + 028895662BE3B34E00102D8C /* NoCoursesView.swift */, + ); + path = Elements; + sourceTree = ""; + }; 027DB33628D8D851002B6862 /* Domain */ = { isa = PBXGroup; children = ( @@ -181,6 +214,11 @@ 02F6EF3F28D9ECA200835477 /* Presentation */ = { isa = PBXGroup; children = ( + 0277241C2BCE9DF300C2908D /* Elements */, + 02935B6E2BCEC91100B22F66 /* LearnView.swift */, + 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */, + 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */, + 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */, 027DB33228D8BDBA002B6862 /* DashboardView.swift */, 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */, 02F3BFE029252FCB0051930C /* DashboardRouter.swift */, @@ -455,14 +493,24 @@ buildActionMask = 2147483647; files = ( 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */, + 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */, + 02935B6F2BCEC91100B22F66 /* LearnView.swift in Sources */, + 02A98F172BD97886009B46BD /* CategoryFilterView.swift in Sources */, 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */, + 02935B712BCEC91F00B22F66 /* LearnViewModel.swift in Sources */, + 02A98F112BD96814009B46BD /* DropDownMenu.swift in Sources */, 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */, + 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */, 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */, 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */, + 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */, 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */, + 02A98F152BD96BC2009B46BD /* AllCoursesViewModel.swift in Sources */, + 02A98F132BD96B9D009B46BD /* AllCoursesView.swift in Sources */, 02F3BFE129252FCB0051930C /* DashboardRouter.swift in Sources */, 027DB33928D8D9F6002B6862 /* DashboardEndpoint.swift in Sources */, 02F6EF4828D9ED8300835477 /* Strings.swift in Sources */, + 027724202BCEA16C00C2908D /* ProgressLineView.swift in Sources */, 97E7DF0B2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift in Sources */, 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */, ); diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index 65816872f..7dd326257 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -11,6 +11,10 @@ import Core public protocol DashboardRepositoryProtocol { func getMyCourses(page: Int) async throws -> [CourseItem] func getMyCoursesOffline() throws -> [CourseItem] + func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments + func getMyLearnCoursesOffline() async throws -> MyEnrollments + func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments + func getAllCoursesOffline() async throws -> MyEnrollments } public class DashboardRepository: DashboardRepositoryProtocol { @@ -42,6 +46,41 @@ public class DashboardRepository: DashboardRepositoryProtocol { return try persistence.loadMyCourses() } + public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { + let result = try await api.requestData( + DashboardEndpoint.getMyLearnCourses( + username: storage.user?.username ?? "", + pageSize: pageSize + ) + ) + .mapResponse(DataLayer.MyLearnCourses.self) + .domain(baseURL: config.baseURL.absoluteString) + persistence.saveMyEnrollments(enrollments: result) + return result + } + + public func getMyLearnCoursesOffline() async throws -> MyEnrollments { + return try persistence.loadMyEnrollments() + } + + public func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments { + let result = try await api.requestData( + DashboardEndpoint.getAllCourses( + username: storage.user?.username ?? "", + filteredBy: filteredBy, + page: page + ) + ) + .mapResponse(DataLayer.MyLearnCourses.self) + .domain(baseURL: config.baseURL.absoluteString) +// persistence.saveMyCourses(items: result.courses) + return result + } + + public func getAllCoursesOffline() async throws -> MyEnrollments { +// let courses = try persistence.loadMyCourses() + return MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + } } // Mark - For testing and SwiftUI preview @@ -75,7 +114,9 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { enrollmentEnd: nil, courseID: "course_id_\(i)", numPages: 1, - coursesCount: 0 + coursesCount: 0, + progressEarned: 0, + progressPossible: 0 ) ) } @@ -83,5 +124,91 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { } func getMyCoursesOffline() throws -> [CourseItem] { return [] } + + public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { + + var courses: [CourseItem] = [] + for i in 0...10 { + courses.append( + CourseItem( + name: "Course name \(i)", + org: "Organization", + shortDescription: "shortDescription", + imageURL: "", + isActive: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "course_id_\(i)", + numPages: 1, + coursesCount: 0, + progressEarned: 4, + progressPossible: 10 + ) + ) + } + + let futureAssignment = Assignment( + type: "Final Exam", + title: "Subsection 3", + description: "", + date: Date(), + complete: false, + firstComponentBlockId: nil + ) + + let primaryCourse = PrimaryCourse( + name: "Primary Course", + org: "Organization", + courseID: "123", + isActive: true, + courseStart: Date(), + courseEnd: Date(), + courseBanner: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + futureAssignments: [futureAssignment], + pastAssignments: [futureAssignment], + progressEarned: 2, + progressPossible: 5, + lastVisitedBlockID: nil, + resumeTitle: nil + ) + return MyEnrollments(primaryCourse: primaryCourse, courses: courses, totalPages: 1, count: 1) + } + + func getMyLearnCoursesOffline() async throws -> Core.MyEnrollments { + Core.MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + } + + + func getAllCourses(filteredBy: String, page: Int) async throws -> Core.MyEnrollments { + var courses: [CourseItem] = [] + for i in 0...10 { + courses.append( + CourseItem( + name: "Course name \(i)", + org: "Organization", + shortDescription: "shortDescription", + imageURL: "", + isActive: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "course_id_\(i)", + numPages: 1, + coursesCount: 0, + progressEarned: 4, + progressPossible: 10 + ) + ) + } + + return MyEnrollments(primaryCourse: nil, courses: courses, totalPages: 1, count: 1) + } + + func getAllCoursesOffline() async throws -> Core.MyEnrollments { + Core.MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + } } #endif diff --git a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift index 1d6845214..8848354d3 100644 --- a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift +++ b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift @@ -11,17 +11,23 @@ import Alamofire enum DashboardEndpoint: EndPointType { case getMyCourses(username: String, page: Int) + case getMyLearnCourses(username: String, pageSize: Int) + case getAllCourses(username: String, filteredBy: String, page: Int) var path: String { switch self { case let .getMyCourses(username, _): return "/api/mobile/v3/users/\(username)/course_enrollments" + case let .getMyLearnCourses(username, _): + return "/api/mobile/v4/users/\(username)/course_enrollments" + case let .getAllCourses(username, _, _): + return "/api/mobile/v4/users/\(username)/course_enrollments" } } var httpMethod: HTTPMethod { switch self { - case .getMyCourses: + case .getMyCourses, .getMyLearnCourses, .getAllCourses: return .get } } @@ -37,6 +43,21 @@ enum DashboardEndpoint: EndPointType { "page": page ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case let .getMyLearnCourses(_, pageSize): + let params: Parameters = [ + "page_size": pageSize + ] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case let .getAllCourses(_, filteredBy, page): + let params: Parameters = [ + "page_size": 10, + "status": filteredBy, + "requested_fields": "course_progress", + "page": page + ] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) } } } diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents index eeee515fe..5d055d271 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents +++ b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents @@ -1,5 +1,18 @@ - + + + + + + + + + + + + + + @@ -10,9 +23,12 @@ + + + @@ -20,4 +36,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift index 14bad2aaa..cf6fe2495 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift @@ -11,6 +11,8 @@ import Core public protocol DashboardPersistenceProtocol { func loadMyCourses() throws -> [CourseItem] func saveMyCourses(items: [CourseItem]) + func loadMyEnrollments() throws -> MyEnrollments + func saveMyEnrollments(enrollments: MyEnrollments) } public final class DashboardBundle { diff --git a/Dashboard/Dashboard/Domain/DashboardInteractor.swift b/Dashboard/Dashboard/Domain/DashboardInteractor.swift index 8e84d847b..dea12037f 100644 --- a/Dashboard/Dashboard/Domain/DashboardInteractor.swift +++ b/Dashboard/Dashboard/Domain/DashboardInteractor.swift @@ -12,6 +12,10 @@ import Core public protocol DashboardInteractorProtocol { func getMyCourses(page: Int) async throws -> [CourseItem] func discoveryOffline() throws -> [CourseItem] + func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments + func getMyLearnCoursesOffline() async throws -> MyEnrollments + func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments + func getAllCoursesOffline() async throws -> MyEnrollments } public class DashboardInteractor: DashboardInteractorProtocol { @@ -30,6 +34,22 @@ public class DashboardInteractor: DashboardInteractorProtocol { public func discoveryOffline() throws -> [CourseItem] { return try repository.getMyCoursesOffline() } + + public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { + return try await repository.getMyLearnCourses(pageSize: pageSize) + } + + public func getMyLearnCoursesOffline() async throws -> MyEnrollments { + return try await repository.getMyLearnCoursesOffline() + } + + public func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments { + return try await repository.getAllCourses(filteredBy: filteredBy, page: page) + } + + public func getAllCoursesOffline() async throws -> MyEnrollments { + return try await repository.getAllCoursesOffline() + } } // Mark - For testing and SwiftUI preview diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift new file mode 100644 index 000000000..dd16616b9 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -0,0 +1,211 @@ +// +// AllCoursesView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import SwiftUI +import Core +import Theme + +public struct AllCoursesView: View { + + @StateObject + private var viewModel: AllCoursesViewModel + private let router: DashboardRouter + @Environment (\.isHorizontal) private var isHorizontal + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + public init(viewModel: AllCoursesViewModel, router: DashboardRouter) { + self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.router = router + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + VStack { + BackNavigationButton( + color: Theme.Colors.textPrimary, + action: { + router.back() + } + ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 9) + .padding(.top, 13) + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + .zIndex(1) + + if let myEnrollments = viewModel.myEnrollments, + myEnrollments.courses.isEmpty, + !viewModel.fetchInProgress { + NoCoursesView(selectedMenu: viewModel.selectedMenu) + } + // MARK: - Page body + VStack(alignment: .center) { + RefreshableScrollViewCompat(action: { + await viewModel.getCourses(page: 1, refresh: true) + }) { + learnTitleAndSearch() + CategoryFilterView(selectedOption: $viewModel.selectedMenu) + .disabled(viewModel.fetchInProgress) + if let myEnrollments = viewModel.myEnrollments { + LazyVGrid(columns: columns(), spacing: 15) { + ForEach( + Array(myEnrollments.courses.enumerated()), + id: \.offset + ) { index, course in + Button(action: { + viewModel.trackDashboardCourseClicked( + courseID: course.courseID, + courseName: course.name + ) + router.showCourseScreens( + courseID: course.courseID, + isActive: course.isActive, + courseStart: course.courseStart, + courseEnd: course.courseEnd, + enrollmentStart: course.enrollmentStart, + enrollmentEnd: course.enrollmentEnd, + title: course.name, + selection: .course, + lastVisitedBlockID: nil + ) + }, label: { + CourseCardView( + courseName: course.name, + courseImage: course.imageURL, + progressEarned: course.progressEarned, + progressPossible: course.progressPossible, + courseStartDate: course.courseStart, + courseEndDate: course.courseEnd, + isActive: course.isActive, + isFullCard: false + ).padding(8) + }) + .accessibilityIdentifier("course_item") + .onAppear { + Task { + await viewModel.getMyCoursesPagination(index: index) + } + } + } + } + .padding(10) + .frameLimit(width: proxy.size.width) + } + // MARK: - ProgressBar + if viewModel.nextPage <= viewModel.totalPages { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 20) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) + } + VStack {}.frame(height: 40) + } + .accessibilityAction {} + } + .padding(.top, 8) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView(connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getCourses(page: 1, refresh: true) + }) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .onFirstAppear { + Task { + await viewModel.getCourses(page: 1) + } + } + .onChange(of: viewModel.selectedMenu) { _ in + Task { + await viewModel.getCourses(page: 1, refresh: true) + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + .navigationTitle(DashboardLocalization.Learn.allCourses) + } + } + + private func columns() -> [GridItem] { + isHorizontal || idiom == .pad + ? [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + : [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + } + + private func learnTitleAndSearch() -> some View { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.allCourses) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("all_courses_header_text") + Spacer() + Button(action: { + router.showDiscoverySearch(searchQuery: "") + }, label: { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier(DashboardLocalization.search) + }) + } + .padding(.horizontal, 20) + .padding(.vertical, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) + } +} + +#if DEBUG +struct AllCoursesView_Previews: PreviewProvider { + static var previews: some View { + let vm = AllCoursesViewModel( + interactor: DashboardInteractor.mock, + connectivity: Connectivity(), + analytics: DashboardAnalyticsMock() + ) + + AllCoursesView(viewModel: vm, router: DashboardRouterMock()) + .preferredColorScheme(.light) + .previewDisplayName("DashboardView Light") + + AllCoursesView(viewModel: vm, router: DashboardRouterMock()) + .preferredColorScheme(.dark) + .previewDisplayName("DashboardView Dark") + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift new file mode 100644 index 000000000..54d180bff --- /dev/null +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -0,0 +1,106 @@ +// +// AllCoursesViewModel.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Foundation +import Core +import SwiftUI +import Combine + +public class AllCoursesViewModel: ObservableObject { + + public var nextPage = 1 + public var totalPages = 1 + @Published public private(set) var fetchInProgress = false + @Published var selectedMenu: CategoryOption = .all + + @Published var myEnrollments: MyEnrollments? + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let connectivity: ConnectivityProtocol + private let interactor: DashboardInteractorProtocol + private let analytics: DashboardAnalytics + private var onCourseEnrolledCancellable: AnyCancellable? + + public init(interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics) { + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + + onCourseEnrolledCancellable = NotificationCenter.default + .publisher(for: .onCourseEnrolled) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getCourses(page: 1, refresh: true) + } + } + } + + @MainActor + public func getCourses(page: Int, refresh: Bool = false) async { + fetchInProgress = true + do { + if connectivity.isInternetAvaliable { + if refresh || page == 1 { + myEnrollments?.courses = [] + nextPage = 1 + myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) + self.totalPages = myEnrollments?.totalPages ?? 1 + self.nextPage = 2 + } else { + myEnrollments?.courses += try await interactor.getAllCourses( + filteredBy: selectedMenu.status, page: page + ).courses + self.nextPage += 1 + } + totalPages = myEnrollments?.totalPages ?? 1 + fetchInProgress = false + } else { + self.totalPages = 1 + self.nextPage = 2 + myEnrollments = try await interactor.getAllCoursesOffline() + fetchInProgress = false + } + } catch let error { + fetchInProgress = false + if error is NoCachedDataError { + errorMessage = CoreLocalization.Error.noCachedData + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + @MainActor + public func getMyCoursesPagination(index: Int) async { + guard let courses = myEnrollments?.courses else { return } + if !fetchInProgress { + if totalPages > 1 { + if index == courses.count - 3 { + if totalPages != 1 { + if nextPage <= totalPages { + await getCourses(page: self.nextPage) + } + } + } + } + } + } + + func trackDashboardCourseClicked(courseID: String, courseName: String) { + analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) + } +} diff --git a/Dashboard/Dashboard/Presentation/DashboardRouter.swift b/Dashboard/Dashboard/Presentation/DashboardRouter.swift index 40bc86c41..7d548e016 100644 --- a/Dashboard/Dashboard/Presentation/DashboardRouter.swift +++ b/Dashboard/Dashboard/Presentation/DashboardRouter.swift @@ -16,7 +16,13 @@ public protocol DashboardRouter: BaseRouter { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String) + title: String, + selection: CourseTab, + lastVisitedBlockID: String?) + + func showAllCourses(courses: [CourseItem]) + + func showDiscoverySearch(searchQuery: String?) } @@ -32,7 +38,13 @@ public class DashboardRouterMock: BaseRouterMock, DashboardRouter { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String) {} + title: String, + selection: CourseTab, + lastVisitedBlockID: String?) {} + + public func showAllCourses(courses: [CourseItem]) {} + + public func showDiscoverySearch(searchQuery: String?) {} } #endif diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index 44c6c3fc8..4b9f00833 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -81,7 +81,9 @@ public struct DashboardView: View { courseEnd: course.courseEnd, enrollmentStart: course.enrollmentStart, enrollmentEnd: course.enrollmentEnd, - title: course.name + title: course.name, + selection: .course, + lastVisitedBlockID: nil ) } .accessibilityIdentifier("course_item") diff --git a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift new file mode 100644 index 000000000..8c4142ee3 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift @@ -0,0 +1,84 @@ +// +// CategoryFilterView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Theme +import Core +import SwiftUI + +enum CategoryOption: String, CaseIterable { + case all + case inProgress + case completed + case expired + + var status: String { + switch self { + case .all: + "all" + case .inProgress: + "in_progress" + case .completed: + "completed" + case .expired: + "expired" + } + } + + var text: String { + switch self { + case .all: + DashboardLocalization.Learn.Category.all + case .inProgress: + DashboardLocalization.Learn.Category.inProgress + case .completed: + DashboardLocalization.Learn.Category.completed + case .expired: + DashboardLocalization.Learn.Category.expired + } + } +} + +struct CategoryFilterView: View { + @Binding var selectedOption: CategoryOption + + var body: some View { + ScrollView(.horizontal) { + HStack(spacing: 8) { + ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { index, option in + Button(action: { + selectedOption = option + }, label: { + HStack { + Text(option.text) + .font(Theme.Fonts.titleSmall) + .foregroundColor( + option == selectedOption ? Theme.Colors.white : Theme.Colors.accentColor + ) + } + .padding(.horizontal, 17) + .padding(.vertical, 8) + .background { + ZStack { + RoundedRectangle(cornerRadius: 20) + .foregroundStyle( + option == selectedOption + ? Theme.Colors.accentColor + : Theme.Colors.background + ) + RoundedRectangle(cornerRadius: 20) + .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + } + .padding(.vertical, 1) + } + }) + .padding(.leading, index == 0 ? 16 : 0) + } + } + .fixedSize() + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift new file mode 100644 index 000000000..2cfa1bd53 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -0,0 +1,125 @@ +// +// CourseCardView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 17.04.2024. +// + +import SwiftUI +import Theme +import Kingfisher +import Core + +struct CourseCardView: View { + + private let courseName: String + private let courseImage: String + private let progressEarned: Double + private let progressPossible: Double + private let courseStartDate: Date? + private let courseEndDate: Date? + private let isActive: Bool + private let isFullCard: Bool + + init( + courseName: String, + courseImage: String, + progressEarned: Double, + progressPossible: Double, + courseStartDate: Date?, + courseEndDate: Date?, + isActive: Bool, + isFullCard: Bool + ) { + self.courseName = courseName + self.courseImage = courseImage + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.courseStartDate = courseStartDate + self.courseEndDate = courseEndDate + self.isActive = isActive + self.isFullCard = isFullCard + } + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 0) { + courseBanner + if isFullCard { + ProgressLineView( + progressEarned: progressEarned, + progressPossible: progressPossible, + height: 4 + ) + } + courseTitle + } + if !isActive { + ZStack(alignment: .center) { + Circle() + .foregroundStyle(Theme.Colors.primaryHeaderColor) + .opacity(0.7) + .frame(width: 24, height: 24) + CoreAssets.lockIcon.swiftUIImage + .foregroundStyle(Theme.Colors.textPrimary) + } + .padding(8) + } + } + .background(Theme.Colors.background) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) + } + + private var courseBanner: some View { + return KFImage(URL(string: courseImage)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(minWidth: 120, minHeight: 90, maxHeight: 100) + .clipped() + .accessibilityElement(children: .ignore) + .accessibilityIdentifier("course_image") + } + + private var courseTitle: some View { + VStack(alignment: .leading, spacing: 3) { + if let courseEndDate { + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear)) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textSecondaryLight) + .multilineTextAlignment(.leading) + } else if let courseStartDate { + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear)) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textSecondaryLight) + .multilineTextAlignment(.leading) + } + Text(courseName) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + .frame(height: isFullCard ? 51 : 40, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 10) + .padding(.horizontal, 12) + .padding(.bottom, 16) + } +} + +#if DEBUG +#Preview { + CourseCardView( + courseName: "Six Sigma Part 2: Analyze, Improve, Control", + courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + progressEarned: 4, + progressPossible: 8, + courseStartDate: nil, + courseEndDate: Date(), + isActive: true, + isFullCard: true + ).frame(width: 170) +} +#endif diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift new file mode 100644 index 000000000..6253201de --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -0,0 +1,81 @@ +// +// DropDownMenu.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Theme +import Core +import SwiftUI + +enum MenuOption: String, CaseIterable { + case courses + case programs + + var text: String { + switch self { + case .courses: + DashboardLocalization.Learn.DropdownMenu.courses + case .programs: + DashboardLocalization.Learn.DropdownMenu.programs + } + } +} + +struct DropDownMenu: View { + @Binding var selectedOption: MenuOption + @State private var expanded: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(selectedOption.text) + .font(Theme.Fonts.titleSmall) + .accessibilityIdentifier("dropdown_menu_text") + Image(systemName: expanded ? "chevron.up" : "chevron.down") + } + .foregroundColor(Theme.Colors.textPrimary) + .onTapGesture { + withAnimation(.snappy(duration: 0.2)) { + expanded.toggle() + } + } + + if expanded { + VStack(spacing: 0) { + ForEach(Array(MenuOption.allCases.enumerated()), id: \.offset) { index, option in + Button(action: { + selectedOption = option + expanded = false + }, label: { + HStack { + Text(option.text) + .font(Theme.Fonts.titleSmall) + .foregroundColor( + option == selectedOption ? Theme.Colors.white : Theme.Colors.textPrimary + ) + Spacer() + } + .padding(10) + .background { + ZStack { + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .foregroundStyle(option == selectedOption + ? Theme.Colors.accentColor + : Theme.Colors.background) + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + } + } + }) + } + } + .frame(minWidth: 182) + .fixedSize() + } + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift new file mode 100644 index 000000000..4b2620299 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift @@ -0,0 +1,94 @@ +// +// NoCoursesView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 02.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct NoCoursesView: View { + + enum NoCoursesType { + case primary + case inProgress + case completed + case expired + + var title: String { + switch self { + case .primary: + DashboardLocalization.Learn.NoCoursesView.noCourses + case .inProgress: + DashboardLocalization.Learn.NoCoursesView.noCoursesInProgress + case .completed: + DashboardLocalization.Learn.NoCoursesView.noCompletedCourses + case .expired: + DashboardLocalization.Learn.NoCoursesView.noExpiredCourses + } + } + + var description: String? { + switch self { + case .primary: + DashboardLocalization.Learn.NoCoursesView.noCoursesDescription + case .inProgress: + nil + case .completed: + nil + case .expired: + nil + } + } + } + + private let type: NoCoursesType + private var openDiscovery: (() -> Void) + + init(openDiscovery: @escaping (() -> Void)) { + self.type = .primary + self.openDiscovery = openDiscovery + } + + init(selectedMenu: CategoryOption) { + switch selectedMenu { + case .all: + self.type = .inProgress + case .inProgress: + self.type = .inProgress + case .completed: + self.type = .completed + case .expired: + self.type = .expired + } + self.openDiscovery = {} + } + + var body: some View { + VStack(spacing: 8) { + Spacer() + CoreAssets.learnBig.swiftUIImage + .resizable() + .frame(width: 96, height: 96) + .foregroundStyle(Theme.Colors.textSecondaryLight) + Text(type.title) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.titleMedium) + if let description = type.description { + Text(description) + .multilineTextAlignment(.center) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.labelMedium) + .frame(width: 245) + } + Spacer() + if type == .primary { + StyledButton(DashboardLocalization.Learn.NoCoursesView.findACourse, action: { + openDiscovery() + }).padding(24) + } + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift new file mode 100644 index 000000000..679eb70bc --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -0,0 +1,275 @@ +// +// PrimaryCardView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Kingfisher +import Theme +import Core + +public struct PrimaryCardView: View { + + private let courseName: String + private let org: String + private let courseImage: String + private let courseStartDate: Date? + private let courseEndDate: Date? + private var futureAssignments: [Assignment]? + private let pastAssignments: [Assignment]? + private let progressEarned: Double + private let progressPossible: Double + private let canResume: Bool + private let resumeTitle: String? + private var pastAssignmentAction: (String?) -> Void + private var futureAssignmentAction: (String?) -> Void + private var resumeAction: () -> Void + + public init( + courseName: String, + org: String, + courseImage: String, + courseStartDate: Date?, + courseEndDate: Date?, + futureAssignments: [Assignment]?, + pastAssignments: [Assignment]?, + progressEarned: Double, + progressPossible: Double, + canResume: Bool, + resumeTitle: String?, + pastAssignmentAction: @escaping (String?) -> Void, + futureAssignmentAction: @escaping (String?) -> Void, + resumeAction: @escaping () -> Void + ) { + self.courseName = courseName + self.org = org + self.courseImage = courseImage + self.courseStartDate = courseStartDate + self.courseEndDate = courseEndDate + self.futureAssignments = futureAssignments + self.pastAssignments = pastAssignments + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.canResume = canResume + self.resumeTitle = resumeTitle + self.pastAssignmentAction = pastAssignmentAction + self.futureAssignmentAction = futureAssignmentAction + self.resumeAction = resumeAction + } + + public var body: some View { + ZStack { + VStack(alignment: .leading, spacing: 0) { + courseBanner + ProgressLineView(progressEarned: progressEarned, progressPossible: progressPossible) + courseTitle + assignments + } + } + .background(Theme.Colors.background) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 4, x: 0, y: 3) + .padding(20) + } + + private var assignments: some View { + VStack(alignment: .leading, spacing: 8) { + // pastAssignments + if let pastAssignments = pastAssignments { + if pastAssignments.count == 1, let pastAssignment = pastAssignments.first { + courseButton( + title: pastAssignment.title, + description: DashboardLocalization.Learn.PrimaryCard.onePastAssignment, + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { pastAssignmentAction(pastAssignments.first?.firstComponentBlockId) } + ) + } else if pastAssignments.count > 1 { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.viewAssignments, + description: DashboardLocalization.Learn.PrimaryCard.pastAssignments(pastAssignments.count), + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { pastAssignmentAction(nil) } + ) + } + } + + // futureAssignment + if let futureAssignments, !futureAssignments.isEmpty { + if futureAssignments.count == 1, let futureAssignment = futureAssignments.first { + let daysRemaining = Calendar.current.dateComponents( + [.day], + from: Date(), + to: futureAssignment.date + ).day ?? 0 + courseButton( + title: futureAssignment.title, + description: DashboardLocalization.Learn.PrimaryCard.dueDays( + futureAssignment.type, + daysRemaining + ), + icon: CoreAssets.chapter.swiftUIImage, + selected: false, + action: { + futureAssignmentAction(futureAssignments.first?.firstComponentBlockId) + } + ) + } else if futureAssignments.count > 1 { + if let firtsData = futureAssignments.sorted(by: { $0.date < $1.date }).first { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.futureAssignments( + futureAssignments.count, + firtsData.date.dateToString(style: .lastPost) + ), + description: nil, + icon: CoreAssets.chapter.swiftUIImage, + selected: false, + action: { + futureAssignmentAction(nil) + } + ) + } + } + } + + // ResumeButton + if canResume { + courseButton( + title: resumeTitle ?? "", + description: DashboardLocalization.Learn.PrimaryCard.resume, + icon: CoreAssets.resumeCourse.swiftUIImage, + selected: true, + action: { resumeAction() } + ) + } else { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.startCourse, + description: nil, + icon: CoreAssets.resumeCourse.swiftUIImage, + selected: true, + action: { resumeAction() } + ) + } + } + } + + private func courseButton( + title: String, + description: String?, + icon: Image, + selected: Bool, + action: @escaping () -> Void + ) -> some View { + + Button(action: { + action() + }, label: { + ZStack(alignment: .top) { + Rectangle().frame(height: 1) + .foregroundStyle(Theme.Colors.cardViewStroke) + HStack(alignment: .center) { + VStack(alignment: .leading) { + HStack(spacing: 0) { + icon + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundStyle( + selected ? Theme.Colors.white : Theme.Colors.textPrimary + ) + .padding(12) + + VStack(alignment: .leading, spacing: 6) { + if let description { + Text(description) + .font(Theme.Fonts.labelSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .foregroundStyle(selected ? Theme.Colors.white : Theme.Colors.textPrimary) + } + Text(title) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .foregroundStyle(selected ? Theme.Colors.white : Theme.Colors.textPrimary) + } + .padding(.top, 2) + } + } + Spacer() + CoreAssets.chevronRight.swiftUIImage + .foregroundStyle(selected ? Theme.Colors.white : Theme.Colors.textPrimary) + .padding(8) + } + .padding(.top, 8) + .padding(.bottom, selected ? 10 : 0) + }.background(selected ? Theme.Colors.accentColor : .clear) + }) + } + + private var courseBanner: some View { + return KFImage(URL(string: courseImage)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 140) + .clipped() + .allowsHitTesting(false) + .accessibilityElement(children: .ignore) + .accessibilityIdentifier("course_image") + } + + private var courseTitle: some View { + VStack(alignment: .leading, spacing: 3) { + Text(org) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + Text(courseName) + .font(Theme.Fonts.titleLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .lineLimit(3) + if let courseEndDate { + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear)) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + } else if let courseStartDate { + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear)) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + } + } + .padding(.top, 10) + .padding(.horizontal, 12) + .padding(.bottom, 16) + } +} + +#if DEBUG +struct PrimaryCardView_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Theme.Colors.background + PrimaryCardView( + courseName: "Course Title", + org: "Organization", + courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + courseStartDate: nil, + courseEndDate: Date(), + futureAssignments: nil, + pastAssignments: nil, + progressEarned: 10, + progressPossible: 45, + canResume: true, + resumeTitle: "Course Chapter 1", + pastAssignmentAction: {_ in }, + futureAssignmentAction: {_ in }, + resumeAction: {} + ) + .loadFonts() + } + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift new file mode 100644 index 000000000..b124e908c --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -0,0 +1,50 @@ +// +// ProgressLineView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Theme + +struct ProgressLineView: View { + private let progressEarned: Double + private let progressPossible: Double + private let height: CGFloat + + var progressValue: CGFloat { + guard progressPossible != 0 else { return 0 } + return CGFloat(progressEarned) / CGFloat(progressPossible) + } + + init(progressEarned: Double, progressPossible: Double, height: CGFloat = 8) { + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.height = height + } + + var body: some View { + ZStack(alignment: .leading) { + if progressPossible != 0 { + GeometryReader { geometry in + Rectangle() + .foregroundStyle(Theme.Colors.cardViewStroke) + Rectangle() + .foregroundStyle(Theme.Colors.accentColor) + .frame(width: geometry.size.width * progressValue) + }.frame(height: height) + } + } + } +} + +#if DEBUG +struct ProgressLineView_Previews: PreviewProvider { + static var previews: some View { + ProgressLineView(progressEarned: 4, progressPossible: 6) + .frame(height: 8) + .padding() + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/LearnView.swift b/Dashboard/Dashboard/Presentation/LearnView.swift new file mode 100644 index 000000000..b2576d445 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/LearnView.swift @@ -0,0 +1,309 @@ +// +// LearnView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Core +import Theme +//import Discovery +import Swinject + +public struct LearnView: View { + + @StateObject + private var viewModel: LearnViewModel + private let router: DashboardRouter + private let config = Container.shared.resolve(ConfigProtocol.self) + @ViewBuilder let programView: ProgramView + private var openDiscoveryPage: () -> Void + + @State private var selectedMenu: MenuOption = .courses + + public init( + viewModel: LearnViewModel, + router: DashboardRouter, + programView: ProgramView, + openDiscoveryPage: @escaping () -> Void + ) { + self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.router = router + self.programView = programView + self.openDiscoveryPage = openDiscoveryPage + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + if !viewModel.fetchInProgress, viewModel.myEnrollments?.primaryCourse == nil { + NoCoursesView(openDiscovery: { + openDiscoveryPage() + }).zIndex(1) + } + // MARK: - Page body + VStack(alignment: .center) { + RefreshableScrollViewCompat(action: { + await viewModel.getMyLearnings(showProgress: false) + }) { + ZStack(alignment: .topLeading) { + learnTitleAndSearch() + .zIndex(1) + if !viewModel.fetchInProgress, viewModel.myEnrollments?.primaryCourse == nil { + + } else if viewModel.fetchInProgress { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) + } else { + LazyVStack(spacing: 0) { + Spacer(minLength: 50) + switch selectedMenu { + case .courses: + if let myEnrollments = viewModel.myEnrollments { + if let primary = myEnrollments.primaryCourse { + PrimaryCardView( + courseName: primary.name, + org: primary.org, + courseImage: primary.courseBanner, + courseStartDate: primary.courseStart, + courseEndDate: primary.courseEnd, + futureAssignments: primary.futureAssignments, + pastAssignments: primary.pastAssignments, + progressEarned: primary.progressEarned ?? 0, + progressPossible: primary.progressPossible ?? 0, + canResume: primary.lastVisitedBlockID != nil, + resumeTitle: primary.resumeTitle, + pastAssignmentAction: { lastVisitedBlockID in + router.showCourseScreens( + courseID: primary.courseID, + isActive: primary.isActive, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + selection: lastVisitedBlockID == nil ? .dates : .course, + lastVisitedBlockID: lastVisitedBlockID + ) + }, + futureAssignmentAction: { lastVisitedBlockID in + router.showCourseScreens( + courseID: primary.courseID, + isActive: primary.isActive, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + selection: lastVisitedBlockID == nil ? .dates : .course, + lastVisitedBlockID: lastVisitedBlockID + ) + }, + resumeAction: { + router.showCourseScreens( + courseID: primary.courseID, + isActive: primary.isActive, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + selection: .course, + lastVisitedBlockID: primary.lastVisitedBlockID + ) + } + ) + } + if !myEnrollments.courses.isEmpty { + viewAll(myEnrollments) + } + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach( + Array(myEnrollments.courses.enumerated()), + id: \.offset + ) { _, course in + Button(action: { + viewModel.trackDashboardCourseClicked( + courseID: course.courseID, + courseName: course.name + ) + router.showCourseScreens( + courseID: course.courseID, + isActive: course.isActive, + courseStart: course.courseStart, + courseEnd: course.courseEnd, + enrollmentStart: course.enrollmentStart, + enrollmentEnd: course.enrollmentEnd, + title: course.name, + selection: .course, + lastVisitedBlockID: nil + ) + }, label: { + CourseCardView( + courseName: course.name, + courseImage: course.imageURL, + progressEarned: 0, + progressPossible: 0, + courseStartDate: nil, + courseEndDate: nil, + isActive: course.isActive, + isFullCard: false + ).frame(width: 120) + } + ) + .accessibilityIdentifier("course_item") + } + if myEnrollments.courses.count < myEnrollments.count { + viewAllButton(myEnrollments) + } + } + .padding(20) + } + } else { + EmptyPageIcon() + } + case .programs: + programView + } + } + } + } + .frameLimit(width: proxy.size.width) + }.accessibilityAction {} + }.padding(.top, 8) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView(connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getMyLearnings(showProgress: false) + }) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .onFirstAppear { + Task { + await viewModel.getMyLearnings() + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + .navigationTitle(DashboardLocalization.title) + } + } + + private func viewAllButton(_ myEnrollments: MyEnrollments) -> some View { + Button(action: { + router.showAllCourses(courses: myEnrollments.courses) + }, label: { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 0) { + Spacer() + CoreAssets.viewAll.swiftUIImage + Text(DashboardLocalization.Learn.viewAll) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + Spacer() + } + .frame(width: 120) + } + .background(Theme.Colors.background) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) + + }) + } + + private func viewAll(_ myEnrollments: MyEnrollments) -> some View { + Button(action: { + router.showAllCourses(courses: myEnrollments.courses) + }, label: { + HStack { + Text(DashboardLocalization.Learn.viewAllCourses(myEnrollments.count)) + .font(Theme.Fonts.titleSmall) + .accessibilityIdentifier("courses_welcomeback_text") + Image(systemName: "chevron.right") + } + .padding(.horizontal, 16) + .foregroundColor(Theme.Colors.textPrimary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + }) + } + + private func learnTitleAndSearch() -> some View { + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.title) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("courses_header_text") + Spacer() + Button(action: { + router.showDiscoverySearch(searchQuery: "") + }, label: { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier(DashboardLocalization.search) + }) + } + if let config, config.program.enabled, config.program.isWebViewConfigured { + DropDownMenu(selectedOption: $selectedMenu) + } + } + .listRowBackground(Color.clear) + .padding(.horizontal, 20) + .padding(.bottom, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) + } +} + +#if DEBUG +struct LearnView_Previews: PreviewProvider { + static var previews: some View { + let vm = LearnViewModel( + interactor: DashboardInteractor.mock, + connectivity: Connectivity(), + analytics: DashboardAnalyticsMock() + ) + + LearnView(viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + }) + .preferredColorScheme(.light) + .previewDisplayName("DashboardView Light") + + LearnView(viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + }) + .preferredColorScheme(.dark) + .previewDisplayName("DashboardView Dark") + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/LearnViewModel.swift b/Dashboard/Dashboard/Presentation/LearnViewModel.swift new file mode 100644 index 000000000..17a1ba1b3 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/LearnViewModel.swift @@ -0,0 +1,76 @@ +// +// LearnViewModel.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import Foundation +import Core +import SwiftUI +import Combine + +public class LearnViewModel: ObservableObject { + + public var nextPage = 1 + public var totalPages = 1 + @Published public private(set) var fetchInProgress = false + + @Published var myEnrollments: MyEnrollments? + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let connectivity: ConnectivityProtocol + private let interactor: DashboardInteractorProtocol + private let analytics: DashboardAnalytics + private var onCourseEnrolledCancellable: AnyCancellable? + + public init(interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics) { + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + + onCourseEnrolledCancellable = NotificationCenter.default + .publisher(for: .onCourseEnrolled) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getMyLearnings() + } + } + } + + @MainActor + public func getMyLearnings(showProgress: Bool = true) async { + let pageSize = UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5 + fetchInProgress = showProgress + do { + if connectivity.isInternetAvaliable { + myEnrollments = try await interactor.getMyLearnCourses(pageSize: pageSize) + fetchInProgress = false + } else { + myEnrollments = try await interactor.getMyLearnCoursesOffline() + fetchInProgress = false + } + } catch let error { + fetchInProgress = false + if error is NoCachedDataError { + errorMessage = CoreLocalization.Error.noCachedData + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + func trackDashboardCourseClicked(courseID: String, courseName: String) { + analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) + } +} diff --git a/Dashboard/Dashboard/SwiftGen/Strings.swift b/Dashboard/Dashboard/SwiftGen/Strings.swift index aac74931c..e6cc9564c 100644 --- a/Dashboard/Dashboard/SwiftGen/Strings.swift +++ b/Dashboard/Dashboard/SwiftGen/Strings.swift @@ -10,6 +10,8 @@ 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 DashboardLocalization { + /// Search + public static let search = DashboardLocalization.tr("Localizable", "SEARCH", fallback: "Search") /// Localizable.strings /// Dashboard /// @@ -25,6 +27,70 @@ public enum DashboardLocalization { /// Welcome back. Let's keep learning. public static let welcomeBack = DashboardLocalization.tr("Localizable", "HEADER.WELCOME_BACK", fallback: "Welcome back. Let's keep learning.") } + public enum Learn { + /// All Courses + public static let allCourses = DashboardLocalization.tr("Localizable", "LEARN.ALL_COURSES", fallback: "All Courses") + /// Learn + public static let title = DashboardLocalization.tr("Localizable", "LEARN.TITLE", fallback: "Learn") + /// View All + public static let viewAll = DashboardLocalization.tr("Localizable", "LEARN.VIEW_ALL", fallback: "View All") + /// View All Courses (%@) + public static func viewAllCourses(_ p1: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.VIEW_ALL_COURSES", String(describing: p1), fallback: "View All Courses (%@)") + } + public enum Category { + /// All + public static let all = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.ALL", fallback: "All") + /// Completed + public static let completed = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.COMPLETED", fallback: "Completed") + /// Expired + public static let expired = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.EXPIRED", fallback: "Expired") + /// In Progress + public static let inProgress = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.IN_PROGRESS", fallback: "In Progress") + } + public enum DropdownMenu { + /// Courses + public static let courses = DashboardLocalization.tr("Localizable", "LEARN.DROPDOWN_MENU.COURSES", fallback: "Courses") + /// Programs + public static let programs = DashboardLocalization.tr("Localizable", "LEARN.DROPDOWN_MENU.PROGRAMS", fallback: "Programs") + } + public enum NoCoursesView { + /// Find a Course + public static let findACourse = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.FIND_A_COURSE", fallback: "Find a Course") + /// No Completed Courses + public static let noCompletedCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES", fallback: "No Completed Courses") + /// No Courses + public static let noCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES", fallback: "No Courses") + /// ou are not currently enrolled in any courses, would you like to explore the course catalog? + public static let noCoursesDescription = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION", fallback: "ou are not currently enrolled in any courses, would you like to explore the course catalog?") + /// No Courses in Progress + public static let noCoursesInProgress = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS", fallback: "No Courses in Progress") + /// No Expired Courses + public static let noExpiredCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES", fallback: "No Expired Courses") + } + public enum PrimaryCard { + /// %@ Due in %@ Days + public static func dueDays(_ p1: Any, _ p2: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.DUE_DAYS", String(describing: p1), String(describing: p2), fallback: "%@ Due in %@ Days") + } + /// %@ Assignments Due %@ + public static func futureAssignments(_ p1: Any, _ p2: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.FUTURE_ASSIGNMENTS", String(describing: p1), String(describing: p2), fallback: "%@ Assignments Due %@ ") + } + /// 1 Past Due Assignment + public static let onePastAssignment = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.ONE_PAST_ASSIGNMENT", fallback: "1 Past Due Assignment") + /// %@ Past Due Assignments + public static func pastAssignments(_ p1: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.PAST_ASSIGNMENTS", String(describing: p1), fallback: "%@ Past Due Assignments") + } + /// Resume Course + public static let resume = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.RESUME", fallback: "Resume Course") + /// Start Course + public static let startCourse = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.START_COURSE", fallback: "Start Course") + /// View Assignments + public static let viewAssignments = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.VIEW_ASSIGNMENTS", fallback: "View Assignments") + } + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Dashboard/Dashboard/en.lproj/Localizable.strings b/Dashboard/Dashboard/en.lproj/Localizable.strings index 88fc5d371..984dd4d60 100644 --- a/Dashboard/Dashboard/en.lproj/Localizable.strings +++ b/Dashboard/Dashboard/en.lproj/Localizable.strings @@ -10,4 +10,36 @@ "HEADER.COURSES" = "Courses"; "HEADER.WELCOME_BACK" = "Welcome back. Let's keep learning."; +"SEARCH" = "Search"; + "EMPTY.SUBTITLE" = "You are not enrolled in any courses yet."; + +"LEARN.TITLE" = "Learn"; +"LEARN.VIEW_ALL" = "View All"; +"LEARN.VIEW_ALL_COURSES" = "View All Courses (%@)"; +"LEARN.ALL_COURSES" = "All Courses"; + +"LEARN.PRIMARY_CARD.ONE_PAST_ASSIGNMENT" = "1 Past Due Assignment"; +"LEARN.PRIMARY_CARD.VIEW_ASSIGNMENTS" = "View Assignments"; +"LEARN.PRIMARY_CARD.PAST_ASSIGNMENTS" = "%@ Past Due Assignments"; +"LEARN.PRIMARY_CARD.FUTURE_ASSIGNMENTS" = "%@ Assignments Due %@ "; +"LEARN.PRIMARY_CARD.DUE_DAYS" = "%@ Due in %@ Days"; +"LEARN.PRIMARY_CARD.RESUME" = "Resume Course"; +"LEARN.PRIMARY_CARD.START_COURSE" = "Start Course"; + +"LEARN.DROPDOWN_MENU.COURSES" = "Courses"; +"LEARN.DROPDOWN_MENU.PROGRAMS" = "Programs"; + +"LEARN.CATEGORY.ALL" = "All"; +"LEARN.CATEGORY.IN_PROGRESS" = "In Progress"; +"LEARN.CATEGORY.COMPLETED" = "Completed"; +"LEARN.CATEGORY.EXPIRED" = "Expired"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES" = "No Courses"; +"LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS" = "No Courses in Progress"; +"LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES" = "No Completed Courses"; +"LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES" = "No Expired Courses"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "ou are not currently enrolled in any courses, would you like to explore the course catalog?"; + +"LEARN.NO_COURSES_VIEW.FIND_A_COURSE" = "Find a Course"; diff --git a/Dashboard/Dashboard/uk.lproj/Localizable.strings b/Dashboard/Dashboard/uk.lproj/Localizable.strings index 748f2c021..378016deb 100644 --- a/Dashboard/Dashboard/uk.lproj/Localizable.strings +++ b/Dashboard/Dashboard/uk.lproj/Localizable.strings @@ -10,5 +10,36 @@ "HEADER.COURSES" = "Курси"; "HEADER.WELCOME_BACK" = "З поверненням. Давайте продовжимо вчитись."; +"SEARCH" = "Пошук"; + "EMPTY.TITLE" = "Нічого немає"; "EMPTY.SUBTITLE" = "Ви не підписані на жодний курс."; + +"LEARN.TITLE" = "Навчання"; +"LEARN.VIEW_ALL" = "Переглянути все (%@)"; +"LEARN.ALL_COURSES" = "Усі курси"; + +"LEARN.PRIMARY_CARD.ONE_PAST_ASSIGNMENT" = "1 прострочене завдання"; +"LEARN.PRIMARY_CARD.VIEW_ASSIGNMENTS" = "Переглянути завдання"; +"LEARN.PRIMARY_CARD.PAST_ASSIGNMENTS" = "%@ Прострочені завдання"; +"LEARN.PRIMARY_CARD.FUTURE_ASSIGNMENTS" = "%@ Завданнь %@"; +"LEARN.PRIMARY_CARD.DUE_DAYS" = "%@ Оплата через %@ днів"; +"LEARN.PRIMARY_CARD.RESUME" = "Відновити курс"; +"LEARN.PRIMARY_CARD.START_COURSE" = "Розпочати курс"; + +"LEARN.DROPDOWN_MENU.COURSES" = "Курси"; +"LEARN.DROPDOWN_MENU.PROGRAMS" = "Програми"; + +"LEARN.CATEGORY.ALL" = "Усі"; +"LEARN.CATEGORY.IN_PROGRESS" = "Виконується"; +"LEARN.CATEGORY.COMPLETED" = "Завершено"; +"LEARN.CATEGORY.EXPIRED" = "Закінчився"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES" = "Немає курсів"; +"LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS" = "Немає поточних курсів"; +"LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES" = "Немає завершених курсів"; +"LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES" = "Немає прострочених курсів"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "наразі ви не зареєстровані на жодному курсі, бажаєте переглянути каталог?"; + +"LEARN.NO_COURSES_VIEW.FIND_A_COURSE" = "Знайти курс"; diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index fb6a1334e..a20e73f6a 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1641,10 +1641,78 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } + open func getMyLearnCourses(pageSize: Int) throws -> MyEnrollments { + addInvocation(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) + let perform = methodPerformValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) as? (Int) -> Void + perform?(`pageSize`) + var __value: MyEnrollments + do { + __value = try methodReturnValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getMyLearnCourses(pageSize: Int). Use given") + Failure("Stub return value not specified for getMyLearnCourses(pageSize: Int). Use given") + } catch { + throw error + } + return __value + } + + open func getMyLearnCoursesOffline() throws -> MyEnrollments { + addInvocation(.m_getMyLearnCoursesOffline) + let perform = methodPerformValue(.m_getMyLearnCoursesOffline) as? () -> Void + perform?() + var __value: MyEnrollments + do { + __value = try methodReturnValue(.m_getMyLearnCoursesOffline).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getMyLearnCoursesOffline(). Use given") + Failure("Stub return value not specified for getMyLearnCoursesOffline(). Use given") + } catch { + throw error + } + return __value + } + + open func getAllCourses(filteredBy: String, page: Int) throws -> MyEnrollments { + addInvocation(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) + let perform = methodPerformValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) as? (String, Int) -> Void + perform?(`filteredBy`, `page`) + var __value: MyEnrollments + do { + __value = try methodReturnValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getAllCourses(filteredBy: String, page: Int). Use given") + Failure("Stub return value not specified for getAllCourses(filteredBy: String, page: Int). Use given") + } catch { + throw error + } + return __value + } + + open func getAllCoursesOffline() throws -> MyEnrollments { + addInvocation(.m_getAllCoursesOffline) + let perform = methodPerformValue(.m_getAllCoursesOffline) as? () -> Void + perform?() + var __value: MyEnrollments + do { + __value = try methodReturnValue(.m_getAllCoursesOffline).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getAllCoursesOffline(). Use given") + Failure("Stub return value not specified for getAllCoursesOffline(). Use given") + } catch { + throw error + } + return __value + } + fileprivate enum MethodType { case m_getMyCourses__page_page(Parameter) case m_discoveryOffline + case m_getMyLearnCourses__pageSize_pageSize(Parameter) + case m_getMyLearnCoursesOffline + case m_getAllCourses__filteredBy_filteredBypage_page(Parameter, Parameter) + case m_getAllCoursesOffline static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1654,6 +1722,21 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return Matcher.ComparisonResult(results) case (.m_discoveryOffline, .m_discoveryOffline): return .match + + case (.m_getMyLearnCourses__pageSize_pageSize(let lhsPagesize), .m_getMyLearnCourses__pageSize_pageSize(let rhsPagesize)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPagesize, rhs: rhsPagesize, with: matcher), lhsPagesize, rhsPagesize, "pageSize")) + return Matcher.ComparisonResult(results) + + case (.m_getMyLearnCoursesOffline, .m_getMyLearnCoursesOffline): return .match + + case (.m_getAllCourses__filteredBy_filteredBypage_page(let lhsFilteredby, let lhsPage), .m_getAllCourses__filteredBy_filteredBypage_page(let rhsFilteredby, let rhsPage)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFilteredby, rhs: rhsFilteredby, with: matcher), lhsFilteredby, rhsFilteredby, "filteredBy")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) + return Matcher.ComparisonResult(results) + + case (.m_getAllCoursesOffline, .m_getAllCoursesOffline): return .match default: return .none } } @@ -1662,12 +1745,20 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { switch self { case let .m_getMyCourses__page_page(p0): return p0.intValue case .m_discoveryOffline: return 0 + case let .m_getMyLearnCourses__pageSize_pageSize(p0): return p0.intValue + case .m_getMyLearnCoursesOffline: return 0 + case let .m_getAllCourses__filteredBy_filteredBypage_page(p0, p1): return p0.intValue + p1.intValue + case .m_getAllCoursesOffline: return 0 } } func assertionName() -> String { switch self { case .m_getMyCourses__page_page: return ".getMyCourses(page:)" case .m_discoveryOffline: return ".discoveryOffline()" + case .m_getMyLearnCourses__pageSize_pageSize: return ".getMyLearnCourses(pageSize:)" + case .m_getMyLearnCoursesOffline: return ".getMyLearnCoursesOffline()" + case .m_getAllCourses__filteredBy_filteredBypage_page: return ".getAllCourses(filteredBy:page:)" + case .m_getAllCoursesOffline: return ".getAllCoursesOffline()" } } } @@ -1687,6 +1778,18 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { public static func discoveryOffline(willReturn: [CourseItem]...) -> MethodStub { return Given(method: .m_discoveryOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func getMyLearnCourses(pageSize: Parameter, willReturn: MyEnrollments...) -> MethodStub { + return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getMyLearnCoursesOffline(willReturn: MyEnrollments...) -> MethodStub { + return Given(method: .m_getMyLearnCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willReturn: MyEnrollments...) -> MethodStub { + return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getAllCoursesOffline(willReturn: MyEnrollments...) -> MethodStub { + return Given(method: .m_getAllCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func getMyCourses(page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getMyCourses__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1707,6 +1810,46 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { willProduce(stubber) return given } + public static func getMyLearnCourses(pageSize: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getMyLearnCourses(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (MyEnrollments).self) + willProduce(stubber) + return given + } + public static func getMyLearnCoursesOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getMyLearnCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (MyEnrollments).self) + willProduce(stubber) + return given + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (MyEnrollments).self) + willProduce(stubber) + return given + } + public static func getAllCoursesOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getAllCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (MyEnrollments).self) + willProduce(stubber) + return given + } } public struct Verify { @@ -1714,6 +1857,10 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { public static func getMyCourses(page: Parameter) -> Verify { return Verify(method: .m_getMyCourses__page_page(`page`))} public static func discoveryOffline() -> Verify { return Verify(method: .m_discoveryOffline)} + public static func getMyLearnCourses(pageSize: Parameter) -> Verify { return Verify(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`))} + public static func getMyLearnCoursesOffline() -> Verify { return Verify(method: .m_getMyLearnCoursesOffline)} + public static func getAllCourses(filteredBy: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`))} + public static func getAllCoursesOffline() -> Verify { return Verify(method: .m_getAllCoursesOffline)} } public struct Perform { @@ -1726,6 +1873,18 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { public static func discoveryOffline(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_discoveryOffline, performs: perform) } + public static func getMyLearnCourses(pageSize: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), performs: perform) + } + public static func getMyLearnCoursesOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getMyLearnCoursesOffline, performs: perform) + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, perform: @escaping (String, Int) -> Void) -> Perform { + return Perform(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), performs: perform) + } + public static func getAllCoursesOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getAllCoursesOffline, performs: perform) + } } public func given(_ method: Given) { diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index d3261b52d..9280f5263 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -32,7 +32,9 @@ final class DashboardViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -44,7 +46,9 @@ final class DashboardViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: true)) @@ -77,7 +81,9 @@ final class DashboardViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -89,7 +95,9 @@ final class DashboardViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: false)) diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index 3b20f84a3..dcd1b1060 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -134,7 +134,9 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", - numPages: 1, coursesCount: 10 + numPages: 1, coursesCount: 10, + progressEarned: 0, + progressPossible: 0 ) ) } @@ -150,13 +152,15 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: nil, + isActive: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", - numPages: 1, coursesCount: 10 + numPages: 1, coursesCount: 10, + progressEarned: 0, + progressPossible: 0 ) ) } @@ -179,7 +183,9 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { enrollmentEnd: nil, courseID: "course_id_\(i)", numPages: 1, - coursesCount: 10 + coursesCount: 10, + progressEarned: 0, + progressPossible: 0 ) ) } diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index cca463e95..e3d393b21 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -25,7 +25,9 @@ public protocol DiscoveryRouter: BaseRouter { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + selection: CourseTab, + lastVisitedBlockID: String? ) func showWebProgramDetails( @@ -56,7 +58,9 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + selection: CourseTab, + lastVisitedBlockID: String? ) {} public func showWebProgramDetails( diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 427cd4ade..1d9723245 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -280,7 +280,9 @@ private struct CourseStateView: View { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: title + title: title, + selection: .course, + lastVisitedBlockID: nil ) } }) diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 18e91733b..5335984de 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -236,7 +236,9 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + selection: .course, + lastVisitedBlockID: nil ) return true diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index 82c234882..cb5d18a5f 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -224,7 +224,9 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + selection: .course, + lastVisitedBlockID: nil ) return true diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index 30b6b5706..df7c19c9a 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -45,7 +45,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -57,7 +59,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + progressEarned: 0, + progressPossible: 0) ] viewModel.courses = items + items + items viewModel.totalPages = 2 @@ -94,7 +98,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 0), + coursesCount: 0, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -106,7 +112,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 0) + coursesCount: 0, + progressEarned: 0, + progressPossible: 0) ] Given(interactor, .discovery(page: 1, willReturn: items)) @@ -142,7 +150,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -154,7 +164,9 @@ final class DiscoveryViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: false)) diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index 3baf45321..a976b12ef 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -47,7 +47,9 @@ final class SearchViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 0), + coursesCount: 0, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", @@ -59,7 +61,9 @@ final class SearchViewModelTests: XCTestCase { enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 0) + coursesCount: 0, + progressEarned: 0, + progressPossible: 0) ] Given(interactor, .search(page: 1, searchTerm: .any, willReturn: items)) diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index ec0ed004c..82a37a0e9 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -169,6 +169,22 @@ class ScreenAssembly: Assembly { ) } + container.register(LearnViewModel.self) { r in + LearnViewModel( + interactor: r.resolve(DashboardInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DashboardAnalytics.self)! + ) + } + + container.register(AllCoursesViewModel.self) { r in + AllCoursesViewModel( + interactor: r.resolve(DashboardInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DashboardAnalytics.self)! + ) + } + // MARK: Profile container.register(ProfileRepositoryProtocol.self) { r in @@ -255,7 +271,7 @@ class ScreenAssembly: Assembly { // MARK: CourseScreensView container.register( CourseContainerViewModel.self - ) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd in + ) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd, selection, lastVisitedBlockID in CourseContainerViewModel( interactor: r.resolve(CourseInteractorProtocol.self)!, authInteractor: r.resolve(AuthInteractorProtocol.self)!, @@ -270,7 +286,9 @@ class ScreenAssembly: Assembly { courseEnd: courseEnd, enrollmentStart: enrollmentStart, enrollmentEnd: enrollmentEnd, - coreAnalytics: r.resolve(CoreAnalytics.self)! + lastVisitedBlockID: lastVisitedBlockID, + coreAnalytics: r.resolve(CoreAnalytics.self)!, + selection: selection ) } diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index e2fc37e54..e1031011d 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -32,7 +32,9 @@ public class CoursePersistence: CoursePersistenceProtocol { enrollmentEnd: $0.enrollmentEnd, courseID: $0.courseID ?? "", numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0)} if let result, !result.isEmpty { return result } else { @@ -48,9 +50,7 @@ public class CoursePersistence: CoursePersistenceProtocol { newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - if let isActive = item.isActive { - newItem.isActive = isActive - } + newItem.isActive = item.isActive newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart @@ -146,6 +146,7 @@ public class CoursePersistence: CoursePersistenceProtocol { public func saveCourseStructure(structure: DataLayer.CourseStructure) { context.performAndWait { + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let newStructure = CDCourseStructure(context: self.context) newStructure.certificate = structure.certificate?.url newStructure.mediaSmall = structure.media.image.small diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index 06241c00f..e656ca1a8 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -24,14 +24,16 @@ public class DashboardPersistence: DashboardPersistenceProtocol { org: $0.org ?? "", shortDescription: $0.desc ?? "", imageURL: $0.imageURL ?? "", - isActive: nil, + isActive: $0.isActive, courseStart: $0.courseStart, courseEnd: $0.courseEnd, enrollmentStart: $0.enrollmentStart, enrollmentEnd: $0.enrollmentEnd, courseID: $0.courseID ?? "", numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0)} if let result, !result.isEmpty { return result } else { @@ -42,12 +44,13 @@ public class DashboardPersistence: DashboardPersistenceProtocol { public func saveMyCourses(items: [CourseItem]) { for item in items { context.performAndWait { - let newItem = CDDashboardCourse(context: context) + let newItem = CDDashboardCourse(context: self.context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newItem.name = item.name newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL + newItem.isActive = item.isActive newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart @@ -63,4 +66,208 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } } + + public func loadMyEnrollments() throws -> MyEnrollments { + let request = CDMyEnrollments.fetchRequest() + if let result = try context.fetch(request).first { + let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in + + let futureAssignments = (cdPrimaryCourse.futureAssignments as? Set ?? []) + .map { future in + return Assignment( + type: future.type ?? "", + title: future.title ?? "", + description: future.descript ?? "", + date: future.date ?? Date(), + complete: future.complete, + firstComponentBlockId: future.firstComponentBlockId + ) + } + + let pastAssignments = (cdPrimaryCourse.pastAssignments as? Set ?? []) + .map { past in + return Assignment( + type: past.type ?? "", + title: past.title ?? "", + description: past.descript ?? "", + date: past.date ?? Date(), + complete: past.complete, + firstComponentBlockId: past.firstComponentBlockId + ) + } + + return PrimaryCourse( + name: cdPrimaryCourse.name ?? "", + org: cdPrimaryCourse.org ?? "", + courseID: cdPrimaryCourse.courseID ?? "", + isActive: cdPrimaryCourse.isActive, + courseStart: cdPrimaryCourse.courseStart, + courseEnd: cdPrimaryCourse.courseEnd, + courseBanner: cdPrimaryCourse.courseBanner ?? "", + futureAssignments: futureAssignments, + pastAssignments: pastAssignments, + progressEarned: cdPrimaryCourse.progressEarned, + progressPossible: cdPrimaryCourse.progressPossible, + lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", + resumeTitle: cdPrimaryCourse.resumeTitle + ) + } + + let courses = (result.courses as? Set ?? []) + .map { cdCourse in + return CourseItem( + name: cdCourse.name ?? "", + org: cdCourse.org ?? "", + shortDescription: cdCourse.desc ?? "", + imageURL: cdCourse.imageURL ?? "", + isActive: cdCourse.isActive, + courseStart: cdCourse.courseStart, + courseEnd: cdCourse.courseEnd, + enrollmentStart: cdCourse.enrollmentStart, + enrollmentEnd: cdCourse.enrollmentEnd, + courseID: cdCourse.courseID ?? "", + numPages: Int(cdCourse.numPages), + coursesCount: Int(cdCourse.courseCount), + progressEarned: cdCourse.progressEarned, + progressPossible: cdCourse.progressPossible + ) + } + + return MyEnrollments( + primaryCourse: primaryCourse, + courses: courses, + totalPages: Int(result.totalPages), + count: Int(result.count) + ) + } else { + throw NoCachedDataError() + } + } + + // swiftlint:disable function_body_length + public func saveMyEnrollments(enrollments: MyEnrollments) { + context.performAndWait { + let request: NSFetchRequest = CDMyEnrollments.fetchRequest() + + let existingEnrollment: CDMyEnrollments? + do { + existingEnrollment = try context.fetch(request).first + } catch { + existingEnrollment = nil + } + + let newEnrollment: CDMyEnrollments + if let existingEnrollment { + newEnrollment = existingEnrollment + } else { + newEnrollment = CDMyEnrollments(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + } + + // saving PrimaryCourse + if let primaryCourse = enrollments.primaryCourse { + let cdPrimaryCourse: CDPrimaryCourse + if let existingPrimaryCourse = newEnrollment.primaryCourse { + cdPrimaryCourse = existingPrimaryCourse + } else { + cdPrimaryCourse = CDPrimaryCourse(context: context) + } + + let futureAssignments = primaryCourse.futureAssignments + let uniqueFutureAssignments = Set(futureAssignments.map { assignment in + let assignmentRequest: NSFetchRequest = CDAssignment.fetchRequest() + assignmentRequest.predicate = NSPredicate(format: "title == %@", assignment.title) + let existingAssignment = try? self.context.fetch(assignmentRequest).first + + let cdAssignment: CDAssignment + if let existingAssignment { + cdAssignment = existingAssignment + } else { + cdAssignment = CDAssignment(context: self.context) + } + + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + }) + + cdPrimaryCourse.futureAssignments = NSSet(array: Array(uniqueFutureAssignments)) + + let pastAssignments = primaryCourse.pastAssignments + let uniqueAssignments = Set(pastAssignments.map { assignment in + let assignmentRequest: NSFetchRequest = CDAssignment.fetchRequest() + assignmentRequest.predicate = NSPredicate(format: "title == %@", assignment.title) + let existingAssignment = try? self.context.fetch(assignmentRequest).first + + let cdAssignment: CDAssignment + if let existingAssignment { + cdAssignment = existingAssignment + } else { + cdAssignment = CDAssignment(context: self.context) + } + + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + }) + + cdPrimaryCourse.pastAssignments = NSSet(array: Array(uniqueAssignments)) + + // saving PrimaryCourse + cdPrimaryCourse.name = primaryCourse.name + cdPrimaryCourse.org = primaryCourse.org + cdPrimaryCourse.courseID = primaryCourse.courseID + cdPrimaryCourse.isActive = primaryCourse.isActive + cdPrimaryCourse.courseStart = primaryCourse.courseStart + cdPrimaryCourse.courseEnd = primaryCourse.courseEnd + cdPrimaryCourse.courseBanner = primaryCourse.courseBanner + cdPrimaryCourse.progressEarned = primaryCourse.progressEarned ?? 0 + cdPrimaryCourse.progressPossible = primaryCourse.progressPossible ?? 0 + cdPrimaryCourse.lastVisitedBlockID = primaryCourse.lastVisitedBlockID + cdPrimaryCourse.resumeTitle = primaryCourse.resumeTitle + + newEnrollment.primaryCourse = cdPrimaryCourse + } + + // saving courses + if let existingCourses = newEnrollment.courses { + for course in existingCourses { + context.delete(course as! CDDashboardCourse) + } + } + + newEnrollment.courses = NSSet(array: enrollments.courses.map { course in + let cdCourse = CDDashboardCourse(context: self.context) + cdCourse.name = course.name + cdCourse.org = course.org + cdCourse.desc = course.shortDescription + cdCourse.imageURL = course.imageURL + cdCourse.courseStart = course.courseStart + cdCourse.courseEnd = course.courseEnd + cdCourse.enrollmentStart = course.enrollmentStart + cdCourse.enrollmentEnd = course.enrollmentEnd + cdCourse.courseID = course.courseID + cdCourse.numPages = Int32(course.numPages) + return cdCourse + }) + + newEnrollment.totalPages = Int32(enrollments.totalPages) + newEnrollment.count = Int32(enrollments.count) + + do { + try context.save() + } catch { + print("Ошибка при сохранении MyEnrollments:", error) + } + } + } + // swiftlint:enable function_body_length } diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift index 189264f41..3ee35927c 100644 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ b/OpenEdX/Data/DiscoveryPersistence.swift @@ -31,7 +31,9 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { enrollmentEnd: $0.enrollmentEnd, courseID: $0.courseID ?? "", numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} + coursesCount: Int($0.courseCount), + progressEarned: 0, + progressPossible: 0)} if let result, !result.isEmpty { return result } else { @@ -48,9 +50,7 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - if let isActive = item.isActive { - newItem.isActive = isActive - } + newItem.isActive = item.isActive newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index ae33a9d64..4ede7b649 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -112,7 +112,9 @@ extension Router: DeepLinkRouter { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + selection: .course, + lastVisitedBlockID: nil ) } else { showCourseDetais( diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 8720ae03f..d835ba149 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -6,6 +6,7 @@ // import Course +import Core import Combine import Discovery import SwiftUI @@ -189,7 +190,9 @@ public class PipManager: PipManagerProtocol { courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + selection: .course, + lastVisitedBlockID: nil ) controller.rootView.viewModel.selection = holder.selectedCourseTab return controller diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 6c96146b3..1478f1a57 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -364,7 +364,9 @@ public class Router: AuthorizationRouter, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + selection: CourseTab, + lastVisitedBlockID: String? ) { let controller = getCourseScreensController( courseID: courseID, @@ -373,7 +375,9 @@ public class Router: AuthorizationRouter, courseEnd: courseEnd, enrollmentStart: enrollmentStart, enrollmentEnd: enrollmentEnd, - title: title + title: title, + selection: selection, + lastVisitedBlockID: lastVisitedBlockID ) navigationController.pushViewController(controller, animated: true) } @@ -385,7 +389,9 @@ public class Router: AuthorizationRouter, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + selection: CourseTab, + lastVisitedBlockID: String? ) -> UIHostingController { let vm = Container.shared.resolve( CourseContainerViewModel.self, @@ -393,7 +399,9 @@ public class Router: AuthorizationRouter, courseStart, courseEnd, enrollmentStart, - enrollmentEnd + enrollmentEnd, + selection, + lastVisitedBlockID )! let screensView = CourseContainerView( viewModel: vm, @@ -404,6 +412,13 @@ public class Router: AuthorizationRouter, return UIHostingController(rootView: screensView) } + public func showAllCourses(courses: [CourseItem]) { + let vm = Container.shared.resolve(AllCoursesViewModel.self)! + let view = AllCoursesView(viewModel: vm, router: self) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + public func showHandoutsUpdatesView( handouts: String?, announcements: [CourseUpdate]?, diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index f51ebc476..c99f6e412 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -22,6 +22,8 @@ struct MainScreenView: View { @State private var updateAvaliable: Bool = false @ObservedObject private(set) var viewModel: MainScreenViewModel + + private let config = Container.shared.resolve(ConfigProtocol.self) init(viewModel: MainScreenViewModel) { self.viewModel = viewModel @@ -38,7 +40,29 @@ struct MainScreenView: View { var body: some View { TabView(selection: $viewModel.selection) { - let config = Container.shared.resolve(ConfigProtocol.self) + if config?.dashboard.type == .learn { + ZStack { + LearnView( + viewModel: Container.shared.resolve(LearnViewModel.self)!, + router: Container.shared.resolve(DashboardRouter.self)!, + programView: ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ), + openDiscoveryPage: { viewModel.selection = .discovery } + ) + if updateAvaliable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.learn.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.learn) + } + .tag(MainTab.dashboard) + .accessibilityIdentifier("dashboard_tabitem") + } + if config?.discovery.enabled ?? false { ZStack { if config?.discovery.type == .native { @@ -68,44 +92,23 @@ struct MainScreenView: View { .accessibilityIdentifier("discovery_tabitem") } - ZStack { - DashboardView( - viewModel: Container.shared.resolve(DashboardViewModel.self)!, - router: Container.shared.resolve(DashboardRouter.self)! - ) - if updateAvaliable { - UpdateNotificationView(config: viewModel.config) - } - } - .tabItem { - CoreAssets.dashboard.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.dashboard) - } - .tag(MainTab.dashboard) - .accessibilityIdentifier("dashboard_tabitem") - - if config?.program.enabled ?? false { + if config?.dashboard.type == .dashboard { ZStack { - if config?.program.type == .webview { - ProgramWebviewView( - viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, - router: Container.shared.resolve(DiscoveryRouter.self)! - ) - } else if config?.program.type == .native { - Text(CoreLocalization.Mainscreen.inDeveloping) - .accessibilityIdentifier("indevelopment_program_text") - } + DashboardView( + viewModel: Container.shared.resolve(DashboardViewModel.self)!, + router: Container.shared.resolve(DashboardRouter.self)! + ) if updateAvaliable { UpdateNotificationView(config: viewModel.config) } } .tabItem { - CoreAssets.programs.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.programs) + CoreAssets.dashboard.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.dashboard) } - .tag(MainTab.programs) - .accessibilityIdentifier("programs_tabitem") + .tag(MainTab.dashboard) + .accessibilityIdentifier("dashboard_tabitem") } VStack { @@ -120,8 +123,8 @@ struct MainScreenView: View { .tag(MainTab.profile) .accessibilityIdentifier("profile_tabitem") } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + .navigationBarHidden(viewModel.selection == .dashboard) + .navigationBarBackButtonHidden(viewModel.selection == .dashboard) .navigationTitle(titleBar()) .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: { @@ -175,7 +178,9 @@ struct MainScreenView: View { case .discovery: return DiscoveryLocalization.title case .dashboard: - return DashboardLocalization.title + return config?.dashboard.type == .dashboard + ? DashboardLocalization.title + : DashboardLocalization.Learn.title case .programs: return CoreLocalization.Mainscreen.programs case .profile: diff --git a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json index 0a5fa0807..d31f2bcff 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.878", - "green" : "0.831", - "red" : "0.800" + "blue" : "0xDF", + "green" : "0xD3", + "red" : "0xCC" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json index 00cf4a827..e9a7a3504 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.961", - "green" : "0.961", - "red" : "0.961" + "blue" : "0xF5", + "green" : "0xF5", + "red" : "0xF5" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json new file mode 100644 index 000000000..e7c5c162d --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.150", + "blue" : "0x2F", + "green" : "0x21", + "red" : "0x19" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift index 733a02635..f6a688d3e 100644 --- a/Theme/Theme/SwiftGen/ThemeAssets.swift +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -36,6 +36,7 @@ public enum ThemeAssets { public static let cardViewStroke = ColorAsset(name: "CardViewStroke") public static let certificateForeground = ColorAsset(name: "CertificateForeground") public static let commentCellBackground = ColorAsset(name: "CommentCellBackground") + public static let courseCardShadow = ColorAsset(name: "CourseCardShadow") public static let datesSectionBackground = ColorAsset(name: "DatesSectionBackground") public static let datesSectionStroke = ColorAsset(name: "DatesSectionStroke") public static let nextWeekTimelineColor = ColorAsset(name: "NextWeekTimelineColor") diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift index 0fad87eb9..742b5d7f7 100644 --- a/Theme/Theme/Theme.swift +++ b/Theme/Theme/Theme.swift @@ -68,6 +68,7 @@ public struct Theme { public private(set) static var slidingStrokeColor = ThemeAssets.slidingStrokeColor.swiftUIColor public private(set) static var primaryHeaderColor = ThemeAssets.primaryHeaderColor.swiftUIColor public private(set) static var secondaryHeaderColor = ThemeAssets.secondaryHeaderColor.swiftUIColor + public private(set) static var courseCardShadow = ThemeAssets.courseCardShadow.swiftUIColor public static func update( accentColor: Color = ThemeAssets.accentColor.swiftUIColor, From 37aa051106ec8269329c95df3a2ba669071aca37 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 9 May 2024 16:33:08 +0300 Subject: [PATCH 02/22] fix: address feedback --- .../Config/DashboardConfig.swift | 6 +- Dashboard/Dashboard.xcodeproj/project.pbxproj | 32 +++++----- .../Presentation/AllCoursesView.swift | 4 +- ...oardView.swift => ListDashboardView.swift} | 20 +++---- ...del.swift => ListDashboardViewModel.swift} | 4 +- ...swift => PrimaryCourseDashboardView.swift} | 16 ++--- ... => PrimaryCourseDashboardViewModel.swift} | 4 +- .../DashboardViewModelTests.swift | 12 ++-- OpenEdX/DI/ScreenAssembly.swift | 8 +-- OpenEdX/View/MainScreenView.swift | 59 +++++++++---------- 10 files changed, 82 insertions(+), 83 deletions(-) rename Dashboard/Dashboard/Presentation/{DashboardView.swift => ListDashboardView.swift} (93%) rename Dashboard/Dashboard/Presentation/{DashboardViewModel.swift => ListDashboardViewModel.swift} (97%) rename Dashboard/Dashboard/Presentation/{LearnView.swift => PrimaryCourseDashboardView.swift} (97%) rename Dashboard/Dashboard/Presentation/{LearnViewModel.swift => PrimaryCourseDashboardViewModel.swift} (95%) diff --git a/Core/Core/Configuration/Config/DashboardConfig.swift b/Core/Core/Configuration/Config/DashboardConfig.swift index 3b7ac4543..6ff21b31d 100644 --- a/Core/Core/Configuration/Config/DashboardConfig.swift +++ b/Core/Core/Configuration/Config/DashboardConfig.swift @@ -8,8 +8,8 @@ import Foundation public enum DashboardConfigType: String { - case learn - case dashboard + case primaryCourse = "primary_course" + case list } private enum DashboardKeys: String, RawStringExtractable { @@ -22,7 +22,7 @@ public class DashboardConfig: NSObject { init(dictionary: [String: AnyObject]) { type = (dictionary[DashboardKeys.dashboardType] as? String).flatMap { DashboardConfigType(rawValue: $0) - } ?? .learn + } ?? .primaryCourse } } diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 30ea18598..c6e7e4e52 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -9,15 +9,15 @@ /* Begin PBXBuildFile section */ 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */; }; 027724202BCEA16C00C2908D /* ProgressLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */; }; - 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33228D8BDBA002B6862 /* DashboardView.swift */; }; + 027DB33328D8BDBA002B6862 /* ListDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */; }; 027DB33928D8D9F6002B6862 /* DashboardEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */; }; 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */; }; 027DB33F28D8E605002B6862 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB33E28D8E605002B6862 /* Core.framework */; }; 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */; }; - 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */; }; + 027DB34528D8E9D2002B6862 /* ListDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */; }; 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028895662BE3B34E00102D8C /* NoCoursesView.swift */; }; - 02935B6F2BCEC91100B22F66 /* LearnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B6E2BCEC91100B22F66 /* LearnView.swift */; }; - 02935B712BCEC91F00B22F66 /* LearnViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */; }; + 02935B6F2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */; }; + 02935B712BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */; }; 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B762BCFB2C100B22F66 /* CourseCardView.swift */; }; 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */; }; 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */; }; @@ -51,15 +51,15 @@ /* Begin PBXFileReference section */ 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCardView.swift; sourceTree = ""; }; 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressLineView.swift; sourceTree = ""; }; - 027DB33228D8BDBA002B6862 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; + 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDashboardView.swift; sourceTree = ""; }; 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEndpoint.swift; sourceTree = ""; }; 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardRepository.swift; sourceTree = ""; }; 027DB33E28D8E605002B6862 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardInteractor.swift; sourceTree = ""; }; - 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; + 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDashboardViewModel.swift; sourceTree = ""; }; 028895662BE3B34E00102D8C /* NoCoursesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCoursesView.swift; sourceTree = ""; }; - 02935B6E2BCEC91100B22F66 /* LearnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnView.swift; sourceTree = ""; }; - 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnViewModel.swift; sourceTree = ""; }; + 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCourseDashboardView.swift; sourceTree = ""; }; + 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCourseDashboardViewModel.swift; sourceTree = ""; }; 02935B762BCFB2C100B22F66 /* CourseCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCardView.swift; sourceTree = ""; }; 02A48B17295ACE200033D5E0 /* DashboardCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DashboardCoreModel.xcdatamodel; sourceTree = ""; }; 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistenceProtocol.swift; sourceTree = ""; }; @@ -215,12 +215,12 @@ isa = PBXGroup; children = ( 0277241C2BCE9DF300C2908D /* Elements */, - 02935B6E2BCEC91100B22F66 /* LearnView.swift */, - 02935B702BCEC91F00B22F66 /* LearnViewModel.swift */, + 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */, + 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */, 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */, 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */, - 027DB33228D8BDBA002B6862 /* DashboardView.swift */, - 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */, + 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */, + 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */, 02F3BFE029252FCB0051930C /* DashboardRouter.swift */, 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */, ); @@ -494,17 +494,17 @@ files = ( 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */, 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */, - 02935B6F2BCEC91100B22F66 /* LearnView.swift in Sources */, + 02935B6F2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift in Sources */, 02A98F172BD97886009B46BD /* CategoryFilterView.swift in Sources */, 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */, - 02935B712BCEC91F00B22F66 /* LearnViewModel.swift in Sources */, + 02935B712BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift in Sources */, 02A98F112BD96814009B46BD /* DropDownMenu.swift in Sources */, 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */, 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */, 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */, - 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */, + 027DB34528D8E9D2002B6862 /* ListDashboardViewModel.swift in Sources */, 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */, - 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */, + 027DB33328D8BDBA002B6862 /* ListDashboardView.swift in Sources */, 02A98F152BD96BC2009B46BD /* AllCoursesViewModel.swift in Sources */, 02A98F132BD96B9D009B46BD /* AllCoursesView.swift in Sources */, 02F3BFE129252FCB0051930C /* DashboardRouter.swift in Sources */, diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index dd16616b9..5233652cf 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -201,11 +201,11 @@ struct AllCoursesView_Previews: PreviewProvider { AllCoursesView(viewModel: vm, router: DashboardRouterMock()) .preferredColorScheme(.light) - .previewDisplayName("DashboardView Light") + .previewDisplayName("AllCoursesView Light") AllCoursesView(viewModel: vm, router: DashboardRouterMock()) .preferredColorScheme(.dark) - .previewDisplayName("DashboardView Dark") + .previewDisplayName("AllCoursesView Dark") } } #endif diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift similarity index 93% rename from Dashboard/Dashboard/Presentation/DashboardView.swift rename to Dashboard/Dashboard/Presentation/ListDashboardView.swift index 4b9f00833..dfb01ba5e 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -1,5 +1,5 @@ // -// DashboardView.swift +// ListDashboardView.swift // Dashboard // // Created by  Stepanok Ivan on 19.09.2022. @@ -9,7 +9,7 @@ import SwiftUI import Core import Theme -public struct DashboardView: View { +public struct ListDashboardView: View { private let dashboardCourses: some View = VStack(alignment: .leading) { Text(DashboardLocalization.Header.courses) .font(Theme.Fonts.displaySmall) @@ -25,10 +25,10 @@ public struct DashboardView: View { .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) @StateObject - private var viewModel: DashboardViewModel + private var viewModel: ListDashboardViewModel private let router: DashboardRouter - public init(viewModel: DashboardViewModel, router: DashboardRouter) { + public init(viewModel: ListDashboardViewModel, router: DashboardRouter) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self.router = router } @@ -140,22 +140,22 @@ public struct DashboardView: View { } #if DEBUG -struct DashboardView_Previews: PreviewProvider { +struct ListDashboardView_Previews: PreviewProvider { static var previews: some View { - let vm = DashboardViewModel( + let vm = ListDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), analytics: DashboardAnalyticsMock() ) let router = DashboardRouterMock() - DashboardView(viewModel: vm, router: router) + ListDashboardView(viewModel: vm, router: router) .preferredColorScheme(.light) - .previewDisplayName("DashboardView Light") + .previewDisplayName("ListDashboardView Light") - DashboardView(viewModel: vm, router: router) + ListDashboardView(viewModel: vm, router: router) .preferredColorScheme(.dark) - .previewDisplayName("DashboardView Dark") + .previewDisplayName("ListDashboardView Dark") } } #endif diff --git a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift similarity index 97% rename from Dashboard/Dashboard/Presentation/DashboardViewModel.swift rename to Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index 6e4d9974a..023c20ca3 100644 --- a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -1,5 +1,5 @@ // -// DashboardViewModel.swift +// ListDashboardViewModel.swift // Dashboard // // Created by  Stepanok Ivan on 19.09.2022. @@ -10,7 +10,7 @@ import Core import SwiftUI import Combine -public class DashboardViewModel: ObservableObject { +public class ListDashboardViewModel: ObservableObject { public var nextPage = 1 public var totalPages = 1 diff --git a/Dashboard/Dashboard/Presentation/LearnView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift similarity index 97% rename from Dashboard/Dashboard/Presentation/LearnView.swift rename to Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index b2576d445..99a606bb3 100644 --- a/Dashboard/Dashboard/Presentation/LearnView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -1,5 +1,5 @@ // -// LearnView.swift +// PrimaryCourseDashboardView.swift // Dashboard // // Created by  Stepanok Ivan on 16.04.2024. @@ -11,10 +11,10 @@ import Theme //import Discovery import Swinject -public struct LearnView: View { +public struct PrimaryCourseDashboardView: View { @StateObject - private var viewModel: LearnViewModel + private var viewModel: PrimaryCourseDashboardViewModel private let router: DashboardRouter private let config = Container.shared.resolve(ConfigProtocol.self) @ViewBuilder let programView: ProgramView @@ -23,7 +23,7 @@ public struct LearnView: View { @State private var selectedMenu: MenuOption = .courses public init( - viewModel: LearnViewModel, + viewModel: PrimaryCourseDashboardViewModel, router: DashboardRouter, programView: ProgramView, openDiscoveryPage: @escaping () -> Void @@ -281,15 +281,15 @@ public struct LearnView: View { } #if DEBUG -struct LearnView_Previews: PreviewProvider { +struct PrimaryCourseDashboardView_Previews: PreviewProvider { static var previews: some View { - let vm = LearnViewModel( + let vm = PrimaryCourseDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), analytics: DashboardAnalyticsMock() ) - LearnView(viewModel: vm, + PrimaryCourseDashboardView(viewModel: vm, router: DashboardRouterMock(), programView: EmptyView(), openDiscoveryPage: { @@ -297,7 +297,7 @@ struct LearnView_Previews: PreviewProvider { .preferredColorScheme(.light) .previewDisplayName("DashboardView Light") - LearnView(viewModel: vm, + PrimaryCourseDashboardView(viewModel: vm, router: DashboardRouterMock(), programView: EmptyView(), openDiscoveryPage: { diff --git a/Dashboard/Dashboard/Presentation/LearnViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift similarity index 95% rename from Dashboard/Dashboard/Presentation/LearnViewModel.swift rename to Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 17a1ba1b3..1ac8b28d8 100644 --- a/Dashboard/Dashboard/Presentation/LearnViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -1,5 +1,5 @@ // -// LearnViewModel.swift +// PrimaryCourseDashboardViewModel.swift // Dashboard // // Created by  Stepanok Ivan on 16.04.2024. @@ -10,7 +10,7 @@ import Core import SwiftUI import Combine -public class LearnViewModel: ObservableObject { +public class PrimaryCourseDashboardViewModel: ObservableObject { public var nextPage = 1 public var totalPages = 1 diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index 9280f5263..760924b1b 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -1,5 +1,5 @@ // -// DashboardViewModelTests.swift +// ListDashboardViewModelTests.swift // DashboardTests // // Created by  Stepanok Ivan on 18.01.2023. @@ -12,13 +12,13 @@ import XCTest import Alamofire import SwiftUI -final class DashboardViewModelTests: XCTestCase { +final class ListDashboardViewModelTests: XCTestCase { func testGetMyCoursesSuccess() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -67,7 +67,7 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -116,7 +116,7 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyCourses(page: .any, willThrow: NoCachedDataError()) ) @@ -134,7 +134,7 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyCourses(page: .any, willThrow: NSError()) ) diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 82a37a0e9..d9d3f22c0 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -161,16 +161,16 @@ class ScreenAssembly: Assembly { repository: r.resolve(DashboardRepositoryProtocol.self)! ) } - container.register(DashboardViewModel.self) { r in - DashboardViewModel( + container.register(ListDashboardViewModel.self) { r in + ListDashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, analytics: r.resolve(DashboardAnalytics.self)! ) } - container.register(LearnViewModel.self) { r in - LearnViewModel( + container.register(PrimaryCourseDashboardViewModel.self) { r in + PrimaryCourseDashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, analytics: r.resolve(DashboardAnalytics.self)! diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index c99f6e412..546165992 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -23,8 +23,8 @@ struct MainScreenView: View { @ObservedObject private(set) var viewModel: MainScreenViewModel - private let config = Container.shared.resolve(ConfigProtocol.self) - + private let config = Container.shared.resolve(ConfigProtocol.self)! + init(viewModel: MainScreenViewModel) { self.viewModel = viewModel UITabBar.appearance().isTranslucent = false @@ -37,13 +37,31 @@ struct MainScreenView: View { for: .normal ) } - + var body: some View { TabView(selection: $viewModel.selection) { - if config?.dashboard.type == .learn { + switch config.dashboard.type { + case .list: + ZStack { + ListDashboardView( + viewModel: Container.shared.resolve(ListDashboardViewModel.self)!, + router: Container.shared.resolve(DashboardRouter.self)! + ) + + if updateAvaliable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.dashboard.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.dashboard) + } + .tag(MainTab.dashboard) + .accessibilityIdentifier("dashboard_tabitem") + case .primaryCourse: ZStack { - LearnView( - viewModel: Container.shared.resolve(LearnViewModel.self)!, + PrimaryCourseDashboardView( + viewModel: Container.shared.resolve(PrimaryCourseDashboardViewModel.self)!, router: Container.shared.resolve(DashboardRouter.self)!, programView: ProgramWebviewView( viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, @@ -62,16 +80,16 @@ struct MainScreenView: View { .tag(MainTab.dashboard) .accessibilityIdentifier("dashboard_tabitem") } - - if config?.discovery.enabled ?? false { + + if config.discovery.enabled { ZStack { - if config?.discovery.type == .native { + if config.discovery.type == .native { DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, sourceScreen: viewModel.sourceScreen ) - } else if config?.discovery.type == .webview { + } else if config.discovery.type == .webview { DiscoveryWebview( viewModel: Container.shared.resolve( DiscoveryWebviewViewModel.self, @@ -92,25 +110,6 @@ struct MainScreenView: View { .accessibilityIdentifier("discovery_tabitem") } - if config?.dashboard.type == .dashboard { - ZStack { - DashboardView( - viewModel: Container.shared.resolve(DashboardViewModel.self)!, - router: Container.shared.resolve(DashboardRouter.self)! - ) - - if updateAvaliable { - UpdateNotificationView(config: viewModel.config) - } - } - .tabItem { - CoreAssets.dashboard.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.dashboard) - } - .tag(MainTab.dashboard) - .accessibilityIdentifier("dashboard_tabitem") - } - VStack { ProfileView( viewModel: Container.shared.resolve(ProfileViewModel.self)!, settingsTapped: $settingsTapped @@ -178,7 +177,7 @@ struct MainScreenView: View { case .discovery: return DiscoveryLocalization.title case .dashboard: - return config?.dashboard.type == .dashboard + return config.dashboard.type == .list ? DashboardLocalization.title : DashboardLocalization.Learn.title case .programs: From a973589f4970f99569ba3a3473c8d566bf824abf Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 9 May 2024 17:55:44 +0300 Subject: [PATCH 03/22] fix: address feedback --- Core/Core.xcodeproj/project.pbxproj | 24 +++--- .../Contents.json | 0 .../learn_big.svg | 0 Core/Core/Data/Model/Data_Discovery.swift | 2 +- ...Dashboard.swift => Data_Enrollments.swift} | 4 +- ...ses.swift => Data_PrimaryEnrollment.swift} | 46 ++++++------ Core/Core/Domain/Model/CourseItem.swift | 14 ++-- ...ollments.swift => PrimaryEnrollment.swift} | 18 ++--- Core/Core/SwiftGen/Assets.swift | 2 +- Core/Core/View/Base/CourseCellView.swift | 2 +- .../CourseCoreModel.xcdatamodel/contents | 6 +- .../Dashboard/Data/DashboardRepository.swift | 73 +++++++------------ .../DashboardCoreModel.xcdatamodel/contents | 12 +-- .../DashboardPersistenceProtocol.swift | 4 +- .../Domain/DashboardInteractor.swift | 33 ++++----- .../Presentation/AllCoursesView.swift | 4 +- .../Presentation/AllCoursesViewModel.swift | 47 ++++++------ .../Presentation/DashboardRouter.swift | 4 +- .../Elements/CourseCardView.swift | 18 ++--- .../Presentation/Elements/NoCoursesView.swift | 2 +- .../Elements/PrimaryCardView.swift | 8 +- .../Elements/ProgressLineView.swift | 6 +- .../Presentation/ListDashboardView.swift | 2 +- .../Presentation/ListDashboardViewModel.swift | 6 +- .../PrimaryCourseDashboardView.swift | 44 +++++------ .../PrimaryCourseDashboardViewModel.swift | 10 +-- .../DashboardMock.generated.swift | 40 +++++----- .../Discovery/Data/DiscoveryRepository.swift | 6 +- .../DiscoveryCoreModel.xcdatamodel/contents | 4 +- .../Presentation/DiscoveryRouter.swift | 4 +- .../NativeDiscovery/CourseDetailsView.swift | 2 +- .../DiscoveryWebviewViewModel.swift | 2 +- .../WebPrograms/ProgramWebviewViewModel.swift | 2 +- OpenEdX/Data/CoursePersistence.swift | 4 +- OpenEdX/Data/DashboardPersistence.swift | 30 ++++---- OpenEdX/Data/DiscoveryPersistence.swift | 4 +- .../DeepLinkRouter/DeepLinkRouter.swift | 2 +- OpenEdX/Managers/PipManager.swift | 4 +- OpenEdX/Router.swift | 8 +- 39 files changed, 236 insertions(+), 267 deletions(-) rename Core/Core/Assets.xcassets/{learn_big.imageset => learn_empty.imageset}/Contents.json (100%) rename Core/Core/Assets.xcassets/{learn_big.imageset => learn_empty.imageset}/learn_big.svg (100%) rename Core/Core/Data/Model/{Data_Dashboard.swift => Data_Enrollments.swift} (98%) rename Core/Core/Data/Model/{Data_MyLearnCourses.swift => Data_PrimaryEnrollment.swift} (88%) rename Core/Core/Domain/Model/{MyEnrollments.swift => PrimaryEnrollment.swift} (88%) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 9a362b53a..420c32f9f 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -63,8 +63,8 @@ 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */; }; 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */; }; 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F38293A452B00DE65D0 /* ResetPassword.swift */; }; - 02935B732BCECAD000B22F66 /* Data_MyLearnCourses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */; }; - 02935B752BCEE6D600B22F66 /* MyEnrollments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */; }; + 02935B732BCECAD000B22F66 /* Data_PrimaryEnrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */; }; + 02935B752BCEE6D600B22F66 /* PrimaryEnrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */; }; 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */; }; 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A463102AEA966C00331037 /* AppReviewView.swift */; }; 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */; }; @@ -76,7 +76,7 @@ 02B2B594295C5C7A00914876 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B2B593295C5C7A00914876 /* Thread.swift */; }; 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */; }; 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */; }; - 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */; }; + 02C917F029CDA99E00DBB8BD /* Data_Enrollments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */; }; 02CA59842BD7DDBE00D517AA /* DashboardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */; }; 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF46C729546AA200A698EE /* NoCachedDataError.swift */; }; 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */; }; @@ -248,8 +248,8 @@ 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleKeyboardInputView.swift; sourceTree = ""; }; 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_ResetPassword.swift; sourceTree = ""; }; 028F9F38293A452B00DE65D0 /* ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPassword.swift; sourceTree = ""; }; - 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_MyLearnCourses.swift; sourceTree = ""; }; - 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyEnrollments.swift; sourceTree = ""; }; + 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_PrimaryEnrollment.swift; sourceTree = ""; }; + 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryEnrollment.swift; sourceTree = ""; }; 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; 02A463102AEA966C00331037 /* AppReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewView.swift; sourceTree = ""; }; 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistenceProtocol.swift; sourceTree = ""; }; @@ -261,7 +261,7 @@ 02B2B593295C5C7A00914876 /* Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thread.swift; sourceTree = ""; }; 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewControllerExtension.swift; sourceTree = ""; }; 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; - 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Dashboard.swift; sourceTree = ""; }; + 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Enrollments.swift; sourceTree = ""; }; 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardConfig.swift; sourceTree = ""; }; 02CF46C729546AA200A698EE /* NoCachedDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCachedDataError.swift; sourceTree = ""; }; 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKStoreReviewControllerExtension.swift; sourceTree = ""; }; @@ -591,8 +591,8 @@ 0727877628D23847002E9142 /* DataLayer.swift */, 0727878428D31657002E9142 /* Data_User.swift */, 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */, - 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */, - 02935B722BCECAD000B22F66 /* Data_MyLearnCourses.swift */, + 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */, + 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */, 021D924728DC860C00ACC565 /* Data_UserProfile.swift */, 0259104929C4A5B6004B5A55 /* UserSettings.swift */, 070019A428F6F17900D5FC78 /* Data_Media.swift */, @@ -618,7 +618,7 @@ children = ( 0727878828D31734002E9142 /* User.swift */, 0284DBFD28D48C5300830893 /* CourseItem.swift */, - 02935B742BCEE6D600B22F66 /* MyEnrollments.swift */, + 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */, 021D924F28DC89D100ACC565 /* UserProfile.swift */, 070019AB28F6FD0100D5FC78 /* CourseDetailBlock.swift */, 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */, @@ -1085,7 +1085,7 @@ 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, 06619EAF2B973B25001FAADE /* AccessibilityInjection.swift in Sources */, BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, - 02935B732BCECAD000B22F66 /* Data_MyLearnCourses.swift in Sources */, + 02935B732BCECAD000B22F66 /* Data_PrimaryEnrollment.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 06DEA4A32BBD66A700110D20 /* BackNavigationButton.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, @@ -1129,7 +1129,7 @@ 02AFCC1A2AEFDC18000360F0 /* ThirdPartyMailer.swift in Sources */, 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */, BA981BCE2B8F5C49005707C2 /* Sequence+Extensions.swift in Sources */, - 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */, + 02C917F029CDA99E00DBB8BD /* Data_Enrollments.swift in Sources */, 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */, 027BD3B52909475900392132 /* KeyboardStateObserver.swift in Sources */, 0283347D28D4D3DE00C828FC /* Data_Discovery.swift in Sources */, @@ -1195,7 +1195,7 @@ 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */, 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */, 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */, - 02935B752BCEE6D600B22F66 /* MyEnrollments.swift in Sources */, + 02935B752BCEE6D600B22F66 /* PrimaryEnrollment.swift in Sources */, 020C31C9290AC3F700D6DEA2 /* PickerFields.swift in Sources */, 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */, 023A1138291432FD00D0D354 /* FieldConfiguration.swift in Sources */, diff --git a/Core/Core/Assets.xcassets/learn_big.imageset/Contents.json b/Core/Core/Assets.xcassets/learn_empty.imageset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/learn_big.imageset/Contents.json rename to Core/Core/Assets.xcassets/learn_empty.imageset/Contents.json diff --git a/Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg b/Core/Core/Assets.xcassets/learn_empty.imageset/learn_big.svg similarity index 100% rename from Core/Core/Assets.xcassets/learn_big.imageset/learn_big.svg rename to Core/Core/Assets.xcassets/learn_empty.imageset/learn_big.svg diff --git a/Core/Core/Data/Model/Data_Discovery.swift b/Core/Core/Data/Model/Data_Discovery.swift index cd0584055..e5e4d01d7 100644 --- a/Core/Core/Data/Model/Data_Discovery.swift +++ b/Core/Core/Data/Model/Data_Discovery.swift @@ -108,7 +108,7 @@ public extension DataLayer.DiscoveryResponce { CourseItem(name: $0.name, org: $0.org, shortDescription: $0.shortDescription ?? "", imageURL: $0.media.image?.small ?? "", - isActive: true, + hasAccess: true, courseStart: Date(iso8601: $0.start ?? ""), courseEnd: Date(iso8601: $0.end ?? ""), enrollmentStart: Date(iso8601: $0.enrollmentStart ?? ""), diff --git a/Core/Core/Data/Model/Data_Dashboard.swift b/Core/Core/Data/Model/Data_Enrollments.swift similarity index 98% rename from Core/Core/Data/Model/Data_Dashboard.swift rename to Core/Core/Data/Model/Data_Enrollments.swift index 830447ec5..3ab330e6b 100644 --- a/Core/Core/Data/Model/Data_Dashboard.swift +++ b/Core/Core/Data/Model/Data_Enrollments.swift @@ -1,5 +1,5 @@ // -// Data_Dashboard.swift +// Data_Enrollments.swift // Core // // Created by  Stepanok Ivan on 24.03.2023. @@ -248,7 +248,7 @@ public extension DataLayer.CourseEnrollments { org: course.org, shortDescription: "", imageURL: fullImageURL, - isActive: course.coursewareAccess.hasAccess, + hasAccess: course.coursewareAccess.hasAccess, courseStart: course.start != nil ? Date(iso8601: course.start!) : nil, courseEnd: course.end != nil ? Date(iso8601: course.end!) : nil, enrollmentStart: course.start != nil diff --git a/Core/Core/Data/Model/Data_MyLearnCourses.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift similarity index 88% rename from Core/Core/Data/Model/Data_MyLearnCourses.swift rename to Core/Core/Data/Model/Data_PrimaryEnrollment.swift index 26b00a606..cb8001c29 100644 --- a/Core/Core/Data/Model/Data_MyLearnCourses.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -8,9 +8,9 @@ import Foundation public extension DataLayer { - struct MyLearnCourses: Codable { - public let userTimezone: String - public let enrollments: Enrollments + struct PrimaryEnrollment: Codable { + public let userTimezone: String? + public let enrollments: Enrollments? public let primary: Primary? enum CodingKeys: String, CodingKey { @@ -19,7 +19,7 @@ public extension DataLayer { case primary = "primary" } - public init(userTimezone: String, enrollments: Enrollments, primary: Primary?) { + public init(userTimezone: String?, enrollments: Enrollments?, primary: Primary?) { self.userTimezone = userTimezone self.enrollments = enrollments self.primary = primary @@ -95,10 +95,10 @@ public extension DataLayer { // MARK: - CourseStatus struct CourseStatus: Codable { - public let lastVisitedModuleID: String - public let lastVisitedModulePath: [String] - public let lastVisitedBlockID: String - public let lastVisitedUnitDisplayName: String + public let lastVisitedModuleID: String? + public let lastVisitedModulePath: [String]? + public let lastVisitedBlockID: String? + public let lastVisitedUnitDisplayName: String? enum CodingKeys: String, CodingKey { case lastVisitedModuleID = "last_visited_module_id" @@ -181,23 +181,23 @@ public extension DataLayer { // MARK: - Progress struct Progress: Codable { - public let assignmentsCompleted: Double - public let totalAssignmentsCount: Double + public let assignmentsCompleted: Int? + public let totalAssignmentsCount: Int? enum CodingKeys: String, CodingKey { case assignmentsCompleted = "assignments_completed" case totalAssignmentsCount = "total_assignments_count" } - public init(assignmentsCompleted: Double, totalAssignmentsCount: Double) { + public init(assignmentsCompleted: Int?, totalAssignmentsCount: Int?) { self.assignmentsCompleted = assignmentsCompleted self.totalAssignmentsCount = totalAssignmentsCount } } } -public extension DataLayer.MyLearnCourses { - func domain(baseURL: String) -> MyEnrollments { +public extension DataLayer.PrimaryEnrollment { + func domain(baseURL: String) -> PrimaryEnrollment { var primaryCourse: PrimaryCourse? if let primary = self.primary { let futureAssignments: [DataLayer.Assignment] = primary.courseAssignments?.futureAssignments ?? [] @@ -207,7 +207,7 @@ public extension DataLayer.MyLearnCourses { name: primary.course?.name ?? "", org: primary.course?.org ?? "", courseID: primary.course?.id ?? "", - isActive: primary.course?.coursewareAccess.hasAccess ?? true, + hasAccess: primary.course?.coursewareAccess.hasAccess ?? true, courseStart: primary.course?.start != nil ? Date(iso8601: primary.course?.start ?? "") : nil, @@ -237,11 +237,11 @@ public extension DataLayer.MyLearnCourses { }, progressEarned: primary.progress?.assignmentsCompleted ?? 0, progressPossible: primary.progress?.totalAssignmentsCount ?? 0, - lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, + lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName ) } - let courses = self.enrollments.results.map { + let courses = self.enrollments?.results.map { let imageUrl = $0.course.media.courseImage?.url ?? "" let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let fullImageURL = baseURL + encodedUrl @@ -250,7 +250,7 @@ public extension DataLayer.MyLearnCourses { org: $0.course.org, shortDescription: "", imageURL: fullImageURL, - isActive: $0.course.coursewareAccess.hasAccess, + hasAccess: $0.course.coursewareAccess.hasAccess, courseStart: $0.course.start != nil ? Date(iso8601: $0.course.start!) : nil, @@ -264,18 +264,18 @@ public extension DataLayer.MyLearnCourses { ? Date(iso8601: $0.course.end!) : nil, courseID: $0.course.id, - numPages: enrollments.numPages ?? 1, - coursesCount: enrollments.count ?? 0, + numPages: enrollments?.numPages ?? 1, + coursesCount: enrollments?.count ?? 0, progressEarned: $0.progress?.assignmentsCompleted ?? 0, progressPossible: $0.progress?.totalAssignmentsCount ?? 0 ) } - return MyEnrollments( + return PrimaryEnrollment( primaryCourse: primaryCourse, - courses: courses, - totalPages: enrollments.numPages ?? 1, - count: enrollments.count ?? 1 + courses: courses ?? [], + totalPages: enrollments?.numPages ?? 1, + count: enrollments?.count ?? 1 ) } } diff --git a/Core/Core/Domain/Model/CourseItem.swift b/Core/Core/Domain/Model/CourseItem.swift index f13d5dc2f..dc386058e 100644 --- a/Core/Core/Domain/Model/CourseItem.swift +++ b/Core/Core/Domain/Model/CourseItem.swift @@ -25,7 +25,7 @@ public struct CourseItem: Hashable { public let org: String public let shortDescription: String public let imageURL: String - public let isActive: Bool + public let hasAccess: Bool public let courseStart: Date? public let courseEnd: Date? public let enrollmentStart: Date? @@ -33,14 +33,14 @@ public struct CourseItem: Hashable { public let courseID: String public let numPages: Int public let coursesCount: Int - public let progressEarned: Double - public let progressPossible: Double + public let progressEarned: Int + public let progressPossible: Int public init(name: String, org: String, shortDescription: String, imageURL: String, - isActive: Bool, + hasAccess: Bool, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, @@ -48,13 +48,13 @@ public struct CourseItem: Hashable { courseID: String, numPages: Int, coursesCount: Int, - progressEarned: Double, - progressPossible: Double) { + progressEarned: Int, + progressPossible: Int) { self.name = name self.org = org self.shortDescription = shortDescription self.imageURL = imageURL - self.isActive = isActive + self.hasAccess = hasAccess self.courseStart = courseStart self.courseEnd = courseEnd self.enrollmentStart = enrollmentStart diff --git a/Core/Core/Domain/Model/MyEnrollments.swift b/Core/Core/Domain/Model/PrimaryEnrollment.swift similarity index 88% rename from Core/Core/Domain/Model/MyEnrollments.swift rename to Core/Core/Domain/Model/PrimaryEnrollment.swift index 5236fe370..d894fd9ab 100644 --- a/Core/Core/Domain/Model/MyEnrollments.swift +++ b/Core/Core/Domain/Model/PrimaryEnrollment.swift @@ -1,5 +1,5 @@ // -// MyEnrollments.swift +// PrimaryEnrollment.swift // Core // // Created by  Stepanok Ivan on 16.04.2024. @@ -7,7 +7,7 @@ import Foundation -public struct MyEnrollments: Hashable { +public struct PrimaryEnrollment: Hashable { public let primaryCourse: PrimaryCourse? public var courses: [CourseItem] public let totalPages: Int @@ -25,14 +25,14 @@ public struct PrimaryCourse: Hashable { public let name: String public let org: String public let courseID: String - public let isActive: Bool + public let hasAccess: Bool public let courseStart: Date? public let courseEnd: Date? public let courseBanner: String public let futureAssignments: [Assignment] public let pastAssignments: [Assignment] - public let progressEarned: Double? - public let progressPossible: Double? + public let progressEarned: Int? + public let progressPossible: Int? public let lastVisitedBlockID: String? public let resumeTitle: String? @@ -40,21 +40,21 @@ public struct PrimaryCourse: Hashable { name: String, org: String, courseID: String, - isActive: Bool, + hasAccess: Bool, courseStart: Date?, courseEnd: Date?, courseBanner: String, futureAssignments: [Assignment], pastAssignments: [Assignment], - progressEarned: Double?, - progressPossible: Double?, + progressEarned: Int?, + progressPossible: Int?, lastVisitedBlockID: String?, resumeTitle: String? ) { self.name = name self.org = org self.courseID = courseID - self.isActive = isActive + self.hasAccess = hasAccess self.courseStart = courseStart self.courseEnd = courseEnd self.courseBanner = courseBanner diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index cf2f76fe0..f951dcdfb 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -102,7 +102,7 @@ public enum CoreAssets { public static let edit = ImageAsset(name: "edit") public static let favorite = ImageAsset(name: "favorite") public static let goodWork = ImageAsset(name: "goodWork") - public static let learnBig = ImageAsset(name: "learn_big") + public static let learnEmpty = ImageAsset(name: "learn_empty") public static let airmail = ImageAsset(name: "airmail") public static let defaultMail = ImageAsset(name: "defaultMail") public static let fastmail = ImageAsset(name: "fastmail") diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index bbf5c4f8f..72f02b2bb 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -130,7 +130,7 @@ struct CourseCellView_Previews: PreviewProvider { org: "Edx", shortDescription: "", imageURL: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", - isActive: true, + hasAccess: true, courseStart: Date(iso8601: "2032-05-26T12:13:14Z"), courseEnd: Date(iso8601: "2033-05-26T12:13:14Z"), enrollmentStart: nil, diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents index dda82f3ae..d25cbc5ad 100644 --- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents +++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -65,8 +65,8 @@ + - @@ -101,4 +101,4 @@ - + \ No newline at end of file diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index 7dd326257..251f19f10 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -9,12 +9,11 @@ import Foundation import Core public protocol DashboardRepositoryProtocol { - func getMyCourses(page: Int) async throws -> [CourseItem] - func getMyCoursesOffline() throws -> [CourseItem] - func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments - func getMyLearnCoursesOffline() async throws -> MyEnrollments - func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments - func getAllCoursesOffline() async throws -> MyEnrollments + func getEnrollments(page: Int) async throws -> [CourseItem] + func getEnrollmentsOffline() throws -> [CourseItem] + func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment + func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment + func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment } public class DashboardRepository: DashboardRepositoryProtocol { @@ -31,7 +30,7 @@ public class DashboardRepository: DashboardRepositoryProtocol { self.persistence = persistence } - public func getMyCourses(page: Int) async throws -> [CourseItem] { + public func getEnrollments(page: Int) async throws -> [CourseItem] { let result = try await api.requestData( DashboardEndpoint.getMyCourses(username: storage.user?.username ?? "", page: page) ) @@ -42,28 +41,28 @@ public class DashboardRepository: DashboardRepositoryProtocol { } - public func getMyCoursesOffline() throws -> [CourseItem] { + public func getEnrollmentsOffline() throws -> [CourseItem] { return try persistence.loadMyCourses() } - public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { let result = try await api.requestData( DashboardEndpoint.getMyLearnCourses( username: storage.user?.username ?? "", pageSize: pageSize ) ) - .mapResponse(DataLayer.MyLearnCourses.self) + .mapResponse(DataLayer.PrimaryEnrollment.self) .domain(baseURL: config.baseURL.absoluteString) persistence.saveMyEnrollments(enrollments: result) return result } - public func getMyLearnCoursesOffline() async throws -> MyEnrollments { + public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { return try persistence.loadMyEnrollments() } - public func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments { + public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { let result = try await api.requestData( DashboardEndpoint.getAllCourses( username: storage.user?.username ?? "", @@ -71,34 +70,18 @@ public class DashboardRepository: DashboardRepositoryProtocol { page: page ) ) - .mapResponse(DataLayer.MyLearnCourses.self) + .mapResponse(DataLayer.PrimaryEnrollment.self) .domain(baseURL: config.baseURL.absoluteString) -// persistence.saveMyCourses(items: result.courses) return result } - - public func getAllCoursesOffline() async throws -> MyEnrollments { -// let courses = try persistence.loadMyCourses() - return MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) - } } +// swiftlint:disable all // Mark - For testing and SwiftUI preview #if DEBUG class DashboardRepositoryMock: DashboardRepositoryProtocol { - func getCourseEnrollments(baseURL: String) async throws -> [CourseItem] { - do { - let courseEnrollments = try - DashboardRepository.CourseEnrollmentsJSON.data(using: .utf8)! - .mapResponse(DataLayer.CourseEnrollments.self) - .domain(baseURL: baseURL) - return courseEnrollments - } catch { - throw error - } - } - func getMyCourses(page: Int) async throws -> [CourseItem] { + func getEnrollments(page: Int) async throws -> [CourseItem] { var models: [CourseItem] = [] for i in 0...10 { models.append( @@ -107,7 +90,7 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, @@ -123,9 +106,9 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { return models } - func getMyCoursesOffline() throws -> [CourseItem] { return [] } + func getEnrollmentsOffline() throws -> [CourseItem] { return [] } - public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { var courses: [CourseItem] = [] for i in 0...10 { @@ -135,7 +118,7 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, @@ -162,7 +145,7 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { name: "Primary Course", org: "Organization", courseID: "123", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: Date(), courseBanner: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", @@ -173,15 +156,14 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { lastVisitedBlockID: nil, resumeTitle: nil ) - return MyEnrollments(primaryCourse: primaryCourse, courses: courses, totalPages: 1, count: 1) + return PrimaryEnrollment(primaryCourse: primaryCourse, courses: courses, totalPages: 1, count: 1) } - func getMyLearnCoursesOffline() async throws -> Core.MyEnrollments { - Core.MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + func getPrimaryEnrollmentOffline() async throws -> Core.PrimaryEnrollment { + Core.PrimaryEnrollment(primaryCourse: nil, courses: [], totalPages: 1, count: 1) } - - func getAllCourses(filteredBy: String, page: Int) async throws -> Core.MyEnrollments { + func getAllCourses(filteredBy: String, page: Int) async throws -> Core.PrimaryEnrollment { var courses: [CourseItem] = [] for i in 0...10 { courses.append( @@ -190,7 +172,7 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, @@ -204,11 +186,8 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { ) } - return MyEnrollments(primaryCourse: nil, courses: courses, totalPages: 1, count: 1) - } - - func getAllCoursesOffline() async throws -> Core.MyEnrollments { - Core.MyEnrollments(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + return PrimaryEnrollment(primaryCourse: nil, courses: courses, totalPages: 1, count: 1) } } #endif +// swiftlint:enable all diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents index 5d055d271..d3bea41fc 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents +++ b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents @@ -22,13 +22,13 @@ + - - - + + @@ -47,12 +47,12 @@ - + - - + + diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift index cf6fe2495..7ea61cccb 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift @@ -11,8 +11,8 @@ import Core public protocol DashboardPersistenceProtocol { func loadMyCourses() throws -> [CourseItem] func saveMyCourses(items: [CourseItem]) - func loadMyEnrollments() throws -> MyEnrollments - func saveMyEnrollments(enrollments: MyEnrollments) + func loadMyEnrollments() throws -> PrimaryEnrollment + func saveMyEnrollments(enrollments: PrimaryEnrollment) } public final class DashboardBundle { diff --git a/Dashboard/Dashboard/Domain/DashboardInteractor.swift b/Dashboard/Dashboard/Domain/DashboardInteractor.swift index dea12037f..0d55f0a4e 100644 --- a/Dashboard/Dashboard/Domain/DashboardInteractor.swift +++ b/Dashboard/Dashboard/Domain/DashboardInteractor.swift @@ -10,12 +10,11 @@ import Core //sourcery: AutoMockable public protocol DashboardInteractorProtocol { - func getMyCourses(page: Int) async throws -> [CourseItem] - func discoveryOffline() throws -> [CourseItem] - func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments - func getMyLearnCoursesOffline() async throws -> MyEnrollments - func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments - func getAllCoursesOffline() async throws -> MyEnrollments + func getEnrollments(page: Int) async throws -> [CourseItem] + func getEnrollmentsOffline() throws -> [CourseItem] + func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment + func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment + func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment } public class DashboardInteractor: DashboardInteractorProtocol { @@ -27,29 +26,25 @@ public class DashboardInteractor: DashboardInteractorProtocol { } @discardableResult - public func getMyCourses(page: Int) async throws -> [CourseItem] { - return try await repository.getMyCourses(page: page) + public func getEnrollments(page: Int) async throws -> [CourseItem] { + return try await repository.getEnrollments(page: page) } - public func discoveryOffline() throws -> [CourseItem] { - return try repository.getMyCoursesOffline() + public func getEnrollmentsOffline() throws -> [CourseItem] { + return try repository.getEnrollmentsOffline() } - public func getMyLearnCourses(pageSize: Int) async throws -> MyEnrollments { - return try await repository.getMyLearnCourses(pageSize: pageSize) + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { + return try await repository.getPrimaryEnrollment(pageSize: pageSize) } - public func getMyLearnCoursesOffline() async throws -> MyEnrollments { - return try await repository.getMyLearnCoursesOffline() + public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { + return try await repository.getPrimaryEnrollmentOffline() } - public func getAllCourses(filteredBy: String, page: Int) async throws -> MyEnrollments { + public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { return try await repository.getAllCourses(filteredBy: filteredBy, page: page) } - - public func getAllCoursesOffline() async throws -> MyEnrollments { - return try await repository.getAllCoursesOffline() - } } // Mark - For testing and SwiftUI preview diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 5233652cf..a11e240ac 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -67,7 +67,7 @@ public struct AllCoursesView: View { ) router.showCourseScreens( courseID: course.courseID, - isActive: course.isActive, + hasAccess: course.hasAccess, courseStart: course.courseStart, courseEnd: course.courseEnd, enrollmentStart: course.enrollmentStart, @@ -84,7 +84,7 @@ public struct AllCoursesView: View { progressPossible: course.progressPossible, courseStartDate: course.courseStart, courseEndDate: course.courseEnd, - isActive: course.isActive, + hasAccess: course.hasAccess, isFullCard: false ).padding(8) }) diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift index 54d180bff..4b74aff3d 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -12,12 +12,12 @@ import Combine public class AllCoursesViewModel: ObservableObject { - public var nextPage = 1 - public var totalPages = 1 - @Published public private(set) var fetchInProgress = false + var nextPage = 1 + var totalPages = 1 + @Published private(set) var fetchInProgress = false @Published var selectedMenu: CategoryOption = .all - @Published var myEnrollments: MyEnrollments? + @Published var myEnrollments: PrimaryEnrollment? @Published var showError: Bool = false var errorMessage: String? { didSet { @@ -32,9 +32,11 @@ public class AllCoursesViewModel: ObservableObject { private let analytics: DashboardAnalytics private var onCourseEnrolledCancellable: AnyCancellable? - public init(interactor: DashboardInteractorProtocol, - connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics) { + public init( + interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics + ) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics @@ -53,27 +55,20 @@ public class AllCoursesViewModel: ObservableObject { public func getCourses(page: Int, refresh: Bool = false) async { fetchInProgress = true do { - if connectivity.isInternetAvaliable { - if refresh || page == 1 { - myEnrollments?.courses = [] - nextPage = 1 - myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) - self.totalPages = myEnrollments?.totalPages ?? 1 - self.nextPage = 2 - } else { - myEnrollments?.courses += try await interactor.getAllCourses( - filteredBy: selectedMenu.status, page: page - ).courses - self.nextPage += 1 - } - totalPages = myEnrollments?.totalPages ?? 1 - fetchInProgress = false - } else { - self.totalPages = 1 + if refresh || page == 1 { + myEnrollments?.courses = [] + nextPage = 1 + myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) + self.totalPages = myEnrollments?.totalPages ?? 1 self.nextPage = 2 - myEnrollments = try await interactor.getAllCoursesOffline() - fetchInProgress = false + } else { + myEnrollments?.courses += try await interactor.getAllCourses( + filteredBy: selectedMenu.status, page: page + ).courses + self.nextPage += 1 } + totalPages = myEnrollments?.totalPages ?? 1 + fetchInProgress = false } catch let error { fetchInProgress = false if error is NoCachedDataError { diff --git a/Dashboard/Dashboard/Presentation/DashboardRouter.swift b/Dashboard/Dashboard/Presentation/DashboardRouter.swift index 7d548e016..0d4397a19 100644 --- a/Dashboard/Dashboard/Presentation/DashboardRouter.swift +++ b/Dashboard/Dashboard/Presentation/DashboardRouter.swift @@ -11,7 +11,7 @@ import Core public protocol DashboardRouter: BaseRouter { func showCourseScreens(courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, @@ -33,7 +33,7 @@ public class DashboardRouterMock: BaseRouterMock, DashboardRouter { public override init() {} public func showCourseScreens(courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift index 2cfa1bd53..97395999b 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -14,21 +14,21 @@ struct CourseCardView: View { private let courseName: String private let courseImage: String - private let progressEarned: Double - private let progressPossible: Double + private let progressEarned: Int + private let progressPossible: Int private let courseStartDate: Date? private let courseEndDate: Date? - private let isActive: Bool + private let hasAccess: Bool private let isFullCard: Bool init( courseName: String, courseImage: String, - progressEarned: Double, - progressPossible: Double, + progressEarned: Int, + progressPossible: Int, courseStartDate: Date?, courseEndDate: Date?, - isActive: Bool, + hasAccess: Bool, isFullCard: Bool ) { self.courseName = courseName @@ -37,7 +37,7 @@ struct CourseCardView: View { self.progressPossible = progressPossible self.courseStartDate = courseStartDate self.courseEndDate = courseEndDate - self.isActive = isActive + self.hasAccess = hasAccess self.isFullCard = isFullCard } @@ -54,7 +54,7 @@ struct CourseCardView: View { } courseTitle } - if !isActive { + if !hasAccess { ZStack(alignment: .center) { Circle() .foregroundStyle(Theme.Colors.primaryHeaderColor) @@ -118,7 +118,7 @@ struct CourseCardView: View { progressPossible: 8, courseStartDate: nil, courseEndDate: Date(), - isActive: true, + hasAccess: true, isFullCard: true ).frame(width: 170) } diff --git a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift index 4b2620299..132cd17fa 100644 --- a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift @@ -69,7 +69,7 @@ struct NoCoursesView: View { var body: some View { VStack(spacing: 8) { Spacer() - CoreAssets.learnBig.swiftUIImage + CoreAssets.learnEmpty.swiftUIImage .resizable() .frame(width: 96, height: 96) .foregroundStyle(Theme.Colors.textSecondaryLight) diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 679eb70bc..3815a275e 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -19,8 +19,8 @@ public struct PrimaryCardView: View { private let courseEndDate: Date? private var futureAssignments: [Assignment]? private let pastAssignments: [Assignment]? - private let progressEarned: Double - private let progressPossible: Double + private let progressEarned: Int + private let progressPossible: Int private let canResume: Bool private let resumeTitle: String? private var pastAssignmentAction: (String?) -> Void @@ -35,8 +35,8 @@ public struct PrimaryCardView: View { courseEndDate: Date?, futureAssignments: [Assignment]?, pastAssignments: [Assignment]?, - progressEarned: Double, - progressPossible: Double, + progressEarned: Int, + progressPossible: Int, canResume: Bool, resumeTitle: String?, pastAssignmentAction: @escaping (String?) -> Void, diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift index b124e908c..548b58e89 100644 --- a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -9,8 +9,8 @@ import SwiftUI import Theme struct ProgressLineView: View { - private let progressEarned: Double - private let progressPossible: Double + private let progressEarned: Int + private let progressPossible: Int private let height: CGFloat var progressValue: CGFloat { @@ -18,7 +18,7 @@ struct ProgressLineView: View { return CGFloat(progressEarned) / CGFloat(progressPossible) } - init(progressEarned: Double, progressPossible: Double, height: CGFloat = 8) { + init(progressEarned: Int, progressPossible: Int, height: CGFloat = 8) { self.progressEarned = progressEarned self.progressPossible = progressPossible self.height = height diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index dfb01ba5e..4e000b99f 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -76,7 +76,7 @@ public struct ListDashboardView: View { ) router.showCourseScreens( courseID: course.courseID, - isActive: course.isActive, + hasAccess: course.hasAccess, courseStart: course.courseStart, courseEnd: course.courseEnd, enrollmentStart: course.enrollmentStart, diff --git a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index 023c20ca3..4e79877c2 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -54,11 +54,11 @@ public class ListDashboardViewModel: ObservableObject { fetchInProgress = true if connectivity.isInternetAvaliable { if refresh { - courses = try await interactor.getMyCourses(page: page) + courses = try await interactor.getEnrollments(page: page) self.totalPages = 1 self.nextPage = 2 } else { - courses += try await interactor.getMyCourses(page: page) + courses += try await interactor.getEnrollments(page: page) self.nextPage += 1 } if !courses.isEmpty { @@ -66,7 +66,7 @@ public class ListDashboardViewModel: ObservableObject { } fetchInProgress = false } else { - courses = try interactor.discoveryOffline() + courses = try interactor.getEnrollmentsOffline() self.nextPage += 1 fetchInProgress = false } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 99a606bb3..5977659b7 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -37,7 +37,7 @@ public struct PrimaryCourseDashboardView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - if !viewModel.fetchInProgress, viewModel.myEnrollments?.primaryCourse == nil { + if !viewModel.fetchInProgress, viewModel.enrollments?.primaryCourse == nil { NoCoursesView(openDiscovery: { openDiscoveryPage() }).zIndex(1) @@ -45,12 +45,12 @@ public struct PrimaryCourseDashboardView: View { // MARK: - Page body VStack(alignment: .center) { RefreshableScrollViewCompat(action: { - await viewModel.getMyLearnings(showProgress: false) + await viewModel.getEnrollments(showProgress: false) }) { ZStack(alignment: .topLeading) { learnTitleAndSearch() .zIndex(1) - if !viewModel.fetchInProgress, viewModel.myEnrollments?.primaryCourse == nil { + if !viewModel.fetchInProgress, viewModel.enrollments?.primaryCourse == nil { } else if viewModel.fetchInProgress { VStack(alignment: .center) { @@ -63,8 +63,8 @@ public struct PrimaryCourseDashboardView: View { Spacer(minLength: 50) switch selectedMenu { case .courses: - if let myEnrollments = viewModel.myEnrollments { - if let primary = myEnrollments.primaryCourse { + if let enrollments = viewModel.enrollments { + if let primary = enrollments.primaryCourse { PrimaryCardView( courseName: primary.name, org: primary.org, @@ -80,7 +80,7 @@ public struct PrimaryCourseDashboardView: View { pastAssignmentAction: { lastVisitedBlockID in router.showCourseScreens( courseID: primary.courseID, - isActive: primary.isActive, + hasAccess: primary.hasAccess, courseStart: primary.courseStart, courseEnd: primary.courseEnd, enrollmentStart: nil, @@ -93,7 +93,7 @@ public struct PrimaryCourseDashboardView: View { futureAssignmentAction: { lastVisitedBlockID in router.showCourseScreens( courseID: primary.courseID, - isActive: primary.isActive, + hasAccess: primary.hasAccess, courseStart: primary.courseStart, courseEnd: primary.courseEnd, enrollmentStart: nil, @@ -106,7 +106,7 @@ public struct PrimaryCourseDashboardView: View { resumeAction: { router.showCourseScreens( courseID: primary.courseID, - isActive: primary.isActive, + hasAccess: primary.hasAccess, courseStart: primary.courseStart, courseEnd: primary.courseEnd, enrollmentStart: nil, @@ -118,13 +118,13 @@ public struct PrimaryCourseDashboardView: View { } ) } - if !myEnrollments.courses.isEmpty { - viewAll(myEnrollments) + if !enrollments.courses.isEmpty { + viewAll(enrollments) } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach( - Array(myEnrollments.courses.enumerated()), + Array(enrollments.courses.enumerated()), id: \.offset ) { _, course in Button(action: { @@ -134,7 +134,7 @@ public struct PrimaryCourseDashboardView: View { ) router.showCourseScreens( courseID: course.courseID, - isActive: course.isActive, + hasAccess: course.hasAccess, courseStart: course.courseStart, courseEnd: course.courseEnd, enrollmentStart: course.enrollmentStart, @@ -151,15 +151,15 @@ public struct PrimaryCourseDashboardView: View { progressPossible: 0, courseStartDate: nil, courseEndDate: nil, - isActive: course.isActive, + hasAccess: course.hasAccess, isFullCard: false ).frame(width: 120) } ) .accessibilityIdentifier("course_item") } - if myEnrollments.courses.count < myEnrollments.count { - viewAllButton(myEnrollments) + if enrollments.courses.count < enrollments.count { + viewAllButton(enrollments) } } .padding(20) @@ -180,7 +180,7 @@ public struct PrimaryCourseDashboardView: View { // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, reloadAction: { - await viewModel.getMyLearnings(showProgress: false) + await viewModel.getEnrollments(showProgress: false) }) // MARK: - Error Alert @@ -201,7 +201,7 @@ public struct PrimaryCourseDashboardView: View { } .onFirstAppear { Task { - await viewModel.getMyLearnings() + await viewModel.getEnrollments() } } .background( @@ -214,9 +214,9 @@ public struct PrimaryCourseDashboardView: View { } } - private func viewAllButton(_ myEnrollments: MyEnrollments) -> some View { + private func viewAllButton(_ enrollments: PrimaryEnrollment) -> some View { Button(action: { - router.showAllCourses(courses: myEnrollments.courses) + router.showAllCourses(courses: enrollments.courses) }, label: { ZStack(alignment: .topTrailing) { VStack(alignment: .leading, spacing: 0) { @@ -236,12 +236,12 @@ public struct PrimaryCourseDashboardView: View { }) } - private func viewAll(_ myEnrollments: MyEnrollments) -> some View { + private func viewAll(_ enrollments: PrimaryEnrollment) -> some View { Button(action: { - router.showAllCourses(courses: myEnrollments.courses) + router.showAllCourses(courses: enrollments.courses) }, label: { HStack { - Text(DashboardLocalization.Learn.viewAllCourses(myEnrollments.count)) + Text(DashboardLocalization.Learn.viewAllCourses(enrollments.count)) .font(Theme.Fonts.titleSmall) .accessibilityIdentifier("courses_welcomeback_text") Image(systemName: "chevron.right") diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 1ac8b28d8..599630511 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -16,7 +16,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { public var totalPages = 1 @Published public private(set) var fetchInProgress = false - @Published var myEnrollments: MyEnrollments? + @Published var enrollments: PrimaryEnrollment? @Published var showError: Bool = false var errorMessage: String? { didSet { @@ -43,21 +43,21 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { .sink { [weak self] _ in guard let self = self else { return } Task { - await self.getMyLearnings() + await self.getEnrollments() } } } @MainActor - public func getMyLearnings(showProgress: Bool = true) async { + public func getEnrollments(showProgress: Bool = true) async { let pageSize = UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5 fetchInProgress = showProgress do { if connectivity.isInternetAvaliable { - myEnrollments = try await interactor.getMyLearnCourses(pageSize: pageSize) + enrollments = try await interactor.getPrimaryEnrollment(pageSize: pageSize) fetchInProgress = false } else { - myEnrollments = try await interactor.getMyLearnCoursesOffline() + enrollments = try await interactor.getPrimaryEnrollmentOffline() fetchInProgress = false } } catch let error { diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index a20e73f6a..35bb2fba5 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1641,11 +1641,11 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } - open func getMyLearnCourses(pageSize: Int) throws -> MyEnrollments { + open func getMyLearnCourses(pageSize: Int) throws -> PrimaryEnrollment { addInvocation(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) let perform = methodPerformValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) as? (Int) -> Void perform?(`pageSize`) - var __value: MyEnrollments + var __value: PrimaryEnrollment do { __value = try methodReturnValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))).casted() } catch MockError.notStubed { @@ -1657,11 +1657,11 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } - open func getMyLearnCoursesOffline() throws -> MyEnrollments { + open func getMyLearnCoursesOffline() throws -> PrimaryEnrollment { addInvocation(.m_getMyLearnCoursesOffline) let perform = methodPerformValue(.m_getMyLearnCoursesOffline) as? () -> Void perform?() - var __value: MyEnrollments + var __value: PrimaryEnrollment do { __value = try methodReturnValue(.m_getMyLearnCoursesOffline).casted() } catch MockError.notStubed { @@ -1673,11 +1673,11 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } - open func getAllCourses(filteredBy: String, page: Int) throws -> MyEnrollments { + open func getAllCourses(filteredBy: String, page: Int) throws -> PrimaryEnrollment { addInvocation(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) let perform = methodPerformValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) as? (String, Int) -> Void perform?(`filteredBy`, `page`) - var __value: MyEnrollments + var __value: PrimaryEnrollment do { __value = try methodReturnValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))).casted() } catch MockError.notStubed { @@ -1689,11 +1689,11 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } - open func getAllCoursesOffline() throws -> MyEnrollments { + open func getAllCoursesOffline() throws -> PrimaryEnrollment { addInvocation(.m_getAllCoursesOffline) let perform = methodPerformValue(.m_getAllCoursesOffline) as? () -> Void perform?() - var __value: MyEnrollments + var __value: PrimaryEnrollment do { __value = try methodReturnValue(.m_getAllCoursesOffline).casted() } catch MockError.notStubed { @@ -1778,16 +1778,16 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { public static func discoveryOffline(willReturn: [CourseItem]...) -> MethodStub { return Given(method: .m_discoveryOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyLearnCourses(pageSize: Parameter, willReturn: MyEnrollments...) -> MethodStub { + public static func getMyLearnCourses(pageSize: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyLearnCoursesOffline(willReturn: MyEnrollments...) -> MethodStub { + public static func getMyLearnCoursesOffline(willReturn: PrimaryEnrollment...) -> MethodStub { return Given(method: .m_getMyLearnCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getAllCourses(filteredBy: Parameter, page: Parameter, willReturn: MyEnrollments...) -> MethodStub { + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getAllCoursesOffline(willReturn: MyEnrollments...) -> MethodStub { + public static func getAllCoursesOffline(willReturn: PrimaryEnrollment...) -> MethodStub { return Given(method: .m_getAllCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getMyCourses(page: Parameter, willThrow: Error...) -> MethodStub { @@ -1813,40 +1813,40 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { public static func getMyLearnCourses(pageSize: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyLearnCourses(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getMyLearnCourses(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (MyEnrollments).self) + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given } public static func getMyLearnCoursesOffline(willThrow: Error...) -> MethodStub { return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyLearnCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getMyLearnCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (MyEnrollments).self) + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given } public static func getAllCourses(filteredBy: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getAllCourses(filteredBy: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (MyEnrollments).self) + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given } public static func getAllCoursesOffline(willThrow: Error...) -> MethodStub { return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) } - public static func getAllCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getAllCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (MyEnrollments).self) + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given } diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index dcd1b1060..1d6f5e0a9 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -128,7 +128,7 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, @@ -152,7 +152,7 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, @@ -176,7 +176,7 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents index 154df9ca8..2c838b0dd 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents +++ b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -27,8 +27,8 @@ + - diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index e3d393b21..9a8134ca7 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -20,7 +20,7 @@ public protocol DiscoveryRouter: BaseRouter { func showDiscoverySearch(searchQuery: String?) func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, @@ -53,7 +53,7 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { public func showDiscoverySearch(searchQuery: String? = nil) {} public func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 1d9723245..63592be0a 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -275,7 +275,7 @@ private struct CourseStateView: View { ) viewModel.router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 5335984de..f431e7363 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -231,7 +231,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index cb5d18a5f..dc4387e6e 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -219,7 +219,7 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index e1031011d..74005ca53 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -25,7 +25,7 @@ public class CoursePersistence: CoursePersistenceProtocol { org: $0.org ?? "", shortDescription: $0.desc ?? "", imageURL: $0.imageURL ?? "", - isActive: $0.isActive, + hasAccess: $0.hasAccess, courseStart: $0.courseStart, courseEnd: $0.courseEnd, enrollmentStart: $0.enrollmentStart, @@ -50,7 +50,7 @@ public class CoursePersistence: CoursePersistenceProtocol { newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - newItem.isActive = item.isActive + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index e656ca1a8..0279b8174 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -24,7 +24,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { org: $0.org ?? "", shortDescription: $0.desc ?? "", imageURL: $0.imageURL ?? "", - isActive: $0.isActive, + hasAccess: $0.hasAccess, courseStart: $0.courseStart, courseEnd: $0.courseEnd, enrollmentStart: $0.enrollmentStart, @@ -50,7 +50,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - newItem.isActive = item.isActive + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart @@ -67,7 +67,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } - public func loadMyEnrollments() throws -> MyEnrollments { + public func loadMyEnrollments() throws -> PrimaryEnrollment { let request = CDMyEnrollments.fetchRequest() if let result = try context.fetch(request).first { let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in @@ -100,15 +100,15 @@ public class DashboardPersistence: DashboardPersistenceProtocol { name: cdPrimaryCourse.name ?? "", org: cdPrimaryCourse.org ?? "", courseID: cdPrimaryCourse.courseID ?? "", - isActive: cdPrimaryCourse.isActive, + hasAccess: cdPrimaryCourse.hasAccess, courseStart: cdPrimaryCourse.courseStart, courseEnd: cdPrimaryCourse.courseEnd, courseBanner: cdPrimaryCourse.courseBanner ?? "", futureAssignments: futureAssignments, pastAssignments: pastAssignments, - progressEarned: cdPrimaryCourse.progressEarned, - progressPossible: cdPrimaryCourse.progressPossible, - lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", + progressEarned: Int(cdPrimaryCourse.progressEarned), + progressPossible: Int(cdPrimaryCourse.progressPossible), + lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", resumeTitle: cdPrimaryCourse.resumeTitle ) } @@ -120,7 +120,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { org: cdCourse.org ?? "", shortDescription: cdCourse.desc ?? "", imageURL: cdCourse.imageURL ?? "", - isActive: cdCourse.isActive, + hasAccess: cdCourse.hasAccess, courseStart: cdCourse.courseStart, courseEnd: cdCourse.courseEnd, enrollmentStart: cdCourse.enrollmentStart, @@ -128,12 +128,12 @@ public class DashboardPersistence: DashboardPersistenceProtocol { courseID: cdCourse.courseID ?? "", numPages: Int(cdCourse.numPages), coursesCount: Int(cdCourse.courseCount), - progressEarned: cdCourse.progressEarned, - progressPossible: cdCourse.progressPossible + progressEarned: Int(cdCourse.progressEarned), + progressPossible: Int(cdCourse.progressPossible) ) } - return MyEnrollments( + return PrimaryEnrollment( primaryCourse: primaryCourse, courses: courses, totalPages: Int(result.totalPages), @@ -145,7 +145,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } // swiftlint:disable function_body_length - public func saveMyEnrollments(enrollments: MyEnrollments) { + public func saveMyEnrollments(enrollments: PrimaryEnrollment) { context.performAndWait { let request: NSFetchRequest = CDMyEnrollments.fetchRequest() @@ -225,12 +225,12 @@ public class DashboardPersistence: DashboardPersistenceProtocol { cdPrimaryCourse.name = primaryCourse.name cdPrimaryCourse.org = primaryCourse.org cdPrimaryCourse.courseID = primaryCourse.courseID - cdPrimaryCourse.isActive = primaryCourse.isActive + cdPrimaryCourse.hasAccess = primaryCourse.hasAccess cdPrimaryCourse.courseStart = primaryCourse.courseStart cdPrimaryCourse.courseEnd = primaryCourse.courseEnd cdPrimaryCourse.courseBanner = primaryCourse.courseBanner - cdPrimaryCourse.progressEarned = primaryCourse.progressEarned ?? 0 - cdPrimaryCourse.progressPossible = primaryCourse.progressPossible ?? 0 + cdPrimaryCourse.progressEarned = Int32(primaryCourse.progressEarned ?? 0) + cdPrimaryCourse.progressPossible = Int32(primaryCourse.progressPossible ?? 0) cdPrimaryCourse.lastVisitedBlockID = primaryCourse.lastVisitedBlockID cdPrimaryCourse.resumeTitle = primaryCourse.resumeTitle diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift index 3ee35927c..2e6d443bd 100644 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ b/OpenEdX/Data/DiscoveryPersistence.swift @@ -24,7 +24,7 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { org: $0.org ?? "", shortDescription: $0.desc ?? "", imageURL: $0.imageURL ?? "", - isActive: $0.isActive, + hasAccess: $0.hasAccess, courseStart: $0.courseStart, courseEnd: $0.courseEnd, enrollmentStart: $0.enrollmentStart, @@ -50,7 +50,7 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - newItem.isActive = item.isActive + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 4ede7b649..ae237a0da 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -107,7 +107,7 @@ extension Router: DeepLinkRouter { if courseDetails.isEnrolled { showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index d835ba149..613d5eaa8 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -182,10 +182,10 @@ public class PipManager: PipManagerProtocol { for holder: PlayerViewControllerHolder ) async throws -> UIHostingController { let courseDetails = try await getCourseDetails(for: holder) - let isActive: Bool? = nil + let hasAccess: Bool? = nil let controller = router.getCourseScreensController( courseID: courseDetails.courseID, - isActive: isActive, + hasAccess: hasAccess, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 1478f1a57..c287531d8 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -359,7 +359,7 @@ public class Router: AuthorizationRouter, public func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, @@ -370,7 +370,7 @@ public class Router: AuthorizationRouter, ) { let controller = getCourseScreensController( courseID: courseID, - isActive: isActive, + hasAccess: hasAccess, courseStart: courseStart, courseEnd: courseEnd, enrollmentStart: enrollmentStart, @@ -384,7 +384,7 @@ public class Router: AuthorizationRouter, public func getCourseScreensController( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, @@ -395,7 +395,7 @@ public class Router: AuthorizationRouter, ) -> UIHostingController { let vm = Container.shared.resolve( CourseContainerViewModel.self, - arguments: isActive, + arguments: hasAccess, courseStart, courseEnd, enrollmentStart, From ced59e3c7cb3cbd2b47008ce3f76f582ca2822b8 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 10 May 2024 12:39:48 +0300 Subject: [PATCH 04/22] fix: address feedback --- .../Data/Model/Data_PrimaryEnrollment.swift | 2 +- Core/Core/Domain/Model/CourseItem.swift | 13 - .../Core/Domain/Model/PrimaryEnrollment.swift | 8 +- .../Container/CourseContainerViewModel.swift | 11 + .../Dashboard/Data/DashboardRepository.swift | 12 +- .../Data/Network/DashboardEndpoint.swift | 14 +- .../DashboardPersistenceProtocol.swift | 8 +- .../Presentation/AllCoursesView.swift | 32 +- .../Presentation/DashboardRouter.swift | 4 +- .../Presentation/Elements/DropDownMenu.swift | 72 ++-- .../Presentation/Elements/NoCoursesView.swift | 11 +- .../Elements/PrimaryCardView.swift | 51 ++- .../Presentation/ListDashboardView.swift | 2 +- .../PrimaryCourseDashboardView.swift | 376 ++++++++++-------- .../PrimaryCourseDashboardViewModel.swift | 20 +- .../DashboardMock.generated.swift | 190 ++++----- .../DashboardViewModelTests.swift | 24 +- .../Presentation/DiscoveryRouter.swift | 4 +- .../NativeDiscovery/CourseDetailsView.swift | 2 +- .../DiscoveryWebviewViewModel.swift | 2 +- .../WebPrograms/ProgramWebviewViewModel.swift | 2 +- .../DiscoveryViewModelTests.swift | 12 +- .../Presentation/SearchViewModelTests.swift | 4 +- OpenEdX.xcodeproj/project.pbxproj | 48 +-- OpenEdX/Data/DashboardPersistence.swift | 8 +- .../DeepLinkRouter/DeepLinkRouter.swift | 2 +- OpenEdX/Managers/PipManager.swift | 2 +- OpenEdX/Router.swift | 8 +- 28 files changed, 471 insertions(+), 473 deletions(-) diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index cb8001c29..59bfd3485 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -1,5 +1,5 @@ // -// Data_MyLearnCourses.swift +// Data_PrimaryEnrollment.swift // Core // // Created by  Stepanok Ivan on 16.04.2024. diff --git a/Core/Core/Domain/Model/CourseItem.swift b/Core/Core/Domain/Model/CourseItem.swift index dc386058e..67647e038 100644 --- a/Core/Core/Domain/Model/CourseItem.swift +++ b/Core/Core/Domain/Model/CourseItem.swift @@ -7,19 +7,6 @@ import Foundation -public enum CourseTab: Int, CaseIterable, Identifiable { - public var id: Int { - rawValue - } - - case course - case videos - case discussion - case dates - case handounds - -} - public struct CourseItem: Hashable { public let name: String public let org: String diff --git a/Core/Core/Domain/Model/PrimaryEnrollment.swift b/Core/Core/Domain/Model/PrimaryEnrollment.swift index d894fd9ab..3f213aae5 100644 --- a/Core/Core/Domain/Model/PrimaryEnrollment.swift +++ b/Core/Core/Domain/Model/PrimaryEnrollment.swift @@ -31,8 +31,8 @@ public struct PrimaryCourse: Hashable { public let courseBanner: String public let futureAssignments: [Assignment] public let pastAssignments: [Assignment] - public let progressEarned: Int? - public let progressPossible: Int? + public let progressEarned: Int + public let progressPossible: Int public let lastVisitedBlockID: String? public let resumeTitle: String? @@ -46,8 +46,8 @@ public struct PrimaryCourse: Hashable { courseBanner: String, futureAssignments: [Assignment], pastAssignments: [Assignment], - progressEarned: Int?, - progressPossible: Int?, + progressEarned: Int, + progressPossible: Int, lastVisitedBlockID: String?, resumeTitle: String? ) { diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 94cac793b..dd31bb45e 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -10,6 +10,17 @@ import SwiftUI import Core import Combine +public enum CourseTab: Int, CaseIterable, Identifiable { + public var id: Int { + rawValue + } + case course + case videos + case discussion + case dates + case handounds +} + extension CourseTab { public var title: String { switch self { diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index 251f19f10..df8cea243 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -32,34 +32,34 @@ public class DashboardRepository: DashboardRepositoryProtocol { public func getEnrollments(page: Int) async throws -> [CourseItem] { let result = try await api.requestData( - DashboardEndpoint.getMyCourses(username: storage.user?.username ?? "", page: page) + DashboardEndpoint.getEnrollments(username: storage.user?.username ?? "", page: page) ) .mapResponse(DataLayer.CourseEnrollments.self) .domain(baseURL: config.baseURL.absoluteString) - persistence.saveMyCourses(items: result) + persistence.saveEnrollments(items: result) return result } public func getEnrollmentsOffline() throws -> [CourseItem] { - return try persistence.loadMyCourses() + return try persistence.loadEnrollments() } public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { let result = try await api.requestData( - DashboardEndpoint.getMyLearnCourses( + DashboardEndpoint.getPrimaryEnrollment( username: storage.user?.username ?? "", pageSize: pageSize ) ) .mapResponse(DataLayer.PrimaryEnrollment.self) .domain(baseURL: config.baseURL.absoluteString) - persistence.saveMyEnrollments(enrollments: result) + persistence.savePrimaryEnrollment(enrollments: result) return result } public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { - return try persistence.loadMyEnrollments() + return try persistence.loadPrimaryEnrollment() } public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { diff --git a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift index 8848354d3..cda65b250 100644 --- a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift +++ b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift @@ -10,15 +10,15 @@ import Core import Alamofire enum DashboardEndpoint: EndPointType { - case getMyCourses(username: String, page: Int) - case getMyLearnCourses(username: String, pageSize: Int) + case getEnrollments(username: String, page: Int) + case getPrimaryEnrollment(username: String, pageSize: Int) case getAllCourses(username: String, filteredBy: String, page: Int) var path: String { switch self { - case let .getMyCourses(username, _): + case let .getEnrollments(username, _): return "/api/mobile/v3/users/\(username)/course_enrollments" - case let .getMyLearnCourses(username, _): + case let .getPrimaryEnrollment(username, _): return "/api/mobile/v4/users/\(username)/course_enrollments" case let .getAllCourses(username, _, _): return "/api/mobile/v4/users/\(username)/course_enrollments" @@ -27,7 +27,7 @@ enum DashboardEndpoint: EndPointType { var httpMethod: HTTPMethod { switch self { - case .getMyCourses, .getMyLearnCourses, .getAllCourses: + case .getEnrollments, .getPrimaryEnrollment, .getAllCourses: return .get } } @@ -38,13 +38,13 @@ enum DashboardEndpoint: EndPointType { var task: HTTPTask { switch self { - case let .getMyCourses(_, page): + case let .getEnrollments(_, page): let params: Parameters = [ "page": page ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) - case let .getMyLearnCourses(_, pageSize): + case let .getPrimaryEnrollment(_, pageSize): let params: Parameters = [ "page_size": pageSize ] diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift index 7ea61cccb..3747d2c8e 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift @@ -9,10 +9,10 @@ import CoreData import Core public protocol DashboardPersistenceProtocol { - func loadMyCourses() throws -> [CourseItem] - func saveMyCourses(items: [CourseItem]) - func loadMyEnrollments() throws -> PrimaryEnrollment - func saveMyEnrollments(enrollments: PrimaryEnrollment) + func loadEnrollments() throws -> [CourseItem] + func saveEnrollments(items: [CourseItem]) + func loadPrimaryEnrollment() throws -> PrimaryEnrollment + func savePrimaryEnrollment(enrollments: PrimaryEnrollment) } public final class DashboardBundle { diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index a11e240ac..f2e477c1c 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -11,14 +11,14 @@ import Theme public struct AllCoursesView: View { - @StateObject + @ObservedObject private var viewModel: AllCoursesViewModel private let router: DashboardRouter @Environment (\.isHorizontal) private var isHorizontal private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init(viewModel: AllCoursesViewModel, router: DashboardRouter) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.router = router } @@ -73,7 +73,7 @@ public struct AllCoursesView: View { enrollmentStart: course.enrollmentStart, enrollmentEnd: course.enrollmentEnd, title: course.name, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) }, label: { @@ -114,10 +114,12 @@ public struct AllCoursesView: View { .padding(.top, 8) // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.getCourses(page: 1, refresh: true) - }) + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getCourses(page: 1, refresh: true) + } + ) // MARK: - Error Alert if viewModel.showError { @@ -175,13 +177,15 @@ public struct AllCoursesView: View { .foregroundColor(Theme.Colors.textPrimary) .accessibilityIdentifier("all_courses_header_text") Spacer() - Button(action: { - router.showDiscoverySearch(searchQuery: "") - }, label: { - Image(systemName: "magnifyingglass") - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier(DashboardLocalization.search) - }) + Button( + action: { + router.showDiscoverySearch(searchQuery: "") + }, label: { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier(DashboardLocalization.search) + } + ) } .padding(.horizontal, 20) .padding(.vertical, 20) diff --git a/Dashboard/Dashboard/Presentation/DashboardRouter.swift b/Dashboard/Dashboard/Presentation/DashboardRouter.swift index 0d4397a19..dabddd0a9 100644 --- a/Dashboard/Dashboard/Presentation/DashboardRouter.swift +++ b/Dashboard/Dashboard/Presentation/DashboardRouter.swift @@ -17,7 +17,7 @@ public protocol DashboardRouter: BaseRouter { enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String?) func showAllCourses(courses: [CourseItem]) @@ -39,7 +39,7 @@ public class DashboardRouterMock: BaseRouterMock, DashboardRouter { enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String?) {} public func showAllCourses(courses: [CourseItem]) {} diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index 6253201de..0398b7495 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -29,48 +29,50 @@ struct DropDownMenu: View { var body: some View { VStack(alignment: .leading, spacing: 2) { - HStack { - Text(selectedOption.text) - .font(Theme.Fonts.titleSmall) - .accessibilityIdentifier("dropdown_menu_text") - Image(systemName: expanded ? "chevron.up" : "chevron.down") - } - .foregroundColor(Theme.Colors.textPrimary) - .onTapGesture { - withAnimation(.snappy(duration: 0.2)) { - expanded.toggle() - } + HStack { + Text(selectedOption.text) + .font(Theme.Fonts.titleSmall) + .accessibilityIdentifier("dropdown_menu_text") + Image(systemName: expanded ? "chevron.up" : "chevron.down") + } + .foregroundColor(Theme.Colors.textPrimary) + .onTapGesture { + withAnimation(.snappy(duration: 0.2)) { + expanded.toggle() } + } if expanded { VStack(spacing: 0) { ForEach(Array(MenuOption.allCases.enumerated()), id: \.offset) { index, option in - Button(action: { - selectedOption = option - expanded = false - }, label: { - HStack { - Text(option.text) - .font(Theme.Fonts.titleSmall) - .foregroundColor( - option == selectedOption ? Theme.Colors.white : Theme.Colors.textPrimary - ) - Spacer() - } - .padding(10) - .background { - ZStack { - RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, - br: index == MenuOption.allCases.count-1 ? 8 : 0) - .foregroundStyle(option == selectedOption - ? Theme.Colors.accentColor - : Theme.Colors.background) - RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, - br: index == MenuOption.allCases.count-1 ? 8 : 0) - .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + Button( + action: { + selectedOption = option + expanded = false + }, label: { + HStack { + Text(option.text) + .font(Theme.Fonts.titleSmall) + .foregroundColor( + option == selectedOption ? Theme.Colors.white : Theme.Colors.textPrimary + ) + Spacer() + } + .padding(10) + .background { + ZStack { + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .foregroundStyle(option == selectedOption + ? Theme.Colors.accentColor + : Theme.Colors.background) + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + } } } - }) + ) } } .frame(minWidth: 182) diff --git a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift index 132cd17fa..aa5847e16 100644 --- a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift @@ -34,11 +34,7 @@ struct NoCoursesView: View { switch self { case .primary: DashboardLocalization.Learn.NoCoursesView.noCoursesDescription - case .inProgress: - nil - case .completed: - nil - case .expired: + case .inProgress, .completed, .expired: nil } } @@ -85,9 +81,8 @@ struct NoCoursesView: View { } Spacer() if type == .primary { - StyledButton(DashboardLocalization.Learn.NoCoursesView.findACourse, action: { - openDiscovery() - }).padding(24) + StyledButton(DashboardLocalization.Learn.NoCoursesView.findACourse, action: { openDiscovery() }) + .padding(24) } } } diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 3815a275e..1d7f33939 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -17,8 +17,8 @@ public struct PrimaryCardView: View { private let courseImage: String private let courseStartDate: Date? private let courseEndDate: Date? - private var futureAssignments: [Assignment]? - private let pastAssignments: [Assignment]? + private var futureAssignments: [Assignment] + private let pastAssignments: [Assignment] private let progressEarned: Int private let progressPossible: Int private let canResume: Bool @@ -33,8 +33,8 @@ public struct PrimaryCardView: View { courseImage: String, courseStartDate: Date?, courseEndDate: Date?, - futureAssignments: [Assignment]?, - pastAssignments: [Assignment]?, + futureAssignments: [Assignment], + pastAssignments: [Assignment], progressEarned: Int, progressPossible: Int, canResume: Bool, @@ -77,28 +77,26 @@ public struct PrimaryCardView: View { private var assignments: some View { VStack(alignment: .leading, spacing: 8) { // pastAssignments - if let pastAssignments = pastAssignments { - if pastAssignments.count == 1, let pastAssignment = pastAssignments.first { - courseButton( - title: pastAssignment.title, - description: DashboardLocalization.Learn.PrimaryCard.onePastAssignment, - icon: CoreAssets.warning.swiftUIImage, - selected: false, - action: { pastAssignmentAction(pastAssignments.first?.firstComponentBlockId) } - ) - } else if pastAssignments.count > 1 { - courseButton( - title: DashboardLocalization.Learn.PrimaryCard.viewAssignments, - description: DashboardLocalization.Learn.PrimaryCard.pastAssignments(pastAssignments.count), - icon: CoreAssets.warning.swiftUIImage, - selected: false, - action: { pastAssignmentAction(nil) } - ) - } + if pastAssignments.count == 1, let pastAssignment = pastAssignments.first { + courseButton( + title: pastAssignment.title, + description: DashboardLocalization.Learn.PrimaryCard.onePastAssignment, + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { pastAssignmentAction(pastAssignments.first?.firstComponentBlockId) } + ) + } else if pastAssignments.count > 1 { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.viewAssignments, + description: DashboardLocalization.Learn.PrimaryCard.pastAssignments(pastAssignments.count), + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { pastAssignmentAction(nil) } + ) } // futureAssignment - if let futureAssignments, !futureAssignments.isEmpty { + if !futureAssignments.isEmpty { if futureAssignments.count == 1, let futureAssignment = futureAssignments.first { let daysRemaining = Calendar.current.dateComponents( [.day], @@ -163,7 +161,6 @@ public struct PrimaryCardView: View { selected: Bool, action: @escaping () -> Void ) -> some View { - Button(action: { action() }, label: { @@ -255,11 +252,11 @@ struct PrimaryCardView_Previews: PreviewProvider { PrimaryCardView( courseName: "Course Title", org: "Organization", - courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", courseStartDate: nil, courseEndDate: Date(), - futureAssignments: nil, - pastAssignments: nil, + futureAssignments: [], + pastAssignments: [], progressEarned: 10, progressPossible: 45, canResume: true, diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 4e000b99f..d3787ebad 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -82,7 +82,7 @@ public struct ListDashboardView: View { enrollmentStart: course.enrollmentStart, enrollmentEnd: course.enrollmentEnd, title: course.name, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 5977659b7..b7b98bc6a 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -8,7 +8,6 @@ import SwiftUI import Core import Theme -//import Discovery import Swinject public struct PrimaryCourseDashboardView: View { @@ -19,6 +18,7 @@ public struct PrimaryCourseDashboardView: View { private let config = Container.shared.resolve(ConfigProtocol.self) @ViewBuilder let programView: ProgramView private var openDiscoveryPage: () -> Void + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @State private var selectedMenu: MenuOption = .courses @@ -37,151 +37,133 @@ public struct PrimaryCourseDashboardView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - if !viewModel.fetchInProgress, viewModel.enrollments?.primaryCourse == nil { + if viewModel.enrollments?.primaryCourse == nil && !viewModel.fetchInProgress { NoCoursesView(openDiscovery: { openDiscoveryPage() }).zIndex(1) } - // MARK: - Page body - VStack(alignment: .center) { - RefreshableScrollViewCompat(action: { - await viewModel.getEnrollments(showProgress: false) - }) { - ZStack(alignment: .topLeading) { - learnTitleAndSearch() - .zIndex(1) - if !viewModel.fetchInProgress, viewModel.enrollments?.primaryCourse == nil { - - } else if viewModel.fetchInProgress { - VStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - }.frame(maxWidth: .infinity, - maxHeight: .infinity) - } else { - LazyVStack(spacing: 0) { - Spacer(minLength: 50) - switch selectedMenu { - case .courses: - if let enrollments = viewModel.enrollments { - if let primary = enrollments.primaryCourse { - PrimaryCardView( - courseName: primary.name, - org: primary.org, - courseImage: primary.courseBanner, - courseStartDate: primary.courseStart, - courseEndDate: primary.courseEnd, - futureAssignments: primary.futureAssignments, - pastAssignments: primary.pastAssignments, - progressEarned: primary.progressEarned ?? 0, - progressPossible: primary.progressPossible ?? 0, - canResume: primary.lastVisitedBlockID != nil, - resumeTitle: primary.resumeTitle, - pastAssignmentAction: { lastVisitedBlockID in - router.showCourseScreens( - courseID: primary.courseID, - hasAccess: primary.hasAccess, - courseStart: primary.courseStart, - courseEnd: primary.courseEnd, - enrollmentStart: nil, - enrollmentEnd: nil, - title: primary.name, - selection: lastVisitedBlockID == nil ? .dates : .course, - lastVisitedBlockID: lastVisitedBlockID - ) - }, - futureAssignmentAction: { lastVisitedBlockID in - router.showCourseScreens( - courseID: primary.courseID, - hasAccess: primary.hasAccess, - courseStart: primary.courseStart, - courseEnd: primary.courseEnd, - enrollmentStart: nil, - enrollmentEnd: nil, - title: primary.name, - selection: lastVisitedBlockID == nil ? .dates : .course, - lastVisitedBlockID: lastVisitedBlockID - ) - }, - resumeAction: { - router.showCourseScreens( - courseID: primary.courseID, - hasAccess: primary.hasAccess, - courseStart: primary.courseStart, - courseEnd: primary.courseEnd, - enrollmentStart: nil, - enrollmentEnd: nil, - title: primary.name, - selection: .course, - lastVisitedBlockID: primary.lastVisitedBlockID - ) - } - ) - } - if !enrollments.courses.isEmpty { - viewAll(enrollments) + learnTitleAndSearch() + .frameLimit(width: proxy.size.width) + .zIndex(1) + // MARK: - Page body + VStack(alignment: .leading) { + + RefreshableScrollViewCompat(action: { + await viewModel.getEnrollments(showProgress: false) + }) { + ZStack(alignment: .topLeading) { + if viewModel.fetchInProgress { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) + } else { + LazyVStack(spacing: 0) { + Spacer(minLength: 50) + switch selectedMenu { + case .courses: + if let enrollments = viewModel.enrollments { + if let primary = enrollments.primaryCourse { + PrimaryCardView( + courseName: primary.name, + org: primary.org, + courseImage: primary.courseBanner, + courseStartDate: primary.courseStart, + courseEndDate: primary.courseEnd, + futureAssignments: primary.futureAssignments, + pastAssignments: primary.pastAssignments, + progressEarned: primary.progressEarned, + progressPossible: primary.progressPossible, + canResume: primary.lastVisitedBlockID != nil, + resumeTitle: primary.resumeTitle, + pastAssignmentAction: { lastVisitedBlockID in + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + showDates: lastVisitedBlockID == nil, + lastVisitedBlockID: lastVisitedBlockID + ) + }, + futureAssignmentAction: { lastVisitedBlockID in + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + showDates: lastVisitedBlockID == nil, + lastVisitedBlockID: lastVisitedBlockID + ) + }, + resumeAction: { + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + showDates: false, + lastVisitedBlockID: primary.lastVisitedBlockID + ) + } + ) + } + if !enrollments.courses.isEmpty { + viewAll(enrollments) + } + if idiom == .pad { + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16) + ], + alignment: .leading, + spacing: 15 + ) { + courses(enrollments) } + .padding(20) + } else { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { - ForEach( - Array(enrollments.courses.enumerated()), - id: \.offset - ) { _, course in - Button(action: { - viewModel.trackDashboardCourseClicked( - courseID: course.courseID, - courseName: course.name - ) - router.showCourseScreens( - courseID: course.courseID, - hasAccess: course.hasAccess, - courseStart: course.courseStart, - courseEnd: course.courseEnd, - enrollmentStart: course.enrollmentStart, - enrollmentEnd: course.enrollmentEnd, - title: course.name, - selection: .course, - lastVisitedBlockID: nil - ) - }, label: { - CourseCardView( - courseName: course.name, - courseImage: course.imageURL, - progressEarned: 0, - progressPossible: 0, - courseStartDate: nil, - courseEndDate: nil, - hasAccess: course.hasAccess, - isFullCard: false - ).frame(width: 120) - } - ) - .accessibilityIdentifier("course_item") - } - if enrollments.courses.count < enrollments.count { - viewAllButton(enrollments) - } + courses(enrollments) } .padding(20) } - } else { - EmptyPageIcon() } - case .programs: - programView + Spacer(minLength: 100) + } else { + EmptyPageIcon() } + case .programs: + programView } } } - .frameLimit(width: proxy.size.width) - }.accessibilityAction {} - }.padding(.top, 8) - + } + .frameLimit(width: proxy.size.width) + }.accessibilityAction {} + }.padding(.top, 8) + // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.getEnrollments(showProgress: false) - }) + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getEnrollments(showProgress: false) + } + ) // MARK: - Error Alert if viewModel.showError { @@ -189,8 +171,10 @@ public struct PrimaryCourseDashboardView: View { Spacer() SnackBarView(message: viewModel.errorMessage) } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) + .padding( + .bottom, + viewModel.connectivity.isInternetAvaliable ? 0 : OfflineSnackBarView.height + ) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -214,25 +198,70 @@ public struct PrimaryCourseDashboardView: View { } } + @ViewBuilder + private func courses(_ enrollments: PrimaryEnrollment) -> some View { + ForEach( + Array(enrollments.courses.enumerated()), + id: \.offset + ) { _, course in + Button(action: { + viewModel.trackDashboardCourseClicked( + courseID: course.courseID, + courseName: course.name + ) + router.showCourseScreens( + courseID: course.courseID, + hasAccess: course.hasAccess, + courseStart: course.courseStart, + courseEnd: course.courseEnd, + enrollmentStart: course.enrollmentStart, + enrollmentEnd: course.enrollmentEnd, + title: course.name, + showDates: false, + lastVisitedBlockID: nil + ) + }, label: { + CourseCardView( + courseName: course.name, + courseImage: course.imageURL, + progressEarned: 0, + progressPossible: 0, + courseStartDate: nil, + courseEndDate: nil, + hasAccess: course.hasAccess, + isFullCard: false + ).frame(width: idiom == .pad ? nil : 120) + } + ) + .accessibilityIdentifier("course_item") + } + if enrollments.courses.count < enrollments.count { + viewAllButton(enrollments) + } + } + private func viewAllButton(_ enrollments: PrimaryEnrollment) -> some View { Button(action: { router.showAllCourses(courses: enrollments.courses) }, label: { ZStack(alignment: .topTrailing) { - VStack(alignment: .leading, spacing: 0) { + HStack { Spacer() - CoreAssets.viewAll.swiftUIImage - Text(DashboardLocalization.Learn.viewAll) - .font(Theme.Fonts.labelMedium) - .foregroundStyle(Theme.Colors.textPrimary) + VStack(alignment: .leading, spacing: 0) { + Spacer() + CoreAssets.viewAll.swiftUIImage + Text(DashboardLocalization.Learn.viewAll) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + Spacer() + } Spacer() } - .frame(width: 120) + .frame(width: idiom == .pad ? nil : 120) } .background(Theme.Colors.background) .cornerRadius(8) .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) - }) } @@ -253,30 +282,33 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch() -> some View { - VStack(alignment: .leading) { - HStack(alignment: .center) { - Text(DashboardLocalization.Learn.title) - .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier("courses_header_text") - Spacer() - Button(action: { - router.showDiscoverySearch(searchQuery: "") - }, label: { - Image(systemName: "magnifyingglass") + ZStack(alignment: .top) { + Theme.Colors.background + .frame(height: 70) + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.title) + .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier(DashboardLocalization.search) - }) - } - if let config, config.program.enabled, config.program.isWebViewConfigured { - DropDownMenu(selectedOption: $selectedMenu) + .accessibilityIdentifier("courses_header_text") + Spacer() + Button(action: { + router.showDiscoverySearch(searchQuery: "") + }, label: { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier(DashboardLocalization.search) + }) + } + if let config, config.program.enabled, config.program.isWebViewConfigured { + DropDownMenu(selectedOption: $selectedMenu) + } } + .listRowBackground(Color.clear) + .padding(.horizontal, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) } - .listRowBackground(Color.clear) - .padding(.horizontal, 20) - .padding(.bottom, 20) - .accessibilityElement(children: .ignore) - .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) } } @@ -289,19 +321,23 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { analytics: DashboardAnalyticsMock() ) - PrimaryCourseDashboardView(viewModel: vm, - router: DashboardRouterMock(), - programView: EmptyView(), - openDiscoveryPage: { - }) + PrimaryCourseDashboardView( + viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + } + ) .preferredColorScheme(.light) .previewDisplayName("DashboardView Light") - PrimaryCourseDashboardView(viewModel: vm, - router: DashboardRouterMock(), - programView: EmptyView(), - openDiscoveryPage: { - }) + PrimaryCourseDashboardView( + viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + } + ) .preferredColorScheme(.dark) .previewDisplayName("DashboardView Dark") } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 599630511..847ed41c6 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -12,10 +12,9 @@ import Combine public class PrimaryCourseDashboardViewModel: ObservableObject { - public var nextPage = 1 - public var totalPages = 1 - @Published public private(set) var fetchInProgress = false - + var nextPage = 1 + var totalPages = 1 + @Published public private(set) var fetchInProgress = true @Published var enrollments: PrimaryEnrollment? @Published var showError: Bool = false var errorMessage: String? { @@ -31,9 +30,14 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { private let analytics: DashboardAnalytics private var onCourseEnrolledCancellable: AnyCancellable? - public init(interactor: DashboardInteractorProtocol, - connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics) { + private let ipadPageSize = 7 + private let iphonePageSize = 5 + + public init( + interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics + ) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics @@ -50,7 +54,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { @MainActor public func getEnrollments(showProgress: Bool = true) async { - let pageSize = UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5 + let pageSize = UIDevice.current.userInterfaceIdiom == .pad ? ipadPageSize : iphonePageSize fetchInProgress = showProgress do { if connectivity.isInternetAvaliable { diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 35bb2fba5..27620fef4 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1609,64 +1609,64 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { - open func getMyCourses(page: Int) throws -> [CourseItem] { - addInvocation(.m_getMyCourses__page_page(Parameter.value(`page`))) - let perform = methodPerformValue(.m_getMyCourses__page_page(Parameter.value(`page`))) as? (Int) -> Void + open func getEnrollments(page: Int) throws -> [CourseItem] { + addInvocation(.m_getEnrollments__page_page(Parameter.value(`page`))) + let perform = methodPerformValue(.m_getEnrollments__page_page(Parameter.value(`page`))) as? (Int) -> Void perform?(`page`) var __value: [CourseItem] do { - __value = try methodReturnValue(.m_getMyCourses__page_page(Parameter.value(`page`))).casted() + __value = try methodReturnValue(.m_getEnrollments__page_page(Parameter.value(`page`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyCourses(page: Int). Use given") - Failure("Stub return value not specified for getMyCourses(page: Int). Use given") + onFatalFailure("Stub return value not specified for getEnrollments(page: Int). Use given") + Failure("Stub return value not specified for getEnrollments(page: Int). Use given") } catch { throw error } return __value } - open func discoveryOffline() throws -> [CourseItem] { - addInvocation(.m_discoveryOffline) - let perform = methodPerformValue(.m_discoveryOffline) as? () -> Void + open func getEnrollmentsOffline() throws -> [CourseItem] { + addInvocation(.m_getEnrollmentsOffline) + let perform = methodPerformValue(.m_getEnrollmentsOffline) as? () -> Void perform?() var __value: [CourseItem] do { - __value = try methodReturnValue(.m_discoveryOffline).casted() + __value = try methodReturnValue(.m_getEnrollmentsOffline).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for discoveryOffline(). Use given") - Failure("Stub return value not specified for discoveryOffline(). Use given") + onFatalFailure("Stub return value not specified for getEnrollmentsOffline(). Use given") + Failure("Stub return value not specified for getEnrollmentsOffline(). Use given") } catch { throw error } return __value } - open func getMyLearnCourses(pageSize: Int) throws -> PrimaryEnrollment { - addInvocation(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) - let perform = methodPerformValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))) as? (Int) -> Void + open func getPrimaryEnrollment(pageSize: Int) throws -> PrimaryEnrollment { + addInvocation(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))) + let perform = methodPerformValue(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))) as? (Int) -> Void perform?(`pageSize`) var __value: PrimaryEnrollment do { - __value = try methodReturnValue(.m_getMyLearnCourses__pageSize_pageSize(Parameter.value(`pageSize`))).casted() + __value = try methodReturnValue(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyLearnCourses(pageSize: Int). Use given") - Failure("Stub return value not specified for getMyLearnCourses(pageSize: Int). Use given") + onFatalFailure("Stub return value not specified for getPrimaryEnrollment(pageSize: Int). Use given") + Failure("Stub return value not specified for getPrimaryEnrollment(pageSize: Int). Use given") } catch { throw error } return __value } - open func getMyLearnCoursesOffline() throws -> PrimaryEnrollment { - addInvocation(.m_getMyLearnCoursesOffline) - let perform = methodPerformValue(.m_getMyLearnCoursesOffline) as? () -> Void + open func getPrimaryEnrollmentOffline() throws -> PrimaryEnrollment { + addInvocation(.m_getPrimaryEnrollmentOffline) + let perform = methodPerformValue(.m_getPrimaryEnrollmentOffline) as? () -> Void perform?() var __value: PrimaryEnrollment do { - __value = try methodReturnValue(.m_getMyLearnCoursesOffline).casted() + __value = try methodReturnValue(.m_getPrimaryEnrollmentOffline).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyLearnCoursesOffline(). Use given") - Failure("Stub return value not specified for getMyLearnCoursesOffline(). Use given") + onFatalFailure("Stub return value not specified for getPrimaryEnrollmentOffline(). Use given") + Failure("Stub return value not specified for getPrimaryEnrollmentOffline(). Use given") } catch { throw error } @@ -1689,76 +1689,55 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { return __value } - open func getAllCoursesOffline() throws -> PrimaryEnrollment { - addInvocation(.m_getAllCoursesOffline) - let perform = methodPerformValue(.m_getAllCoursesOffline) as? () -> Void - perform?() - var __value: PrimaryEnrollment - do { - __value = try methodReturnValue(.m_getAllCoursesOffline).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getAllCoursesOffline(). Use given") - Failure("Stub return value not specified for getAllCoursesOffline(). Use given") - } catch { - throw error - } - return __value - } - fileprivate enum MethodType { - case m_getMyCourses__page_page(Parameter) - case m_discoveryOffline - case m_getMyLearnCourses__pageSize_pageSize(Parameter) - case m_getMyLearnCoursesOffline + case m_getEnrollments__page_page(Parameter) + case m_getEnrollmentsOffline + case m_getPrimaryEnrollment__pageSize_pageSize(Parameter) + case m_getPrimaryEnrollmentOffline case m_getAllCourses__filteredBy_filteredBypage_page(Parameter, Parameter) - case m_getAllCoursesOffline static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_getMyCourses__page_page(let lhsPage), .m_getMyCourses__page_page(let rhsPage)): + case (.m_getEnrollments__page_page(let lhsPage), .m_getEnrollments__page_page(let rhsPage)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) return Matcher.ComparisonResult(results) - case (.m_discoveryOffline, .m_discoveryOffline): return .match + case (.m_getEnrollmentsOffline, .m_getEnrollmentsOffline): return .match - case (.m_getMyLearnCourses__pageSize_pageSize(let lhsPagesize), .m_getMyLearnCourses__pageSize_pageSize(let rhsPagesize)): + case (.m_getPrimaryEnrollment__pageSize_pageSize(let lhsPagesize), .m_getPrimaryEnrollment__pageSize_pageSize(let rhsPagesize)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPagesize, rhs: rhsPagesize, with: matcher), lhsPagesize, rhsPagesize, "pageSize")) return Matcher.ComparisonResult(results) - case (.m_getMyLearnCoursesOffline, .m_getMyLearnCoursesOffline): return .match + case (.m_getPrimaryEnrollmentOffline, .m_getPrimaryEnrollmentOffline): return .match case (.m_getAllCourses__filteredBy_filteredBypage_page(let lhsFilteredby, let lhsPage), .m_getAllCourses__filteredBy_filteredBypage_page(let rhsFilteredby, let rhsPage)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFilteredby, rhs: rhsFilteredby, with: matcher), lhsFilteredby, rhsFilteredby, "filteredBy")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) return Matcher.ComparisonResult(results) - - case (.m_getAllCoursesOffline, .m_getAllCoursesOffline): return .match default: return .none } } func intValue() -> Int { switch self { - case let .m_getMyCourses__page_page(p0): return p0.intValue - case .m_discoveryOffline: return 0 - case let .m_getMyLearnCourses__pageSize_pageSize(p0): return p0.intValue - case .m_getMyLearnCoursesOffline: return 0 + case let .m_getEnrollments__page_page(p0): return p0.intValue + case .m_getEnrollmentsOffline: return 0 + case let .m_getPrimaryEnrollment__pageSize_pageSize(p0): return p0.intValue + case .m_getPrimaryEnrollmentOffline: return 0 case let .m_getAllCourses__filteredBy_filteredBypage_page(p0, p1): return p0.intValue + p1.intValue - case .m_getAllCoursesOffline: return 0 } } func assertionName() -> String { switch self { - case .m_getMyCourses__page_page: return ".getMyCourses(page:)" - case .m_discoveryOffline: return ".discoveryOffline()" - case .m_getMyLearnCourses__pageSize_pageSize: return ".getMyLearnCourses(pageSize:)" - case .m_getMyLearnCoursesOffline: return ".getMyLearnCoursesOffline()" + case .m_getEnrollments__page_page: return ".getEnrollments(page:)" + case .m_getEnrollmentsOffline: return ".getEnrollmentsOffline()" + case .m_getPrimaryEnrollment__pageSize_pageSize: return ".getPrimaryEnrollment(pageSize:)" + case .m_getPrimaryEnrollmentOffline: return ".getPrimaryEnrollmentOffline()" case .m_getAllCourses__filteredBy_filteredBypage_page: return ".getAllCourses(filteredBy:page:)" - case .m_getAllCoursesOffline: return ".getAllCoursesOffline()" } } } @@ -1772,60 +1751,57 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { } - public static func getMyCourses(page: Parameter, willReturn: [CourseItem]...) -> MethodStub { - return Given(method: .m_getMyCourses__page_page(`page`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getEnrollments(page: Parameter, willReturn: [CourseItem]...) -> MethodStub { + return Given(method: .m_getEnrollments__page_page(`page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func discoveryOffline(willReturn: [CourseItem]...) -> MethodStub { - return Given(method: .m_discoveryOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getEnrollmentsOffline(willReturn: [CourseItem]...) -> MethodStub { + return Given(method: .m_getEnrollmentsOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyLearnCourses(pageSize: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { - return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getPrimaryEnrollment(pageSize: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyLearnCoursesOffline(willReturn: PrimaryEnrollment...) -> MethodStub { - return Given(method: .m_getMyLearnCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getPrimaryEnrollmentOffline(willReturn: PrimaryEnrollment...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollmentOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getAllCourses(filteredBy: Parameter, page: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getAllCoursesOffline(willReturn: PrimaryEnrollment...) -> MethodStub { - return Given(method: .m_getAllCoursesOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getMyCourses(page: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyCourses__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getEnrollments(page: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getEnrollments__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyCourses(page: Parameter, willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { + public static func getEnrollments(page: Parameter, willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyCourses__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getEnrollments__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: ([CourseItem]).self) willProduce(stubber) return given } - public static func discoveryOffline(willThrow: Error...) -> MethodStub { - return Given(method: .m_discoveryOffline, products: willThrow.map({ StubProduct.throw($0) })) + public static func getEnrollmentsOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getEnrollmentsOffline, products: willThrow.map({ StubProduct.throw($0) })) } - public static func discoveryOffline(willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { + public static func getEnrollmentsOffline(willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_discoveryOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getEnrollmentsOffline, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: ([CourseItem]).self) willProduce(stubber) return given } - public static func getMyLearnCourses(pageSize: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getPrimaryEnrollment(pageSize: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyLearnCourses(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getPrimaryEnrollment(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given } - public static func getMyLearnCoursesOffline(willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) + public static func getPrimaryEnrollmentOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollmentOffline, products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyLearnCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getPrimaryEnrollmentOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyLearnCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getPrimaryEnrollmentOffline, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (PrimaryEnrollment).self) willProduce(stubber) return given @@ -1840,51 +1816,37 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { willProduce(stubber) return given } - public static func getAllCoursesOffline(willThrow: Error...) -> MethodStub { - return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) - } - public static func getAllCoursesOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getAllCoursesOffline, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (PrimaryEnrollment).self) - willProduce(stubber) - return given - } } public struct Verify { fileprivate var method: MethodType - public static func getMyCourses(page: Parameter) -> Verify { return Verify(method: .m_getMyCourses__page_page(`page`))} - public static func discoveryOffline() -> Verify { return Verify(method: .m_discoveryOffline)} - public static func getMyLearnCourses(pageSize: Parameter) -> Verify { return Verify(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`))} - public static func getMyLearnCoursesOffline() -> Verify { return Verify(method: .m_getMyLearnCoursesOffline)} + public static func getEnrollments(page: Parameter) -> Verify { return Verify(method: .m_getEnrollments__page_page(`page`))} + public static func getEnrollmentsOffline() -> Verify { return Verify(method: .m_getEnrollmentsOffline)} + public static func getPrimaryEnrollment(pageSize: Parameter) -> Verify { return Verify(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`))} + public static func getPrimaryEnrollmentOffline() -> Verify { return Verify(method: .m_getPrimaryEnrollmentOffline)} public static func getAllCourses(filteredBy: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`))} - public static func getAllCoursesOffline() -> Verify { return Verify(method: .m_getAllCoursesOffline)} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func getMyCourses(page: Parameter, perform: @escaping (Int) -> Void) -> Perform { - return Perform(method: .m_getMyCourses__page_page(`page`), performs: perform) + public static func getEnrollments(page: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_getEnrollments__page_page(`page`), performs: perform) } - public static func discoveryOffline(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_discoveryOffline, performs: perform) + public static func getEnrollmentsOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getEnrollmentsOffline, performs: perform) } - public static func getMyLearnCourses(pageSize: Parameter, perform: @escaping (Int) -> Void) -> Perform { - return Perform(method: .m_getMyLearnCourses__pageSize_pageSize(`pageSize`), performs: perform) + public static func getPrimaryEnrollment(pageSize: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), performs: perform) } - public static func getMyLearnCoursesOffline(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getMyLearnCoursesOffline, performs: perform) + public static func getPrimaryEnrollmentOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getPrimaryEnrollmentOffline, performs: perform) } public static func getAllCourses(filteredBy: Parameter, page: Parameter, perform: @escaping (String, Int) -> Void) -> Perform { return Perform(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), performs: perform) } - public static func getAllCoursesOffline(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getAllCoursesOffline, performs: perform) - } } public func given(_ method: Given) { diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index 760924b1b..1d3ab3db9 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -25,7 +25,7 @@ final class ListDashboardViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -39,7 +39,7 @@ final class ListDashboardViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -52,11 +52,11 @@ final class ListDashboardViewModelTests: XCTestCase { ] Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willReturn: items)) + Given(interactor, .getEnrollments(page: .any, willReturn: items)) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses == items) XCTAssertNil(viewModel.errorMessage) @@ -74,7 +74,7 @@ final class ListDashboardViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -88,7 +88,7 @@ final class ListDashboardViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -101,11 +101,11 @@ final class ListDashboardViewModelTests: XCTestCase { ] Given(connectivity, .isInternetAvaliable(getter: false)) - Given(interactor, .discoveryOffline(willReturn: items)) + Given(interactor, .getEnrollmentsOffline(willReturn: items)) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .discoveryOffline()) + Verify(interactor, 1, .getEnrollmentsOffline()) XCTAssertTrue(viewModel.courses == items) XCTAssertNil(viewModel.errorMessage) @@ -119,11 +119,11 @@ final class ListDashboardViewModelTests: XCTestCase { let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willThrow: NoCachedDataError()) ) + Given(interactor, .getEnrollments(page: .any, willThrow: NoCachedDataError()) ) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses.isEmpty) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.noCachedData) @@ -137,11 +137,11 @@ final class ListDashboardViewModelTests: XCTestCase { let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willThrow: NSError()) ) + Given(interactor, .getEnrollments(page: .any, willThrow: NSError()) ) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses.isEmpty) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index 9a8134ca7..4416d9659 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -26,7 +26,7 @@ public protocol DiscoveryRouter: BaseRouter { enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String? ) @@ -59,7 +59,7 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String? ) {} diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index e73a19343..80864b8fd 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -281,7 +281,7 @@ private struct CourseStateView: View { enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, title: title, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) } diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index f431e7363..836323072 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -237,7 +237,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, title: courseDetails.courseTitle, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index dc4387e6e..ad0c89987 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -225,7 +225,7 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, title: courseDetails.courseTitle, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index df7c19c9a..241178b03 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -38,7 +38,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -52,7 +52,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -91,7 +91,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -105,7 +105,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -143,7 +143,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -157,7 +157,7 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index a976b12ef..e1596add9 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -40,7 +40,7 @@ final class SearchViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), @@ -54,7 +54,7 @@ final class SearchViewModelTests: XCTestCase { org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 2c94092fc..8341e2233 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -44,8 +44,8 @@ 07A7D79028F5C9060000BE81 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 07A7D78E28F5C9060000BE81 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; - 0AF04487E2B198E7A14F037F /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */; }; 149FF39E2B9F1AB50034B33F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF39C2B9F1AB50034B33F /* LaunchScreen.storyboard */; }; + 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */; }; A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668A2B613ED10024680B /* PushNotificationsManager.swift */; }; A500668D2B6143000024680B /* FCMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668C2B6143000024680B /* FCMProvider.swift */; }; A50066912B61467B0024680B /* BrazeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066902B61467B0024680B /* BrazeProvider.swift */; }; @@ -128,9 +128,9 @@ 07D5DA3128D075AA00752FD9 /* OpenEdX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenEdX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; 149FF39D2B9F1AB50034B33F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; + 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; A500668A2B613ED10024680B /* PushNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = ""; }; A500668C2B6143000024680B /* FCMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMProvider.swift; sourceTree = ""; }; A50066902B61467B0024680B /* BrazeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeProvider.swift; sourceTree = ""; }; @@ -142,13 +142,13 @@ A59568982B616D9400ED4F90 /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; A59702282B83C87900CA064C /* FirebaseAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAnalyticsService.swift; sourceTree = ""; }; A5C10D8E2B861A70008E864D /* SegmentAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentAnalyticsService.swift; sourceTree = ""; }; + A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; + AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; BA7468752B96201D00793145 /* DeepLinkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouter.swift; sourceTree = ""; }; - BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; - C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; - D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; - E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; E0D6E6A22B1626B10089F9C9 /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -170,7 +170,7 @@ A5BD3E302B83B0F7006A8983 /* SegmentFirebase in Frameworks */, A51CDBED2B6D2BEE009B6D4E /* SegmentBraze in Frameworks */, A51CDBEF2B6D2BEE009B6D4E /* SegmentBrazeUI in Frameworks */, - 0AF04487E2B198E7A14F037F /* Pods_App_OpenEdX.framework in Frameworks */, + 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -263,7 +263,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */, + FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -271,12 +271,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */, - E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */, - BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */, - 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */, - 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */, - D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */, + A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */, + 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */, + DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */, + FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */, + AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */, + 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -390,7 +390,7 @@ isa = PBXNativeTarget; buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */; buildPhases = ( - CC61D82AF6FA18BD30E5E8E5 /* [CP] Check Pods Manifest.lock */, + B9442FD26CE9A85A43FC2CFA /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -512,7 +512,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - CC61D82AF6FA18BD30E5E8E5 /* [CP] Check Pods Manifest.lock */ = { + B9442FD26CE9A85A43FC2CFA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -695,7 +695,7 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */; + baseConfigurationReference = 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -783,7 +783,7 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */; + baseConfigurationReference = AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -877,7 +877,7 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */; + baseConfigurationReference = DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -965,7 +965,7 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */; + baseConfigurationReference = 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1113,7 +1113,7 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */; + baseConfigurationReference = A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1147,7 +1147,7 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */; + baseConfigurationReference = FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index 0279b8174..9591d9a55 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -18,7 +18,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { self.context = context } - public func loadMyCourses() throws -> [CourseItem] { + public func loadEnrollments() throws -> [CourseItem] { let result = try? context.fetch(CDDashboardCourse.fetchRequest()) .map { CourseItem(name: $0.name ?? "", org: $0.org ?? "", @@ -41,7 +41,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } - public func saveMyCourses(items: [CourseItem]) { + public func saveEnrollments(items: [CourseItem]) { for item in items { context.performAndWait { let newItem = CDDashboardCourse(context: self.context) @@ -67,7 +67,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } - public func loadMyEnrollments() throws -> PrimaryEnrollment { + public func loadPrimaryEnrollment() throws -> PrimaryEnrollment { let request = CDMyEnrollments.fetchRequest() if let result = try context.fetch(request).first { let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in @@ -145,7 +145,7 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } // swiftlint:disable function_body_length - public func saveMyEnrollments(enrollments: PrimaryEnrollment) { + public func savePrimaryEnrollment(enrollments: PrimaryEnrollment) { context.performAndWait { let request: NSFetchRequest = CDMyEnrollments.fetchRequest() diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index ae237a0da..0ae41e30a 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -113,7 +113,7 @@ extension Router: DeepLinkRouter { enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, title: courseDetails.courseTitle, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) } else { diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 613d5eaa8..0719b1e9b 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -191,7 +191,7 @@ public class PipManager: PipManagerProtocol { enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, title: courseDetails.courseTitle, - selection: .course, + showDates: false, lastVisitedBlockID: nil ) controller.rootView.viewModel.selection = holder.selectedCourseTab diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 62f04b60b..4197d9967 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -365,7 +365,7 @@ public class Router: AuthorizationRouter, enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String? ) { let controller = getCourseScreensController( @@ -376,7 +376,7 @@ public class Router: AuthorizationRouter, enrollmentStart: enrollmentStart, enrollmentEnd: enrollmentEnd, title: title, - selection: selection, + showDates: showDates, lastVisitedBlockID: lastVisitedBlockID ) navigationController.pushViewController(controller, animated: true) @@ -390,7 +390,7 @@ public class Router: AuthorizationRouter, enrollmentStart: Date?, enrollmentEnd: Date?, title: String, - selection: CourseTab, + showDates: Bool, lastVisitedBlockID: String? ) -> UIHostingController { let vm = Container.shared.resolve( @@ -400,7 +400,7 @@ public class Router: AuthorizationRouter, courseEnd, enrollmentStart, enrollmentEnd, - selection, + showDates ? CourseTab.dates : CourseTab.course, lastVisitedBlockID )! From caef59908fcb88cefac4712b8080c14c52e03a01 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 10 May 2024 16:47:54 +0300 Subject: [PATCH 05/22] fix: address feedback --- Core/Core/Data/Model/Data_Enrollments.swift | 10 +- .../Data/Model/Data_PrimaryEnrollment.swift | 166 ++++++++---------- Core/Core/SwiftGen/Strings.swift | 2 + Core/Core/en.lproj/Localizable.strings | 1 + .../Presentation/AllCoursesView.swift | 25 +-- .../Presentation/AllCoursesViewModel.swift | 7 +- .../Presentation/DashboardRouter.swift | 3 + .../Elements/CourseCardView.swift | 12 +- .../Elements/ProgressLineView.swift | 4 +- .../PrimaryCourseDashboardView.swift | 56 +++--- Dashboard/Dashboard/SwiftGen/Strings.swift | 4 +- .../Dashboard/en.lproj/Localizable.strings | 2 +- .../Dashboard/uk.lproj/Localizable.strings | 2 +- .../NativeDiscovery/SearchView.swift | 2 +- 14 files changed, 152 insertions(+), 144 deletions(-) diff --git a/Core/Core/Data/Model/Data_Enrollments.swift b/Core/Core/Data/Model/Data_Enrollments.swift index 3ab330e6b..b634cb665 100644 --- a/Core/Core/Data/Model/Data_Enrollments.swift +++ b/Core/Core/Data/Model/Data_Enrollments.swift @@ -29,7 +29,7 @@ public extension DataLayer { public let numPages: Int? public let currentPage: Int? public let start: Int? - public let results: [Result] + public let results: [Enrollment] enum CodingKeys: String, CodingKey { case next @@ -48,7 +48,7 @@ public extension DataLayer { numPages: Int?, currentPage: Int?, start: Int?, - results: [Result] + results: [Enrollment] ) { self.next = next self.previous = previous @@ -60,8 +60,8 @@ public extension DataLayer { } } - // MARK: - Result - struct Result: Codable { + // MARK: - Enrollment + struct Enrollment: Codable { public let auditAccessExpires: String? public let created: String public let mode: Mode @@ -77,7 +77,7 @@ public extension DataLayer { case isActive = "is_active" case course case courseModes = "course_modes" - case progress + case progress = "course_progress" } public init( diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index 59bfd3485..c21e1b5f5 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -26,22 +26,6 @@ public extension DataLayer { } } - // MARK: - CourseImage - struct CourseImage: Codable { - public let uri: String - public let name: String - - enum CodingKeys: String, CodingKey { - case uri = "uri" - case name = "name" - } - - public init(uri: String, name: String) { - self.uri = uri - self.name = name - } - } - // MARK: - Primary struct Primary: Codable { public let auditAccessExpires: Date? @@ -197,85 +181,89 @@ public extension DataLayer { } public extension DataLayer.PrimaryEnrollment { + func domain(baseURL: String) -> PrimaryEnrollment { - var primaryCourse: PrimaryCourse? - if let primary = self.primary { - let futureAssignments: [DataLayer.Assignment] = primary.courseAssignments?.futureAssignments ?? [] - let pastAssignments: [DataLayer.Assignment] = primary.courseAssignments?.pastAssignments ?? [] - - primaryCourse = PrimaryCourse( - name: primary.course?.name ?? "", - org: primary.course?.org ?? "", - courseID: primary.course?.id ?? "", - hasAccess: primary.course?.coursewareAccess.hasAccess ?? true, - courseStart: primary.course?.start != nil - ? Date(iso8601: primary.course?.start ?? "") - : nil, - courseEnd: primary.course?.end != nil - ? Date(iso8601: primary.course?.end ?? "") - : nil, - courseBanner: baseURL + (primary.course?.media.courseImage?.url ?? ""), - futureAssignments: futureAssignments.map { - Assignment( - type: $0.assignmentType ?? "", - title: $0.title ?? "", - description: $0.description, - date: Date(iso8601: $0.date ?? ""), - complete: $0.complete ?? false, - firstComponentBlockId: $0.firstComponentBlockID - ) - }, - pastAssignments: pastAssignments.map { - Assignment( - type: $0.assignmentType ?? "", - title: $0.title ?? "", - description: $0.description ?? "", - date: Date(iso8601: $0.date ?? ""), - complete: $0.complete ?? false, - firstComponentBlockId: $0.firstComponentBlockID - ) - }, - progressEarned: primary.progress?.assignmentsCompleted ?? 0, - progressPossible: primary.progress?.totalAssignmentsCount ?? 0, - lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, - resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName - ) - } - let courses = self.enrollments?.results.map { - let imageUrl = $0.course.media.courseImage?.url ?? "" - let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" - let fullImageURL = baseURL + encodedUrl - return CourseItem( - name: $0.course.name, - org: $0.course.org, - shortDescription: "", - imageURL: fullImageURL, - hasAccess: $0.course.coursewareAccess.hasAccess, - courseStart: $0.course.start != nil - ? Date(iso8601: $0.course.start!) - : nil, - courseEnd: $0.course.end != nil - ? Date(iso8601: $0.course.end!) - : nil, - enrollmentStart: $0.course.start != nil - ? Date(iso8601: $0.course.start!) - : nil, - enrollmentEnd: $0.course.end != nil - ? Date(iso8601: $0.course.end!) - : nil, - courseID: $0.course.id, - numPages: enrollments?.numPages ?? 1, - coursesCount: enrollments?.count ?? 0, - progressEarned: $0.progress?.assignmentsCompleted ?? 0, - progressPossible: $0.progress?.totalAssignmentsCount ?? 0 - ) - } + let primaryCourse = createPrimaryCourse(from: self.primary, baseURL: baseURL) + let courses = createCourseItems(from: self.enrollments, baseURL: baseURL) return PrimaryEnrollment( primaryCourse: primaryCourse, - courses: courses ?? [], + courses: courses, totalPages: enrollments?.numPages ?? 1, count: enrollments?.count ?? 1 ) } + + private func createPrimaryCourse(from primary: DataLayer.Primary?, baseURL: String) -> PrimaryCourse? { + guard let primary = primary else { return nil } + + let futureAssignments = primary.courseAssignments?.futureAssignments ?? [] + let pastAssignments = primary.courseAssignments?.pastAssignments ?? [] + + return PrimaryCourse( + name: primary.course?.name ?? "", + org: primary.course?.org ?? "", + courseID: primary.course?.id ?? "", + hasAccess: primary.course?.coursewareAccess.hasAccess ?? true, + courseStart: primary.course?.start.flatMap { Date(iso8601: $0) }, + courseEnd: primary.course?.end.flatMap { Date(iso8601: $0) }, + courseBanner: baseURL + (primary.course?.media.courseImage?.url ?? ""), + futureAssignments: futureAssignments.map { createAssignment(from: $0) }, + pastAssignments: pastAssignments.map { createAssignment(from: $0) }, + progressEarned: primary.progress?.assignmentsCompleted ?? 0, + progressPossible: primary.progress?.totalAssignmentsCount ?? 0, + lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, + resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName + ) + } + + private func createAssignment(from assignment: DataLayer.Assignment) -> Assignment { + return Assignment( + type: assignment.assignmentType ?? "", + title: assignment.title ?? "", + description: assignment.description ?? "", + date: Date(iso8601: assignment.date ?? ""), + complete: assignment.complete ?? false, + firstComponentBlockId: assignment.firstComponentBlockID + ) + } + + private func createCourseItems(from enrollments: DataLayer.Enrollments?, baseURL: String) -> [CourseItem] { + return enrollments?.results.map { + createCourseItem( + from: $0, + baseURL: baseURL, + numPages: enrollments?.numPages ?? 1, + count: enrollments?.count ?? 0 + ) + } ?? [] + } + + private func createCourseItem( + from enrollment: DataLayer.Enrollment, + baseURL: String, + numPages: Int, + count: Int + ) -> CourseItem { + let imageUrl = enrollment.course.media.courseImage?.url ?? "" + let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let fullImageURL = baseURL + encodedUrl + + return CourseItem( + name: enrollment.course.name, + org: enrollment.course.org, + shortDescription: "", + imageURL: fullImageURL, + hasAccess: enrollment.course.coursewareAccess.hasAccess, + courseStart: enrollment.course.start.flatMap { Date(iso8601: $0) }, + courseEnd: enrollment.course.end.flatMap { Date(iso8601: $0) }, + enrollmentStart: enrollment.course.start.flatMap { Date(iso8601: $0) }, + enrollmentEnd: enrollment.course.end.flatMap { Date(iso8601: $0) }, + courseID: enrollment.course.id, + numPages: numPages, + coursesCount: count, + progressEarned: enrollment.progress?.assignmentsCompleted ?? 0, + progressPossible: enrollment.progress?.totalAssignmentsCount ?? 0 + ) + } } diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index faa821bc2..4bd41f9eb 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -99,6 +99,8 @@ public enum CoreLocalization { public static let mmmDdYyyy = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMM_DD_YYYY", fallback: "MMM dd, yyyy") /// MMMM dd public static let mmmmDd = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMMM_DD", fallback: "MMMM dd") + /// MMMM dd, yyyy + public static let mmmmDdYyyy = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMMM_DD_YYYY", fallback: "MMMM dd, yyyy") } public enum DownloadManager { /// Completed diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index ca5657132..44186571a 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -69,6 +69,7 @@ "DATE_FORMAT.MMMM_DD" = "MMMM dd"; "DATE_FORMAT.MMM_DD_YYYY" = "MMM dd, yyyy"; +"DATE_FORMAT.MMMM_DD_YYYY" = "MMMM dd, yyyy"; "DOWNLOAD_MANAGER.DOWNLOAD" = "Download"; "DOWNLOAD_MANAGER.DOWNLOADED" = "Downloaded"; diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index f2e477c1c..13e9602e7 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -33,8 +33,8 @@ public struct AllCoursesView: View { } ) .backViewStyle() - .padding(.leading, isHorizontal ? 48 : 9) - .padding(.top, 13) + .padding(.top, isHorizontal ? 32 : 16) + .padding(.leading, 7) }.frame(minWidth: 0, maxWidth: .infinity, @@ -42,18 +42,21 @@ public struct AllCoursesView: View { .zIndex(1) if let myEnrollments = viewModel.myEnrollments, - myEnrollments.courses.isEmpty, - !viewModel.fetchInProgress { + myEnrollments.courses.isEmpty, + !viewModel.fetchInProgress, + !viewModel.refresh { NoCoursesView(selectedMenu: viewModel.selectedMenu) } // MARK: - Page body VStack(alignment: .center) { + learnTitleAndSearch() + .frameLimit(width: proxy.size.width) RefreshableScrollViewCompat(action: { await viewModel.getCourses(page: 1, refresh: true) }) { - learnTitleAndSearch() CategoryFilterView(selectedOption: $viewModel.selectedMenu) .disabled(viewModel.fetchInProgress) + .frameLimit(width: proxy.size.width) if let myEnrollments = viewModel.myEnrollments { LazyVGrid(columns: columns(), spacing: 15) { ForEach( @@ -85,7 +88,7 @@ public struct AllCoursesView: View { courseStartDate: course.courseStart, courseEndDate: course.courseEnd, hasAccess: course.hasAccess, - isFullCard: false + showProgress: true ).padding(8) }) .accessibilityIdentifier("course_item") @@ -100,7 +103,7 @@ public struct AllCoursesView: View { .frameLimit(width: proxy.size.width) } // MARK: - ProgressBar - if viewModel.nextPage <= viewModel.totalPages { + if viewModel.nextPage <= viewModel.totalPages, !viewModel.refresh { VStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 20) @@ -144,7 +147,8 @@ public struct AllCoursesView: View { } .onChange(of: viewModel.selectedMenu) { _ in Task { - await viewModel.getCourses(page: 1, refresh: true) + viewModel.myEnrollments?.courses = [] + await viewModel.getCourses(page: 1, refresh: false) } } .background( @@ -188,9 +192,10 @@ public struct AllCoursesView: View { ) } .padding(.horizontal, 20) - .padding(.vertical, 20) + .padding(.top, 20) + .padding(.bottom, 10) .accessibilityElement(children: .ignore) - .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) + .accessibilityLabel(DashboardLocalization.Learn.allCourses) } } diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift index 4b74aff3d..b045ba5df 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -15,6 +15,7 @@ public class AllCoursesViewModel: ObservableObject { var nextPage = 1 var totalPages = 1 @Published private(set) var fetchInProgress = false + @Published private(set) var refresh = false @Published var selectedMenu: CategoryOption = .all @Published var myEnrollments: PrimaryEnrollment? @@ -53,15 +54,17 @@ public class AllCoursesViewModel: ObservableObject { @MainActor public func getCourses(page: Int, refresh: Bool = false) async { - fetchInProgress = true + self.refresh = refresh do { if refresh || page == 1 { + fetchInProgress = true myEnrollments?.courses = [] nextPage = 1 myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) self.totalPages = myEnrollments?.totalPages ?? 1 self.nextPage = 2 } else { + fetchInProgress = true myEnrollments?.courses += try await interactor.getAllCourses( filteredBy: selectedMenu.status, page: page ).courses @@ -69,8 +72,10 @@ public class AllCoursesViewModel: ObservableObject { } totalPages = myEnrollments?.totalPages ?? 1 fetchInProgress = false + self.refresh = false } catch let error { fetchInProgress = false + self.refresh = false if error is NoCachedDataError { errorMessage = CoreLocalization.Error.noCachedData } else { diff --git a/Dashboard/Dashboard/Presentation/DashboardRouter.swift b/Dashboard/Dashboard/Presentation/DashboardRouter.swift index dabddd0a9..0e66ff2a8 100644 --- a/Dashboard/Dashboard/Presentation/DashboardRouter.swift +++ b/Dashboard/Dashboard/Presentation/DashboardRouter.swift @@ -24,6 +24,8 @@ public protocol DashboardRouter: BaseRouter { func showDiscoverySearch(searchQuery: String?) + func showSettings() + } // Mark - For testing and SwiftUI preview @@ -46,5 +48,6 @@ public class DashboardRouterMock: BaseRouterMock, DashboardRouter { public func showDiscoverySearch(searchQuery: String?) {} + public func showSettings() {} } #endif diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift index 97395999b..7a1f03ba1 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -19,7 +19,7 @@ struct CourseCardView: View { private let courseStartDate: Date? private let courseEndDate: Date? private let hasAccess: Bool - private let isFullCard: Bool + private let showProgress: Bool init( courseName: String, @@ -29,7 +29,7 @@ struct CourseCardView: View { courseStartDate: Date?, courseEndDate: Date?, hasAccess: Bool, - isFullCard: Bool + showProgress: Bool ) { self.courseName = courseName self.courseImage = courseImage @@ -38,14 +38,14 @@ struct CourseCardView: View { self.courseStartDate = courseStartDate self.courseEndDate = courseEndDate self.hasAccess = hasAccess - self.isFullCard = isFullCard + self.showProgress = showProgress } var body: some View { ZStack(alignment: .topTrailing) { VStack(alignment: .leading, spacing: 0) { courseBanner - if isFullCard { + if showProgress { ProgressLineView( progressEarned: progressEarned, progressPossible: progressPossible, @@ -101,7 +101,7 @@ struct CourseCardView: View { .lineLimit(2) .multilineTextAlignment(.leading) } - .frame(height: isFullCard ? 51 : 40, alignment: .topLeading) + .frame(height: showProgress ? 51 : 40, alignment: .topLeading) .fixedSize(horizontal: false, vertical: true) .padding(.top, 10) .padding(.horizontal, 12) @@ -119,7 +119,7 @@ struct CourseCardView: View { courseStartDate: nil, courseEndDate: Date(), hasAccess: true, - isFullCard: true + showProgress: true ).frame(width: 170) } #endif diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift index 548b58e89..e622f2fd3 100644 --- a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -26,7 +26,7 @@ struct ProgressLineView: View { var body: some View { ZStack(alignment: .leading) { - if progressPossible != 0 { +// if progressPossible != 0 { GeometryReader { geometry in Rectangle() .foregroundStyle(Theme.Colors.cardViewStroke) @@ -34,7 +34,7 @@ struct ProgressLineView: View { .foregroundStyle(Theme.Colors.accentColor) .frame(width: geometry.size.width * progressValue) }.frame(height: height) - } +// } } } } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index b7b98bc6a..9c52fd7db 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -15,7 +15,7 @@ public struct PrimaryCourseDashboardView: View { @StateObject private var viewModel: PrimaryCourseDashboardViewModel private let router: DashboardRouter - private let config = Container.shared.resolve(ConfigProtocol.self) + private let config = Container.shared.resolve(ConfigProtocol.self)! @ViewBuilder let programView: ProgramView private var openDiscoveryPage: () -> Void private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -42,8 +42,7 @@ public struct PrimaryCourseDashboardView: View { openDiscoveryPage() }).zIndex(1) } - learnTitleAndSearch() - .frameLimit(width: proxy.size.width) + learnTitleAndSearch(proxy: proxy) .zIndex(1) // MARK: - Page body VStack(alignment: .leading) { @@ -60,7 +59,7 @@ public struct PrimaryCourseDashboardView: View { maxHeight: .infinity) } else { LazyVStack(spacing: 0) { - Spacer(minLength: 50) + Spacer(minLength: 40) switch selectedMenu { case .courses: if let enrollments = viewModel.enrollments { @@ -144,8 +143,6 @@ public struct PrimaryCourseDashboardView: View { } } Spacer(minLength: 100) - } else { - EmptyPageIcon() } case .programs: programView @@ -156,14 +153,13 @@ public struct PrimaryCourseDashboardView: View { .frameLimit(width: proxy.size.width) }.accessibilityAction {} }.padding(.top, 8) - // MARK: - Offline mode SnackBar OfflineSnackBarView( connectivity: viewModel.connectivity, reloadAction: { await viewModel.getEnrollments(showProgress: false) } - ) + ).zIndex(2) // MARK: - Error Alert if viewModel.showError { @@ -181,6 +177,7 @@ public struct PrimaryCourseDashboardView: View { viewModel.errorMessage = nil } } + .zIndex(2) } } .onFirstAppear { @@ -229,7 +226,7 @@ public struct PrimaryCourseDashboardView: View { courseStartDate: nil, courseEndDate: nil, hasAccess: course.hasAccess, - isFullCard: false + showProgress: false ).frame(width: idiom == .pad ? nil : 120) } ) @@ -270,7 +267,7 @@ public struct PrimaryCourseDashboardView: View { router.showAllCourses(courses: enrollments.courses) }, label: { HStack { - Text(DashboardLocalization.Learn.viewAllCourses(enrollments.count)) + Text(DashboardLocalization.Learn.viewAllCourses(enrollments.count + 1)) .font(Theme.Fonts.titleSmall) .accessibilityIdentifier("courses_welcomeback_text") Image(systemName: "chevron.right") @@ -281,29 +278,36 @@ public struct PrimaryCourseDashboardView: View { }) } - private func learnTitleAndSearch() -> some View { - ZStack(alignment: .top) { + private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { + let showDropdown = config.program.enabled && config.program.isWebViewConfigured + return ZStack(alignment: .top) { Theme.Colors.background - .frame(height: 70) - VStack(alignment: .leading) { - HStack(alignment: .center) { - Text(DashboardLocalization.Learn.title) - .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier("courses_header_text") + .frame(height: showDropdown ? 70 : 50) + ZStack(alignment: .trailing) { + VStack { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.title) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("courses_header_text") + Spacer() + } + if showDropdown { + DropDownMenu(selectedOption: $selectedMenu) + } + } + .frameLimit(width: proxy.size.width) + HStack { Spacer() Button(action: { - router.showDiscoverySearch(searchQuery: "") + router.showSettings() }, label: { - Image(systemName: "magnifyingglass") - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier(DashboardLocalization.search) + CoreAssets.settings.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) }) } - if let config, config.program.enabled, config.program.isWebViewConfigured { - DropDownMenu(selectedOption: $selectedMenu) - } } + .offset(x: idiom == .pad ? 1 : 4, y: idiom == .pad ? 4 : -5) .listRowBackground(Color.clear) .padding(.horizontal, 20) .accessibilityElement(children: .ignore) diff --git a/Dashboard/Dashboard/SwiftGen/Strings.swift b/Dashboard/Dashboard/SwiftGen/Strings.swift index e6cc9564c..7b5924613 100644 --- a/Dashboard/Dashboard/SwiftGen/Strings.swift +++ b/Dashboard/Dashboard/SwiftGen/Strings.swift @@ -61,8 +61,8 @@ public enum DashboardLocalization { public static let noCompletedCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES", fallback: "No Completed Courses") /// No Courses public static let noCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES", fallback: "No Courses") - /// ou are not currently enrolled in any courses, would you like to explore the course catalog? - public static let noCoursesDescription = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION", fallback: "ou are not currently enrolled in any courses, would you like to explore the course catalog?") + /// You are not currently enrolled in any courses, would you like to explore the course catalog? + public static let noCoursesDescription = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION", fallback: "You are not currently enrolled in any courses, would you like to explore the course catalog?") /// No Courses in Progress public static let noCoursesInProgress = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS", fallback: "No Courses in Progress") /// No Expired Courses diff --git a/Dashboard/Dashboard/en.lproj/Localizable.strings b/Dashboard/Dashboard/en.lproj/Localizable.strings index 984dd4d60..406b6c34e 100644 --- a/Dashboard/Dashboard/en.lproj/Localizable.strings +++ b/Dashboard/Dashboard/en.lproj/Localizable.strings @@ -40,6 +40,6 @@ "LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES" = "No Completed Courses"; "LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES" = "No Expired Courses"; -"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "ou are not currently enrolled in any courses, would you like to explore the course catalog?"; +"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "You are not currently enrolled in any courses, would you like to explore the course catalog?"; "LEARN.NO_COURSES_VIEW.FIND_A_COURSE" = "Find a Course"; diff --git a/Dashboard/Dashboard/uk.lproj/Localizable.strings b/Dashboard/Dashboard/uk.lproj/Localizable.strings index 378016deb..e02337c90 100644 --- a/Dashboard/Dashboard/uk.lproj/Localizable.strings +++ b/Dashboard/Dashboard/uk.lproj/Localizable.strings @@ -40,6 +40,6 @@ "LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES" = "Немає завершених курсів"; "LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES" = "Немає прострочених курсів"; -"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "наразі ви не зареєстровані на жодному курсі, бажаєте переглянути каталог?"; +"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "Наразі ви не зареєстровані на жодному курсі, бажаєте переглянути каталог?"; "LEARN.NO_COURSES_VIEW.FIND_A_COURSE" = "Знайти курс"; diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index 265631697..99d7ecea9 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -173,8 +173,8 @@ public struct SearchView: View { .onDisappear { viewModel.searchText = "" } - .background(Theme.Colors.background.ignoresSafeArea()) .avoidKeyboard(dismissKeyboardByTap: true) + .background(Theme.Colors.background.ignoresSafeArea()) } } From f1c7bf3cdc4cf140dbd402589e720dcb19a73b6a Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 10 May 2024 17:20:32 +0300 Subject: [PATCH 06/22] fix: address feedback --- .../Presentation/PrimaryCourseDashboardView.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 9c52fd7db..841445762 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -146,6 +146,7 @@ public struct PrimaryCourseDashboardView: View { } case .programs: programView + .padding(.top, 50) } } } @@ -283,7 +284,7 @@ public struct PrimaryCourseDashboardView: View { return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) - ZStack(alignment: .trailing) { + ZStack(alignment: .topTrailing) { VStack { HStack(alignment: .center) { Text(DashboardLocalization.Learn.title) @@ -293,7 +294,10 @@ public struct PrimaryCourseDashboardView: View { Spacer() } if showDropdown { - DropDownMenu(selectedOption: $selectedMenu) + HStack(alignment: .center) { + DropDownMenu(selectedOption: $selectedMenu) + Spacer() + } } } .frameLimit(width: proxy.size.width) @@ -306,8 +310,10 @@ public struct PrimaryCourseDashboardView: View { .foregroundColor(Theme.Colors.accentColor) }) } + .padding(.top, 8) + .offset(x: idiom == .pad ? 1 : 5, y: idiom == .pad ? 4 : -5) } - .offset(x: idiom == .pad ? 1 : 4, y: idiom == .pad ? 4 : -5) + .listRowBackground(Color.clear) .padding(.horizontal, 20) .accessibilityElement(children: .ignore) From 19a265e42d489a7cd8898922dea7cf5b6dbaf282 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 10 May 2024 17:38:27 +0300 Subject: [PATCH 07/22] fix: address feedback --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 841445762..a47e88f52 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -46,7 +46,7 @@ public struct PrimaryCourseDashboardView: View { .zIndex(1) // MARK: - Page body VStack(alignment: .leading) { - + Spacer(minLength: 50) RefreshableScrollViewCompat(action: { await viewModel.getEnrollments(showProgress: false) }) { @@ -59,7 +59,6 @@ public struct PrimaryCourseDashboardView: View { maxHeight: .infinity) } else { LazyVStack(spacing: 0) { - Spacer(minLength: 40) switch selectedMenu { case .courses: if let enrollments = viewModel.enrollments { From 5bce027df342eed6118165004721640f9e378f4d Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Tue, 14 May 2024 00:09:03 +0300 Subject: [PATCH 08/22] fix: address feedback --- .../settings.imageset/Contents.json | 2 +- .../icon-manage_accounts.svg | 5 +++++ .../settings.imageset/settingsIcon.svg | 4 ---- .../Elements/CategoryFilterView.swift | 19 ++++++++++++++----- .../Presentation/Elements/DropDownMenu.swift | 8 ++++++-- .../Elements/PrimaryCardView.swift | 2 +- .../PrimaryCourseDashboardView.swift | 4 ++-- .../Colors/AccentColor.colorset/Contents.json | 6 +++--- .../AccentXColor.colorset/Contents.json | 6 +++--- 9 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg delete mode 100644 Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg diff --git a/Core/Core/Assets.xcassets/settings.imageset/Contents.json b/Core/Core/Assets.xcassets/settings.imageset/Contents.json index aa6427af7..30cb38b07 100644 --- a/Core/Core/Assets.xcassets/settings.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/settings.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "settingsIcon.svg", + "filename" : "icon-manage_accounts.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg b/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg new file mode 100644 index 000000000..5cf416fb2 --- /dev/null +++ b/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg b/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg deleted file mode 100644 index c1181ff8e..000000000 --- a/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift index 8c4142ee3..6200ddd76 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift @@ -44,19 +44,25 @@ enum CategoryOption: String, CaseIterable { struct CategoryFilterView: View { @Binding var selectedOption: CategoryOption + @Environment (\.colorScheme) var colorScheme var body: some View { ScrollView(.horizontal) { HStack(spacing: 8) { - ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { index, option in + ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { + index, + option in Button(action: { selectedOption = option - }, label: { + }, + label: { HStack { Text(option.text) .font(Theme.Fonts.titleSmall) .foregroundColor( - option == selectedOption ? Theme.Colors.white : Theme.Colors.accentColor + option == selectedOption ? Theme.Colors.white : ( + colorScheme == .light ? Theme.Colors.accentColor : .white + ) ) } .padding(.horizontal, 17) @@ -67,10 +73,13 @@ struct CategoryFilterView: View { .foregroundStyle( option == selectedOption ? Theme.Colors.accentColor - : Theme.Colors.background + : Theme.Colors.cardViewBackground ) RoundedRectangle(cornerRadius: 20) - .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + .stroke( + colorScheme == .light ? Theme.Colors.accentColor : .clear, + style: .init(lineWidth: 1) + ) } .padding(.vertical, 1) } diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index 0398b7495..f1439dbe2 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -33,7 +33,11 @@ struct DropDownMenu: View { Text(selectedOption.text) .font(Theme.Fonts.titleSmall) .accessibilityIdentifier("dropdown_menu_text") - Image(systemName: expanded ? "chevron.up" : "chevron.down") + Image(systemName: "chevron.up") + .rotation3DEffect( + .degrees(expanded ? 180 : 0), + axis: (x: 1.0, y: 0.0, z: 0.0) + ) } .foregroundColor(Theme.Colors.textPrimary) .onTapGesture { @@ -65,7 +69,7 @@ struct DropDownMenu: View { br: index == MenuOption.allCases.count-1 ? 8 : 0) .foregroundStyle(option == selectedOption ? Theme.Colors.accentColor - : Theme.Colors.background) + : Theme.Colors.cardViewBackground) RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, br: index == MenuOption.allCases.count-1 ? 8 : 0) .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 1d7f33939..4f52f1446 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -165,7 +165,7 @@ public struct PrimaryCardView: View { action() }, label: { ZStack(alignment: .top) { - Rectangle().frame(height: 1) + Rectangle().frame(height: selected ? 0 : 1) .foregroundStyle(Theme.Colors.cardViewStroke) HStack(alignment: .center) { VStack(alignment: .leading) { diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index a47e88f52..ad76c10e4 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -256,7 +256,7 @@ public struct PrimaryCourseDashboardView: View { } .frame(width: idiom == .pad ? nil : 120) } - .background(Theme.Colors.background) + .background(Theme.Colors.cardViewBackground) .cornerRadius(8) .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) }) @@ -279,7 +279,7 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { - let showDropdown = config.program.enabled && config.program.isWebViewConfigured + let showDropdown = true//config.program.enabled && config.program.isWebViewConfigured return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json index 00d59cb46..bf1a96417 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xF8", + "green" : "0x78", + "red" : "0x53" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json index 00d59cb46..bf1a96417 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xF8", + "green" : "0x78", + "red" : "0x53" } }, "idiom" : "universal" From 3012c6f95e32a65a1adc12dd72396b2be754faf1 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Wed, 15 May 2024 12:06:43 +0300 Subject: [PATCH 09/22] fix: change DropDown menu arrow direction --- Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift | 2 +- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index f1439dbe2..c6f28c3b3 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -33,7 +33,7 @@ struct DropDownMenu: View { Text(selectedOption.text) .font(Theme.Fonts.titleSmall) .accessibilityIdentifier("dropdown_menu_text") - Image(systemName: "chevron.up") + Image(systemName: "chevron.down") .rotation3DEffect( .degrees(expanded ? 180 : 0), axis: (x: 1.0, y: 0.0, z: 0.0) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index ad76c10e4..410eaa5e1 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -279,7 +279,7 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { - let showDropdown = true//config.program.enabled && config.program.isWebViewConfigured + let showDropdown = config.program.enabled && config.program.isWebViewConfigured return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) From 0a1ce7c26e39439b68a9c6d1be6510bb13c237de Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 16 May 2024 12:10:18 +0300 Subject: [PATCH 10/22] fix: address feedback --- .../Data/Model/Data_PrimaryEnrollment.swift | 22 ++++++------ .../ScrollSlidingTabBar.swift | 3 ++ .../Elements/PrimaryCardView.swift | 36 ++++++++++--------- .../Elements/ProgressLineView.swift | 2 -- .../PrimaryCourseDashboardView.swift | 17 +++++---- .../PrimaryCourseDashboardViewModel.swift | 5 ++- OpenEdX/DI/ScreenAssembly.swift | 3 +- OpenEdX/View/MainScreenView.swift | 12 +++---- 8 files changed, 53 insertions(+), 47 deletions(-) diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index c21e1b5f5..a8ef19954 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -15,8 +15,8 @@ public extension DataLayer { enum CodingKeys: String, CodingKey { case userTimezone = "user_timezone" - case enrollments = "enrollments" - case primary = "primary" + case enrollments + case primary } public init(userTimezone: String?, enrollments: Enrollments?, primary: Primary?) { @@ -41,11 +41,11 @@ public extension DataLayer { enum CodingKeys: String, CodingKey { case auditAccessExpires = "audit_access_expires" - case created = "created" - case mode = "mode" + case created + case mode case isActive = "is_active" - case course = "course" - case certificate = "certificate" + case course + case certificate case courseModes = "course_modes" case courseStatus = "course_status" case progress = "course_progress" @@ -124,14 +124,14 @@ public extension DataLayer { enum CodingKeys: String, CodingKey { case assignmentType = "assignment_type" - case complete = "complete" - case date = "date" + case complete + case date case dateType = "date_type" - case description = "description" + case description case learnerHasAccess = "learner_has_access" - case link = "link" + case link case linkText = "link_text" - case title = "title" + case title case extraInfo = "extra_info" case firstComponentBlockID = "first_component_block_id" } diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift index bd558d197..ef300e279 100644 --- a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift +++ b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift @@ -49,6 +49,9 @@ public struct ScrollSlidingTabBar: View { } .onTapGesture {} // Fix button tapable area bug – https://forums.developer.apple.com/forums/thread/745059 + .onAppear { + proxy.scrollTo(selection, anchor: .center) + } .onChange(of: selection) { newValue in withAnimation { proxy.scrollTo(newValue, anchor: .center) diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 4f52f1446..8428c1857 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -23,8 +23,8 @@ public struct PrimaryCardView: View { private let progressPossible: Int private let canResume: Bool private let resumeTitle: String? - private var pastAssignmentAction: (String?) -> Void - private var futureAssignmentAction: (String?) -> Void + private var assignmentAction: (String?) -> Void + private var openCourseAction: () -> Void private var resumeAction: () -> Void public init( @@ -39,8 +39,8 @@ public struct PrimaryCardView: View { progressPossible: Int, canResume: Bool, resumeTitle: String?, - pastAssignmentAction: @escaping (String?) -> Void, - futureAssignmentAction: @escaping (String?) -> Void, + assignmentAction: @escaping (String?) -> Void, + openCourseAction: @escaping () -> Void, resumeAction: @escaping () -> Void ) { self.courseName = courseName @@ -54,17 +54,22 @@ public struct PrimaryCardView: View { self.progressPossible = progressPossible self.canResume = canResume self.resumeTitle = resumeTitle - self.pastAssignmentAction = pastAssignmentAction - self.futureAssignmentAction = futureAssignmentAction + self.assignmentAction = assignmentAction + self.openCourseAction = openCourseAction self.resumeAction = resumeAction } public var body: some View { ZStack { VStack(alignment: .leading, spacing: 0) { - courseBanner - ProgressLineView(progressEarned: progressEarned, progressPossible: progressPossible) - courseTitle + Group { + courseBanner + ProgressLineView(progressEarned: progressEarned, progressPossible: progressPossible) + courseTitle + } + .onTapGesture { + openCourseAction() + } assignments } } @@ -83,7 +88,7 @@ public struct PrimaryCardView: View { description: DashboardLocalization.Learn.PrimaryCard.onePastAssignment, icon: CoreAssets.warning.swiftUIImage, selected: false, - action: { pastAssignmentAction(pastAssignments.first?.firstComponentBlockId) } + action: { assignmentAction(pastAssignments.first?.firstComponentBlockId) } ) } else if pastAssignments.count > 1 { courseButton( @@ -91,7 +96,7 @@ public struct PrimaryCardView: View { description: DashboardLocalization.Learn.PrimaryCard.pastAssignments(pastAssignments.count), icon: CoreAssets.warning.swiftUIImage, selected: false, - action: { pastAssignmentAction(nil) } + action: { assignmentAction(nil) } ) } @@ -112,7 +117,7 @@ public struct PrimaryCardView: View { icon: CoreAssets.chapter.swiftUIImage, selected: false, action: { - futureAssignmentAction(futureAssignments.first?.firstComponentBlockId) + assignmentAction(futureAssignments.first?.firstComponentBlockId) } ) } else if futureAssignments.count > 1 { @@ -126,7 +131,7 @@ public struct PrimaryCardView: View { icon: CoreAssets.chapter.swiftUIImage, selected: false, action: { - futureAssignmentAction(nil) + assignmentAction(nil) } ) } @@ -214,7 +219,6 @@ public struct PrimaryCardView: View { .aspectRatio(contentMode: .fill) .frame(height: 140) .clipped() - .allowsHitTesting(false) .accessibilityElement(children: .ignore) .accessibilityIdentifier("course_image") } @@ -261,8 +265,8 @@ struct PrimaryCardView_Previews: PreviewProvider { progressPossible: 45, canResume: true, resumeTitle: "Course Chapter 1", - pastAssignmentAction: {_ in }, - futureAssignmentAction: {_ in }, + assignmentAction: {_ in }, + openCourseAction: {}, resumeAction: {} ) .loadFonts() diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift index e622f2fd3..611ddbcb1 100644 --- a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -26,7 +26,6 @@ struct ProgressLineView: View { var body: some View { ZStack(alignment: .leading) { -// if progressPossible != 0 { GeometryReader { geometry in Rectangle() .foregroundStyle(Theme.Colors.cardViewStroke) @@ -34,7 +33,6 @@ struct ProgressLineView: View { .foregroundStyle(Theme.Colors.accentColor) .frame(width: geometry.size.width * progressValue) }.frame(height: height) -// } } } } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 410eaa5e1..a53ae872f 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -12,10 +12,8 @@ import Swinject public struct PrimaryCourseDashboardView: View { - @StateObject - private var viewModel: PrimaryCourseDashboardViewModel + @StateObject private var viewModel: PrimaryCourseDashboardViewModel private let router: DashboardRouter - private let config = Container.shared.resolve(ConfigProtocol.self)! @ViewBuilder let programView: ProgramView private var openDiscoveryPage: () -> Void private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -75,7 +73,7 @@ public struct PrimaryCourseDashboardView: View { progressPossible: primary.progressPossible, canResume: primary.lastVisitedBlockID != nil, resumeTitle: primary.resumeTitle, - pastAssignmentAction: { lastVisitedBlockID in + assignmentAction: { lastVisitedBlockID in router.showCourseScreens( courseID: primary.courseID, hasAccess: primary.hasAccess, @@ -88,7 +86,7 @@ public struct PrimaryCourseDashboardView: View { lastVisitedBlockID: lastVisitedBlockID ) }, - futureAssignmentAction: { lastVisitedBlockID in + openCourseAction: { router.showCourseScreens( courseID: primary.courseID, hasAccess: primary.hasAccess, @@ -97,8 +95,8 @@ public struct PrimaryCourseDashboardView: View { enrollmentStart: nil, enrollmentEnd: nil, title: primary.name, - showDates: lastVisitedBlockID == nil, - lastVisitedBlockID: lastVisitedBlockID + showDates: false, + lastVisitedBlockID: nil ) }, resumeAction: { @@ -279,7 +277,7 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { - let showDropdown = config.program.enabled && config.program.isWebViewConfigured + let showDropdown = viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) @@ -327,7 +325,8 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { let vm = PrimaryCourseDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), - analytics: DashboardAnalyticsMock() + analytics: DashboardAnalyticsMock(), + config: ConfigMock() ) PrimaryCourseDashboardView( diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 847ed41c6..5ae1c7bd7 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -28,6 +28,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics + let config: ConfigProtocol private var onCourseEnrolledCancellable: AnyCancellable? private let ipadPageSize = 7 @@ -36,11 +37,13 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { public init( interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics + analytics: DashboardAnalytics, + config: ConfigProtocol ) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics + self.config = config onCourseEnrolledCancellable = NotificationCenter.default .publisher(for: .onCourseEnrolled) diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 3539fee7c..9f6a37e77 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -173,7 +173,8 @@ class ScreenAssembly: Assembly { PrimaryCourseDashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - analytics: r.resolve(DashboardAnalytics.self)! + analytics: r.resolve(DashboardAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)! ) } diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index 275acbcc8..e9e049245 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -22,8 +22,6 @@ struct MainScreenView: View { @ObservedObject private(set) var viewModel: MainScreenViewModel - private let config = Container.shared.resolve(ConfigProtocol.self)! - init(viewModel: MainScreenViewModel) { self.viewModel = viewModel UITabBar.appearance().isTranslucent = false @@ -39,7 +37,7 @@ struct MainScreenView: View { var body: some View { TabView(selection: $viewModel.selection) { - switch config.dashboard.type { + switch viewModel.config.dashboard.type { case .list: ZStack { ListDashboardView( @@ -80,15 +78,15 @@ struct MainScreenView: View { .accessibilityIdentifier("dashboard_tabitem") } - if config.discovery.enabled { + if viewModel.config.discovery.enabled { ZStack { - if config.discovery.type == .native { + if viewModel.config.discovery.type == .native { DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, sourceScreen: viewModel.sourceScreen ) - } else if config.discovery.type == .webview { + } else if viewModel.config.discovery.type == .webview { DiscoveryWebview( viewModel: Container.shared.resolve( DiscoveryWebviewViewModel.self, @@ -173,7 +171,7 @@ struct MainScreenView: View { case .discovery: return DiscoveryLocalization.title case .dashboard: - return config.dashboard.type == .list + return viewModel.config.dashboard.type == .list ? DashboardLocalization.title : DashboardLocalization.Learn.title case .programs: From 296c0937f1ae45bca66f7844c8a69fa8189de583 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Mon, 20 May 2024 18:50:46 +0300 Subject: [PATCH 11/22] fix: address feedback --- Core/Core/Data/Model/Data_Enrollments.swift | 4 +- .../Data/Model/Data_PrimaryEnrollment.swift | 16 +++--- Core/Core/Extensions/Notification.swift | 1 + Core/Core/Extensions/ViewExtension.swift | 16 ++++++ Course/Course/Domain/CourseInteractor.swift | 1 + .../Container/CourseContainerViewModel.swift | 49 +++++++++---------- .../Presentation/AllCoursesViewModel.swift | 3 +- .../Elements/CategoryFilterView.swift | 4 +- .../Elements/CourseCardView.swift | 2 +- .../Presentation/Elements/DropDownMenu.swift | 1 + .../Presentation/Elements/NoCoursesView.swift | 10 ++-- .../PrimaryCourseDashboardViewModel.swift | 12 +++-- 12 files changed, 69 insertions(+), 50 deletions(-) diff --git a/Core/Core/Data/Model/Data_Enrollments.swift b/Core/Core/Data/Model/Data_Enrollments.swift index b634cb665..527a69daa 100644 --- a/Core/Core/Data/Model/Data_Enrollments.swift +++ b/Core/Core/Data/Model/Data_Enrollments.swift @@ -68,7 +68,7 @@ public extension DataLayer { public let isActive: Bool public let course: DashboardCourse public let courseModes: [CourseMode] - public let progress: Progress? + public let progress: CourseProgress? enum CodingKeys: String, CodingKey { case auditAccessExpires = "audit_access_expires" @@ -87,7 +87,7 @@ public extension DataLayer { isActive: Bool, course: DashboardCourse, courseModes: [CourseMode], - progress: Progress? + progress: CourseProgress? ) { self.auditAccessExpires = auditAccessExpires self.created = created diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift index a8ef19954..1102bae78 100644 --- a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -11,7 +11,7 @@ public extension DataLayer { struct PrimaryEnrollment: Codable { public let userTimezone: String? public let enrollments: Enrollments? - public let primary: Primary? + public let primary: ActiveEnrollment? enum CodingKeys: String, CodingKey { case userTimezone = "user_timezone" @@ -19,7 +19,7 @@ public extension DataLayer { case primary } - public init(userTimezone: String?, enrollments: Enrollments?, primary: Primary?) { + public init(userTimezone: String?, enrollments: Enrollments?, primary: ActiveEnrollment?) { self.userTimezone = userTimezone self.enrollments = enrollments self.primary = primary @@ -27,7 +27,7 @@ public extension DataLayer { } // MARK: - Primary - struct Primary: Codable { + struct ActiveEnrollment: Codable { public let auditAccessExpires: Date? public let created: String? public let mode: String? @@ -36,7 +36,7 @@ public extension DataLayer { public let certificate: DataLayer.Certificate? public let courseModes: [CourseMode]? public let courseStatus: CourseStatus? - public let progress: Progress? + public let progress: CourseProgress? public let courseAssignments: CourseAssignments? enum CodingKeys: String, CodingKey { @@ -61,7 +61,7 @@ public extension DataLayer { certificate: DataLayer.Certificate?, courseModes: [CourseMode]?, courseStatus: CourseStatus?, - progress: Progress?, + progress: CourseProgress?, courseAssignments: CourseAssignments? ) { self.auditAccessExpires = auditAccessExpires @@ -163,8 +163,8 @@ public extension DataLayer { } } - // MARK: - Progress - struct Progress: Codable { + // MARK: - CourseProgress + struct CourseProgress: Codable { public let assignmentsCompleted: Int? public let totalAssignmentsCount: Int? @@ -194,7 +194,7 @@ public extension DataLayer.PrimaryEnrollment { ) } - private func createPrimaryCourse(from primary: DataLayer.Primary?, baseURL: String) -> PrimaryCourse? { + private func createPrimaryCourse(from primary: DataLayer.ActiveEnrollment?, baseURL: String) -> PrimaryCourse? { guard let primary = primary else { return nil } let futureAssignments = primary.courseAssignments?.futureAssignments ?? [] diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index 9f792fb2a..d8e99731b 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -9,6 +9,7 @@ import Foundation public extension Notification.Name { static let onCourseEnrolled = Notification.Name("onCourseEnrolled") + static let onblockCompletionRequested = Notification.Name("onblockCompletionRequested") static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed") static let onActualVersionReceived = Notification.Name("onActualVersionReceived") static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped") diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index 71392ebd7..9db5e1087 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -237,6 +237,22 @@ public extension View { .padding(.horizontal, 8) .offset(y: topPadding) } + + @ViewBuilder + private func onTapBackgroundContent(enabled: Bool, _ action: @escaping () -> Void) -> some View { + if enabled { + Color.clear + .frame(width: UIScreen.main.bounds.width * 2, height: UIScreen.main.bounds.height * 2) + .contentShape(Rectangle()) + .onTapGesture(perform: action) + } + } + + func onTapBackground(enabled: Bool, _ action: @escaping () -> Void) -> some View { + background( + onTapBackgroundContent(enabled: enabled, action) + ) + } } public extension View { diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index 1b01e7a59..dfec16d70 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -64,6 +64,7 @@ public class CourseInteractor: CourseInteractorProtocol { } public func blockCompletionRequest(courseID: String, blockID: String) async throws { + NotificationCenter.default.post(name: .onblockCompletionRequested, object: courseID) return try await repository.blockCompletionRequest(courseID: courseID, blockID: blockID) } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index dd31bb45e..f2678a915 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -138,33 +138,32 @@ public class CourseContainerViewModel: BaseCourseViewModel { } func openLastVisitedBlock() { - if let continueWith = continueWith, - let courseStructure = courseStructure { - let chapter = courseStructure.childs[continueWith.chapterIndex] - let sequential = chapter.childs[continueWith.sequentialIndex] - let continueUnit = sequential.childs[continueWith.verticalIndex] - - var continueBlock: CourseBlock? - continueUnit.childs.forEach { block in - if block.id == continueWith.lastVisitedBlockId { - continueBlock = block - } + guard let continueWith = continueWith, + let courseStructure = courseStructure else { return } + let chapter = courseStructure.childs[continueWith.chapterIndex] + let sequential = chapter.childs[continueWith.sequentialIndex] + let continueUnit = sequential.childs[continueWith.verticalIndex] + + var continueBlock: CourseBlock? + continueUnit.childs.forEach { block in + if block.id == continueWith.lastVisitedBlockId { + continueBlock = block } - - trackResumeCourseClicked( - blockId: continueBlock?.id ?? "" - ) - - router.showCourseUnit( - courseName: courseStructure.displayName, - blockId: continueBlock?.id ?? "", - courseID: courseStructure.id, - verticalIndex: continueWith.verticalIndex, - chapters: courseStructure.childs, - chapterIndex: continueWith.chapterIndex, - sequentialIndex: continueWith.sequentialIndex - ) } + + trackResumeCourseClicked( + blockId: continueBlock?.id ?? "" + ) + + router.showCourseUnit( + courseName: courseStructure.displayName, + blockId: continueBlock?.id ?? "", + courseID: courseStructure.id, + verticalIndex: continueWith.verticalIndex, + chapters: courseStructure.childs, + chapterIndex: continueWith.chapterIndex, + sequentialIndex: continueWith.sequentialIndex + ) } @MainActor diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift index b045ba5df..750c0936b 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -62,14 +62,13 @@ public class AllCoursesViewModel: ObservableObject { nextPage = 1 myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) self.totalPages = myEnrollments?.totalPages ?? 1 - self.nextPage = 2 } else { fetchInProgress = true myEnrollments?.courses += try await interactor.getAllCourses( filteredBy: selectedMenu.status, page: page ).courses - self.nextPage += 1 } + self.nextPage += 1 totalPages = myEnrollments?.totalPages ?? 1 fetchInProgress = false self.refresh = false diff --git a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift index 6200ddd76..c01444840 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift @@ -49,9 +49,7 @@ struct CategoryFilterView: View { var body: some View { ScrollView(.horizontal) { HStack(spacing: 8) { - ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { - index, - option in + ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { index, option in Button(action: { selectedOption = option }, diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift index 7a1f03ba1..53a0911db 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -115,7 +115,7 @@ struct CourseCardView: View { courseName: "Six Sigma Part 2: Analyze, Improve, Control", courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", progressEarned: 4, - progressPossible: 8, + progressPossible: 8, courseStartDate: nil, courseEndDate: Date(), hasAccess: true, diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index c6f28c3b3..19fde450a 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -83,5 +83,6 @@ struct DropDownMenu: View { .fixedSize() } } + .onTapBackground(enabled: expanded, { expanded = false }) } } diff --git a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift index aa5847e16..82a471509 100644 --- a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift @@ -51,15 +51,15 @@ struct NoCoursesView: View { init(selectedMenu: CategoryOption) { switch selectedMenu { case .all: - self.type = .inProgress + type = .inProgress case .inProgress: - self.type = .inProgress + type = .inProgress case .completed: - self.type = .completed + type = .completed case .expired: - self.type = .expired + type = .expired } - self.openDiscovery = {} + openDiscovery = {} } var body: some View { diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 5ae1c7bd7..d6825b086 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -29,8 +29,8 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics let config: ConfigProtocol - private var onCourseEnrolledCancellable: AnyCancellable? - + private var cancellables = Set() + private let ipadPageSize = 7 private let iphonePageSize = 5 @@ -45,14 +45,18 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { self.analytics = analytics self.config = config - onCourseEnrolledCancellable = NotificationCenter.default - .publisher(for: .onCourseEnrolled) + let enrollmentPublisher = NotificationCenter.default.publisher(for: .onCourseEnrolled) + let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) + + enrollmentPublisher + .merge(with: completionPublisher) .sink { [weak self] _ in guard let self = self else { return } Task { await self.getEnrollments() } } + .store(in: &cancellables) } @MainActor From 52de67da54443651d9d9c687f22a0d18eca0bf85 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Tue, 21 May 2024 16:34:34 +0300 Subject: [PATCH 12/22] fix: address feedback --- .../Dashboard/Presentation/AllCoursesView.swift | 9 --------- .../PrimaryCourseDashboardView.swift | 5 ++++- .../PrimaryCourseDashboardViewModel.swift | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 13e9602e7..332ae13c4 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -181,15 +181,6 @@ public struct AllCoursesView: View { .foregroundColor(Theme.Colors.textPrimary) .accessibilityIdentifier("all_courses_header_text") Spacer() - Button( - action: { - router.showDiscoverySearch(searchQuery: "") - }, label: { - Image(systemName: "magnifyingglass") - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier(DashboardLocalization.search) - } - ) } .padding(.horizontal, 20) .padding(.top, 20) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index a53ae872f..54be41456 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -183,6 +183,9 @@ public struct PrimaryCourseDashboardView: View { await viewModel.getEnrollments() } } + .onAppear { + viewModel.updateNeeded = true + } .background( Theme.Colors.background .ignoresSafeArea() @@ -277,7 +280,7 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { - let showDropdown = viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured + let showDropdown = true //viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index d6825b086..7b3a51e37 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -17,6 +17,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { @Published public private(set) var fetchInProgress = true @Published var enrollments: PrimaryEnrollment? @Published var showError: Bool = false + @Published var updateNeeded: Bool = false var errorMessage: String? { didSet { withAnimation { @@ -49,7 +50,6 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) enrollmentPublisher - .merge(with: completionPublisher) .sink { [weak self] _ in guard let self = self else { return } Task { @@ -57,6 +57,21 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { } } .store(in: &cancellables) + + completionPublisher + .sink { [weak self] _ in + guard let self = self else { return } + updateEnrollmentsIfNeeded() + } + .store(in: &cancellables) + } + + private func updateEnrollmentsIfNeeded() { + guard updateNeeded else { return } + Task { + await getEnrollments() + updateNeeded = false + } } @MainActor From c46d6162523332d26d14c9cae23d6fa229ab730d Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 23 May 2024 13:22:54 +0300 Subject: [PATCH 13/22] fix: address feedback --- Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift | 3 +++ .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift index 19fde450a..85ba10548 100644 --- a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -84,5 +84,8 @@ struct DropDownMenu: View { } } .onTapBackground(enabled: expanded, { expanded = false }) + .onDisappear { + expanded = false + } } } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 54be41456..f357db932 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -280,7 +280,7 @@ public struct PrimaryCourseDashboardView: View { } private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { - let showDropdown = true //viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured + let showDropdown = viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured return ZStack(alignment: .top) { Theme.Colors.background .frame(height: showDropdown ? 70 : 50) From 8527f028f9c24e9486f9bdb142cf4e9c81a95e1f Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 24 May 2024 15:59:18 +0300 Subject: [PATCH 14/22] fix: address feedback --- OpenEdX/View/MainScreenView.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index e9e049245..79488abc5 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -55,6 +55,27 @@ struct MainScreenView: View { } .tag(MainTab.dashboard) .accessibilityIdentifier("dashboard_tabitem") + if viewModel.config.program.enabled { + ZStack { + if viewModel.config.program.type == .webview { + ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ) + } else if viewModel.config.program.type == .native { + Text(CoreLocalization.Mainscreen.inDeveloping) + } + + if updateAvailable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.programs.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.programs) + } + .tag(MainTab.programs) + } case .primaryCourse: ZStack { PrimaryCourseDashboardView( From be6dbf6c5d0ca3df1a79db8e7eb9e75ea1628953 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Mon, 27 May 2024 16:02:48 +0300 Subject: [PATCH 15/22] fix: address feedback --- OpenEdX/Data/DashboardPersistence.swift | 163 ++++++++++-------------- 1 file changed, 69 insertions(+), 94 deletions(-) diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index 9591d9a55..b7e0f062a 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -147,81 +147,59 @@ public class DashboardPersistence: DashboardPersistenceProtocol { // swiftlint:disable function_body_length public func savePrimaryEnrollment(enrollments: PrimaryEnrollment) { context.performAndWait { - let request: NSFetchRequest = CDMyEnrollments.fetchRequest() + // Deleting all old data before saving new ones + clearOldEnrollmentsData() - let existingEnrollment: CDMyEnrollments? - do { - existingEnrollment = try context.fetch(request).first - } catch { - existingEnrollment = nil - } - - let newEnrollment: CDMyEnrollments - if let existingEnrollment { - newEnrollment = existingEnrollment - } else { - newEnrollment = CDMyEnrollments(context: context) - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - } + let newEnrollment = CDMyEnrollments(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - // saving PrimaryCourse + // Saving new courses + newEnrollment.courses = NSSet(array: enrollments.courses.map { course in + let cdCourse = CDDashboardCourse(context: self.context) + cdCourse.name = course.name + cdCourse.org = course.org + cdCourse.desc = course.shortDescription + cdCourse.imageURL = course.imageURL + cdCourse.courseStart = course.courseStart + cdCourse.courseEnd = course.courseEnd + cdCourse.enrollmentStart = course.enrollmentStart + cdCourse.enrollmentEnd = course.enrollmentEnd + cdCourse.courseID = course.courseID + cdCourse.numPages = Int32(course.numPages) + cdCourse.hasAccess = course.hasAccess + cdCourse.progressEarned = Int32(course.progressEarned) + cdCourse.progressPossible = Int32(course.progressPossible) + return cdCourse + }) + + // Saving PrimaryCourse if let primaryCourse = enrollments.primaryCourse { - let cdPrimaryCourse: CDPrimaryCourse - if let existingPrimaryCourse = newEnrollment.primaryCourse { - cdPrimaryCourse = existingPrimaryCourse - } else { - cdPrimaryCourse = CDPrimaryCourse(context: context) - } + let cdPrimaryCourse = CDPrimaryCourse(context: context) - let futureAssignments = primaryCourse.futureAssignments - let uniqueFutureAssignments = Set(futureAssignments.map { assignment in - let assignmentRequest: NSFetchRequest = CDAssignment.fetchRequest() - assignmentRequest.predicate = NSPredicate(format: "title == %@", assignment.title) - let existingAssignment = try? self.context.fetch(assignmentRequest).first - - let cdAssignment: CDAssignment - if let existingAssignment { - cdAssignment = existingAssignment - } else { - cdAssignment = CDAssignment(context: self.context) - } - - cdAssignment.type = assignment.type - cdAssignment.title = assignment.title - cdAssignment.descript = assignment.description - cdAssignment.date = assignment.date - cdAssignment.complete = assignment.complete - cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId - return cdAssignment - }) + let futureAssignments = primaryCourse.futureAssignments.map { assignment in + let cdAssignment = CDAssignment(context: self.context) + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + } + cdPrimaryCourse.futureAssignments = NSSet(array: futureAssignments) - cdPrimaryCourse.futureAssignments = NSSet(array: Array(uniqueFutureAssignments)) - - let pastAssignments = primaryCourse.pastAssignments - let uniqueAssignments = Set(pastAssignments.map { assignment in - let assignmentRequest: NSFetchRequest = CDAssignment.fetchRequest() - assignmentRequest.predicate = NSPredicate(format: "title == %@", assignment.title) - let existingAssignment = try? self.context.fetch(assignmentRequest).first - - let cdAssignment: CDAssignment - if let existingAssignment { - cdAssignment = existingAssignment - } else { - cdAssignment = CDAssignment(context: self.context) - } - - cdAssignment.type = assignment.type - cdAssignment.title = assignment.title - cdAssignment.descript = assignment.description - cdAssignment.date = assignment.date - cdAssignment.complete = assignment.complete - cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId - return cdAssignment - }) - - cdPrimaryCourse.pastAssignments = NSSet(array: Array(uniqueAssignments)) + let pastAssignments = primaryCourse.pastAssignments.map { assignment in + let cdAssignment = CDAssignment(context: self.context) + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + } + cdPrimaryCourse.pastAssignments = NSSet(array: pastAssignments) - // saving PrimaryCourse cdPrimaryCourse.name = primaryCourse.name cdPrimaryCourse.org = primaryCourse.org cdPrimaryCourse.courseID = primaryCourse.courseID @@ -229,45 +207,42 @@ public class DashboardPersistence: DashboardPersistenceProtocol { cdPrimaryCourse.courseStart = primaryCourse.courseStart cdPrimaryCourse.courseEnd = primaryCourse.courseEnd cdPrimaryCourse.courseBanner = primaryCourse.courseBanner - cdPrimaryCourse.progressEarned = Int32(primaryCourse.progressEarned ?? 0) - cdPrimaryCourse.progressPossible = Int32(primaryCourse.progressPossible ?? 0) + cdPrimaryCourse.progressEarned = Int32(primaryCourse.progressEarned) + cdPrimaryCourse.progressPossible = Int32(primaryCourse.progressPossible) cdPrimaryCourse.lastVisitedBlockID = primaryCourse.lastVisitedBlockID cdPrimaryCourse.resumeTitle = primaryCourse.resumeTitle newEnrollment.primaryCourse = cdPrimaryCourse } - // saving courses - if let existingCourses = newEnrollment.courses { - for course in existingCourses { - context.delete(course as! CDDashboardCourse) - } - } - - newEnrollment.courses = NSSet(array: enrollments.courses.map { course in - let cdCourse = CDDashboardCourse(context: self.context) - cdCourse.name = course.name - cdCourse.org = course.org - cdCourse.desc = course.shortDescription - cdCourse.imageURL = course.imageURL - cdCourse.courseStart = course.courseStart - cdCourse.courseEnd = course.courseEnd - cdCourse.enrollmentStart = course.enrollmentStart - cdCourse.enrollmentEnd = course.enrollmentEnd - cdCourse.courseID = course.courseID - cdCourse.numPages = Int32(course.numPages) - return cdCourse - }) - newEnrollment.totalPages = Int32(enrollments.totalPages) newEnrollment.count = Int32(enrollments.count) do { try context.save() } catch { - print("Ошибка при сохранении MyEnrollments:", error) + print("Error when saving MyEnrollments:", error) } } } // swiftlint:enable function_body_length + + func clearOldEnrollmentsData() { + let fetchRequest1: NSFetchRequest = CDDashboardCourse.fetchRequest() + let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1) + + let fetchRequest2: NSFetchRequest = CDPrimaryCourse.fetchRequest() + let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) + + let fetchRequest3: NSFetchRequest = CDMyEnrollments.fetchRequest() + let batchDeleteRequest3 = NSBatchDeleteRequest(fetchRequest: fetchRequest3) + + do { + try context.execute(batchDeleteRequest1) + try context.execute(batchDeleteRequest2) + try context.execute(batchDeleteRequest3) + } catch { + print("Error when deleting old data:", error) + } + } } From b225629b5b4514aa02ab03d0bf8f2f54b001387c Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Wed, 29 May 2024 15:06:11 +0300 Subject: [PATCH 16/22] fix: address feedback --- .../Presentation/PrimaryCourseDashboardView.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index f357db932..a7dcbd083 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -45,6 +45,8 @@ public struct PrimaryCourseDashboardView: View { // MARK: - Page body VStack(alignment: .leading) { Spacer(minLength: 50) + switch selectedMenu { + case .courses: RefreshableScrollViewCompat(action: { await viewModel.getEnrollments(showProgress: false) }) { @@ -57,8 +59,6 @@ public struct PrimaryCourseDashboardView: View { maxHeight: .infinity) } else { LazyVStack(spacing: 0) { - switch selectedMenu { - case .courses: if let enrollments = viewModel.enrollments { if let primary = enrollments.primaryCourse { PrimaryCardView( @@ -141,15 +141,15 @@ public struct PrimaryCourseDashboardView: View { } Spacer(minLength: 100) } - case .programs: - programView - .padding(.top, 50) - } } } } .frameLimit(width: proxy.size.width) }.accessibilityAction {} + case .programs: + programView + .padding(.top, 50) + } }.padding(.top, 8) // MARK: - Offline mode SnackBar OfflineSnackBarView( From c37f9fbe4a90a2747bc44a3f3b69464ffc583309 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 11:33:53 +0300 Subject: [PATCH 17/22] fix: address feedback --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index a7dcbd083..7e8e79f10 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -148,7 +148,6 @@ public struct PrimaryCourseDashboardView: View { }.accessibilityAction {} case .programs: programView - .padding(.top, 50) } }.padding(.top, 8) // MARK: - Offline mode SnackBar From b7fab1b9b27b70fa20a5ef23b94dccc11869001b Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 16:01:10 +0300 Subject: [PATCH 18/22] fix: address feedback --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 7e8e79f10..ab0a5a670 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -35,7 +35,9 @@ public struct PrimaryCourseDashboardView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - if viewModel.enrollments?.primaryCourse == nil && !viewModel.fetchInProgress { + if viewModel.enrollments?.primaryCourse == nil + && !viewModel.fetchInProgress + && selectedMenu == .courses { NoCoursesView(openDiscovery: { openDiscoveryPage() }).zIndex(1) From 865bd610ce2b48ecb57cabb6be6abae0f5033221 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 16:01:53 +0300 Subject: [PATCH 19/22] fix: address feedback --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index ab0a5a670..10e1fc076 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -35,8 +35,8 @@ public struct PrimaryCourseDashboardView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - if viewModel.enrollments?.primaryCourse == nil - && !viewModel.fetchInProgress + if viewModel.enrollments?.primaryCourse == nil + && !viewModel.fetchInProgress && selectedMenu == .courses { NoCoursesView(openDiscovery: { openDiscoveryPage() From 5a1266605058cd160bea645d3458f954dcd280c4 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 16:09:20 +0300 Subject: [PATCH 20/22] fix: address feedback --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 10e1fc076..7ac041110 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -329,7 +329,7 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { let vm = PrimaryCourseDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), - analytics: DashboardAnalyticsMock(), + analytics: DashboardAnalyticsMock(), config: ConfigMock() ) From dab54f99124180867ba4fb82e9fe6a33cbaae0d8 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 19:41:50 +0300 Subject: [PATCH 21/22] fix: address feedback --- Core/Core/Configuration/Config/DiscoveryConfig.swift | 4 ++-- OpenEdX/Router.swift | 2 +- OpenEdX/View/MainScreenView.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift index d6d29de2b..6d3b8615b 100644 --- a/Core/Core/Configuration/Config/DiscoveryConfig.swift +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -8,7 +8,7 @@ import Foundation public enum DiscoveryConfigType: String { - case native + case gallery case webview case none } @@ -45,7 +45,7 @@ public class DiscoveryConfig: NSObject { init(dictionary: [String: AnyObject]) { type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { DiscoveryConfigType(rawValue: $0) - } ?? .native + } ?? .gallery webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:]) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 4197d9967..e196b4b91 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -281,7 +281,7 @@ public class Router: AuthorizationRouter, public func showDiscoveryScreen(searchQuery: String? = nil, sourceScreen: LogistrationSourceScreen) { let config = Container.shared.resolve(ConfigProtocol.self) - if config?.discovery.type == .native { + if config?.discovery.type == .gallery { let view = DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index 79488abc5..8e6ac3ef2 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -62,7 +62,7 @@ struct MainScreenView: View { viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)! ) - } else if viewModel.config.program.type == .native { + } else if viewModel.config.program.type == .gallery { Text(CoreLocalization.Mainscreen.inDeveloping) } @@ -101,7 +101,7 @@ struct MainScreenView: View { if viewModel.config.discovery.enabled { ZStack { - if viewModel.config.discovery.type == .native { + if viewModel.config.discovery.type == .gallery { DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, From d2f865fc9e0afc803f0cc250304647151d1cd2c2 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 30 May 2024 19:47:36 +0300 Subject: [PATCH 22/22] fix: address feedback --- Core/Core/Configuration/Config/DashboardConfig.swift | 4 ++-- Core/Core/Configuration/Config/DiscoveryConfig.swift | 4 ++-- OpenEdX/Router.swift | 2 +- OpenEdX/View/MainScreenView.swift | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Core/Core/Configuration/Config/DashboardConfig.swift b/Core/Core/Configuration/Config/DashboardConfig.swift index 6ff21b31d..cd2b335b1 100644 --- a/Core/Core/Configuration/Config/DashboardConfig.swift +++ b/Core/Core/Configuration/Config/DashboardConfig.swift @@ -8,7 +8,7 @@ import Foundation public enum DashboardConfigType: String { - case primaryCourse = "primary_course" + case gallery case list } @@ -22,7 +22,7 @@ public class DashboardConfig: NSObject { init(dictionary: [String: AnyObject]) { type = (dictionary[DashboardKeys.dashboardType] as? String).flatMap { DashboardConfigType(rawValue: $0) - } ?? .primaryCourse + } ?? .gallery } } diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift index 6d3b8615b..d6d29de2b 100644 --- a/Core/Core/Configuration/Config/DiscoveryConfig.swift +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -8,7 +8,7 @@ import Foundation public enum DiscoveryConfigType: String { - case gallery + case native case webview case none } @@ -45,7 +45,7 @@ public class DiscoveryConfig: NSObject { init(dictionary: [String: AnyObject]) { type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { DiscoveryConfigType(rawValue: $0) - } ?? .gallery + } ?? .native webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:]) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index e196b4b91..4197d9967 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -281,7 +281,7 @@ public class Router: AuthorizationRouter, public func showDiscoveryScreen(searchQuery: String? = nil, sourceScreen: LogistrationSourceScreen) { let config = Container.shared.resolve(ConfigProtocol.self) - if config?.discovery.type == .gallery { + if config?.discovery.type == .native { let view = DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index 8e6ac3ef2..7e3e30d60 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -62,7 +62,7 @@ struct MainScreenView: View { viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)! ) - } else if viewModel.config.program.type == .gallery { + } else if viewModel.config.program.type == .native { Text(CoreLocalization.Mainscreen.inDeveloping) } @@ -76,7 +76,7 @@ struct MainScreenView: View { } .tag(MainTab.programs) } - case .primaryCourse: + case .gallery: ZStack { PrimaryCourseDashboardView( viewModel: Container.shared.resolve(PrimaryCourseDashboardViewModel.self)!, @@ -101,7 +101,7 @@ struct MainScreenView: View { if viewModel.config.discovery.enabled { ZStack { - if viewModel.config.discovery.type == .gallery { + if viewModel.config.discovery.type == .native { DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!,