diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index d0543bc53..f718ae857 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 020306CC2932C0C4000949EA /* PickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020306CB2932C0C4000949EA /* PickerView.swift */; }; 02066B482906F73400F4307E /* PickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02066B472906F73400F4307E /* PickerMenu.swift */; }; 020C31C9290AC3F700D6DEA2 /* PickerFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C31C8290AC3F700D6DEA2 /* PickerFields.swift */; }; + 020D72F42BB76DFE00773319 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D72F32BB76DFE00773319 /* VisualEffectView.swift */; }; 021D924828DC860C00ACC565 /* Data_UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924728DC860C00ACC565 /* Data_UserProfile.swift */; }; 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; @@ -34,6 +35,7 @@ 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024D723429C8BB1A006D36ED /* NavigationBar.swift */; }; 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024FCCFF28EF1CD300232339 /* WebBrowser.swift */; }; 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */; }; + 0254D1912BCD699F000CDE89 /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0254D1902BCD699F000CDE89 /* RefreshProgressView.swift */; }; 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */; }; 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104929C4A5B6004B5A55 /* UserSettings.swift */; }; 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025B36742A13B7D5001A640E /* UnitButtonView.swift */; }; @@ -76,6 +78,7 @@ 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 */; }; + 02E224DB2BB76B3E00EF1ADB /* DynamicOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E224DA2BB76B3E00EF1ADB /* DynamicOffsetView.swift */; }; 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E225AF291D29EB0067769A /* UrlExtension.swift */; }; 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */; }; 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F164362902A9EB0090DDEF /* StringExtension.swift */; }; @@ -190,6 +193,7 @@ 020306CB2932C0C4000949EA /* PickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerView.swift; sourceTree = ""; }; 02066B472906F73400F4307E /* PickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerMenu.swift; sourceTree = ""; }; 020C31C8290AC3F700D6DEA2 /* PickerFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerFields.swift; sourceTree = ""; }; + 020D72F32BB76DFE00773319 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; 021D924728DC860C00ACC565 /* Data_UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_UserProfile.swift; sourceTree = ""; }; 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; @@ -214,6 +218,7 @@ 024D723429C8BB1A006D36ED /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; 024FCCFF28EF1CD300232339 /* WebBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowser.swift; sourceTree = ""; }; 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandlerProtocol.swift; sourceTree = ""; }; + 0254D1902BCD699F000CDE89 /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadBodyEncoding.swift; sourceTree = ""; }; 0259104929C4A5B6004B5A55 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; 025B36742A13B7D5001A640E /* UnitButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitButtonView.swift; sourceTree = ""; }; @@ -255,6 +260,7 @@ 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 = ""; }; + 02E224DA2BB76B3E00EF1ADB /* DynamicOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicOffsetView.swift; sourceTree = ""; }; 02E225AF291D29EB0067769A /* UrlExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlExtension.swift; sourceTree = ""; }; 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewViewModel.swift; sourceTree = ""; }; 02ED50CB29A64B84008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; @@ -729,6 +735,9 @@ BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, 02E93F862AEBAED4006C4750 /* AppReview */, BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */, + 02E224DA2BB76B3E00EF1ADB /* DynamicOffsetView.swift */, + 0254D1902BCD699F000CDE89 /* RefreshProgressView.swift */, + 020D72F32BB76DFE00773319 /* VisualEffectView.swift */, 06DEA4A22BBD66A700110D20 /* BackNavigationButton.swift */, 06DEA4A42BBD66D700110D20 /* BackNavigationButtonViewModel.swift */, ); @@ -1137,6 +1146,7 @@ 0727877F28D25B24002E9142 /* Alamofire+Error.swift in Sources */, 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */, 064987952B4D69FF0071642A /* SurveyCssInjection.swift in Sources */, + 02E224DB2BB76B3E00EF1ADB /* DynamicOffsetView.swift in Sources */, 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */, 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */, 071009D028D1E3A600344290 /* Constants.swift in Sources */, @@ -1194,9 +1204,11 @@ 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */, 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */, 07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */, + 020D72F42BB76DFE00773319 /* VisualEffectView.swift in Sources */, 0727878928D31734002E9142 /* User.swift in Sources */, A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */, 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */, + 0254D1912BCD699F000CDE89 /* RefreshProgressView.swift in Sources */, 02066B482906F73400F4307E /* PickerMenu.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Frame.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Frame.svg deleted file mode 100644 index c3cd5daca..000000000 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Frame.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Frame-2.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Frame-2.svg deleted file mode 100644 index 24bcbb8bf..000000000 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Frame-2.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/Contents.json similarity index 54% rename from Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Contents.json rename to Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/Contents.json index 38543073b..10d2b3951 100644 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/bubble.left.circle.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Frame-2.svg", + "filename" : "dates.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/dates.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/dates.svg new file mode 100644 index 000000000..07b637430 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/dates.imageset/dates.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/Contents.json new file mode 100644 index 000000000..84c60bd48 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "discussions.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/discussions.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/discussions.svg new file mode 100644 index 000000000..c84b5fbbf --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/discussions.imageset/discussions.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Frame-4.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Frame-4.svg deleted file mode 100644 index bf501bf7e..000000000 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Frame-4.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/Contents.json new file mode 100644 index 000000000..bfe77dc87 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "downloads.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/downloads.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/downloads.svg new file mode 100644 index 000000000..1f933d639 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/downloads.imageset/downloads.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/Contents.json similarity index 55% rename from Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Contents.json rename to Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/Contents.json index 06f078f72..0a2791382 100644 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/book.circle.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Frame.svg", + "filename" : "home.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/home.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/home.svg new file mode 100644 index 000000000..29541dcd3 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/home.imageset/home.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/Contents.json similarity index 55% rename from Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Contents.json rename to Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/Contents.json index afc817643..38459c2c4 100644 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/doc.circle.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Frame-4.svg", + "filename" : "more.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/more.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/more.svg new file mode 100644 index 000000000..fe575b37d --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/more.imageset/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Frame-3.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Frame-3.svg deleted file mode 100644 index 87eabe7ac..000000000 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Frame-3.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/Contents.json similarity index 54% rename from Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Contents.json rename to Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/Contents.json index 47094a079..a795237ff 100644 --- a/Core/Core/Assets.xcassets/CourseNavigationBar/video.circle.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Frame-3.svg", + "filename" : "videos.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/videos.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/videos.svg new file mode 100644 index 000000000..0109810ba --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/videos.imageset/videos.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Configuration/Config/UIComponentsConfig.swift b/Core/Core/Configuration/Config/UIComponentsConfig.swift index 03a1ca4bb..cb5ce3e68 100644 --- a/Core/Core/Configuration/Config/UIComponentsConfig.swift +++ b/Core/Core/Configuration/Config/UIComponentsConfig.swift @@ -9,22 +9,16 @@ import Foundation private enum Keys: String, RawStringExtractable { case courseNestedListEnabled = "COURSE_NESTED_LIST_ENABLED" - case courseTopTabBarEnabled = "COURSE_TOP_TAB_BAR_ENABLED" - case courseBannerEnabled = "COURSE_BANNER_ENABLED" case courseUnitProgressEnabled = "COURSE_UNIT_PROGRESS_ENABLED" } public class UIComponentsConfig: NSObject { public var courseNestedListEnabled: Bool - public var courseBannerEnabled: Bool public var courseUnitProgressEnabled: Bool - public var courseTopTabBarEnabled: Bool init(dictionary: [String: Any]) { courseNestedListEnabled = dictionary[Keys.courseNestedListEnabled] as? Bool ?? false - courseBannerEnabled = dictionary[Keys.courseBannerEnabled] as? Bool ?? false courseUnitProgressEnabled = dictionary[Keys.courseUnitProgressEnabled] as? Bool ?? false - courseTopTabBarEnabled = dictionary[Keys.courseTopTabBarEnabled] as? Bool ?? false super.init() } } diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index cc5c48732..406bea3ed 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -22,6 +22,7 @@ public struct CourseStructure: Equatable { public var childs: [CourseChapter] public let media: DataLayer.CourseMedia //FIXME Domain model public let certificate: Certificate? + public let org: String public let isSelfPaced: Bool public init( @@ -35,6 +36,7 @@ public struct CourseStructure: Equatable { childs: [CourseChapter], media: DataLayer.CourseMedia, certificate: Certificate?, + org: String, isSelfPaced: Bool ) { self.id = id @@ -47,6 +49,7 @@ public struct CourseStructure: Equatable { self.childs = childs self.media = media self.certificate = certificate + self.org = org self.isSelfPaced = isSelfPaced } diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index 17b1fad70..e25f452d0 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -35,10 +35,12 @@ public enum CoreAssets { public static let lockWithWatchIcon = ImageAsset(name: "lock_with_watch_icon") public static let schoolCapIcon = ImageAsset(name: "school_cap_icon") public static let syncToCalendar = ImageAsset(name: "sync_to_calendar") - public static let bookCircle = ImageAsset(name: "book.circle") - public static let bubbleLeftCircle = ImageAsset(name: "bubble.left.circle") - public static let docCircle = ImageAsset(name: "doc.circle") - public static let videoCircle = ImageAsset(name: "video.circle") + public static let dates = ImageAsset(name: "dates") + public static let discussions = ImageAsset(name: "discussions") + public static let downloads = ImageAsset(name: "downloads") + public static let home = ImageAsset(name: "home") + public static let more = ImageAsset(name: "more") + public static let videos = ImageAsset(name: "videos") public static let dashboardEmptyPage = ImageAsset(name: "DashboardEmptyPage") public static let addComment = ImageAsset(name: "addComment") public static let allPosts = ImageAsset(name: "allPosts") diff --git a/Core/Core/View/Base/DynamicOffsetView.swift b/Core/Core/View/Base/DynamicOffsetView.swift new file mode 100644 index 000000000..59bedd83d --- /dev/null +++ b/Core/Core/View/Base/DynamicOffsetView.swift @@ -0,0 +1,77 @@ +// +// ResponsiveView.swift +// Core +// +// Created by Stepanok Ivan on 26.03.2024. +// + +import SwiftUI + +public struct DynamicOffsetView: View { + + private let padHeight: CGFloat = 290 + private let collapsedHorizontalHeight: CGFloat = 120 + private let collapsedVerticalHeight: CGFloat = 100 + private let expandedHeight: CGFloat = 240 + private let coordinateBoundaryLower: CGFloat = -115 + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool + @State private var collapseHeight: CGFloat = .zero + + @Environment(\.isHorizontal) private var isHorizontal + + public init( + coordinate: Binding, + collapsed: Binding + ) { + self._coordinate = coordinate + self._collapsed = collapsed + } + + public var body: some View { + VStack { + } + .frame(height: collapseHeight) + .overlay( + GeometryReader { geometry -> Color in + guard idiom != .pad else { + return .clear + } + guard !isHorizontal else { + coordinate = coordinateBoundaryLower + return .clear + } + DispatchQueue.main.async { + coordinate = geometry.frame(in: .global).minY + } + return .clear + } + ) + .onAppear { + changeCollapsedHeight() + } + .onChange(of: collapsed) { collapsed in + if !collapsed { + changeCollapsedHeight() + } + } + .onChange(of: isHorizontal) { isHorizontal in + if isHorizontal { + collapsed = true + } + changeCollapsedHeight() + } + } + + private func changeCollapsedHeight() { + collapseHeight = idiom == .pad + ? padHeight + : ( + collapsed + ? (isHorizontal ? collapsedHorizontalHeight : collapsedVerticalHeight) + : expandedHeight + ) + } +} diff --git a/Core/Core/View/Base/RefreshProgressView.swift b/Core/Core/View/Base/RefreshProgressView.swift new file mode 100644 index 000000000..25ed1c199 --- /dev/null +++ b/Core/Core/View/Base/RefreshProgressView.swift @@ -0,0 +1,23 @@ +// +// RefreshProgressView.swift +// Course +// +// Created by  Stepanok Ivan on 15.04.2024. +// + +import SwiftUI + +public struct RefreshProgressView: View { + + @Binding private var isShowRefresh: Bool + + public init(isShowRefresh: Binding) { + self._isShowRefresh = isShowRefresh + } + + public var body: some View { + ProgressView() + .padding(.top, isShowRefresh ? 20 : -60) + .padding(.bottom, isShowRefresh ? 20 : 0) + } +} diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift index 9e79a9dea..5f09777a7 100644 --- a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift +++ b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift @@ -9,36 +9,33 @@ import SwiftUI import Theme public struct ScrollSlidingTabBar: View { - + @Binding private var selection: Int @State private var buttonFrames: [Int: CGRect] = [:] private let containerWidth: CGFloat - private let tabs: [String] + private let tabs: [(String, Image)] private let style: Style private let onTap: ((Int) -> Void)? - + private var containerSpace: String { return "container" } public init( selection: Binding, - tabs: [String], + tabs: [(String, Image)], style: Style = .default, containerWidth: CGFloat, onTap: ((Int) -> Void)? = nil) { - self._selection = selection - self.tabs = tabs - self.style = style - self.onTap = onTap - self.containerWidth = containerWidth - } + self._selection = selection + self.tabs = tabs + self.style = style + self.onTap = onTap + self.containerWidth = containerWidth + } public var body: some View { ZStack(alignment: .bottomLeading) { - Rectangle() - .fill(style.borderColor) - .frame(height: style.borderHeight, alignment: .leading) ScrollViewReader { proxy in ScrollView(.horizontal, showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { @@ -50,6 +47,8 @@ public struct ScrollSlidingTabBar: View { } .coordinateSpace(name: containerSpace) } + .onTapGesture {} + // Fix button tapable area bug – https://forums.developer.apple.com/forums/thread/745059 .onChange(of: selection) { newValue in withAnimation { proxy.scrollTo(newValue, anchor: .center) @@ -66,20 +65,53 @@ extension ScrollSlidingTabBar { private func buttons() -> some View { HStack(spacing: 0) { ForEach(Array(tabs.enumerated()), id: \.offset) { obj in - Button { - selection = obj.offset - onTap?(obj.offset) - } label: { - HStack { - Text(obj.element) - .font(isSelected(index: obj.offset) ? style.selectedFont : style.font) + Button( + action: { + selection = obj.offset + onTap?(obj.offset) + }, + label: { + ZStack { + RoundedRectangle(cornerRadius: 20) + .foregroundStyle( + isSelected(index: obj.offset) + ? style.activeAccentColor + : style.inactiveAccentColor + ) + .onTapGesture { + selection = obj.offset + onTap?(obj.offset) + } + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke( + isSelected(index: obj.offset) + ? .clear + : style.borderColor, + lineWidth: style.borderHeight + ) + ) + HStack { + obj.element.1.renderingMode(.template) + .padding(.leading, 12) + Text(obj.element.0) + .padding(.trailing, 12) + .font(isSelected(index: obj.offset) ? style.selectedFont : style.font) + } + .accentColor( + isSelected(index: obj.offset) + ? Theme.Colors.white + : Theme.Colors.slidingTextColor + ) + } + .frame( height: 40) + .fixedSize(horizontal: true, vertical: true) } - .padding(.horizontal, style.buttonHInset) - .padding(.vertical, style.buttonVInset) - } - .accentColor( - isSelected(index: obj.offset) ? style.activeAccentColor : style.inactiveAccentColor ) + .padding(.leading, obj.offset == 0 ? style.buttonLeadingPadding : 0) + .padding(.trailing, obj.offset == tabs.count - 1 ? style.buttonTrailingPadding : 0) + .padding(.horizontal, style.buttonHInset) + .padding(.vertical, style.buttonVInset) .readFrame(in: .named(containerSpace)) { buttonFrames[obj.offset] = $0 } @@ -142,6 +174,9 @@ extension ScrollSlidingTabBar { public let buttonHInset: CGFloat public let buttonVInset: CGFloat + public let buttonLeadingPadding: CGFloat + public let buttonTrailingPadding: CGFloat + public init( font: Font, selectedFont: Font, @@ -151,7 +186,9 @@ extension ScrollSlidingTabBar { borderColor: Color, borderHeight: CGFloat, buttonHInset: CGFloat, - buttonVInset: CGFloat + buttonVInset: CGFloat, + buttonLeadingPadding: CGFloat, + buttonTrailingPadding: CGFloat ) { self.font = font self.selectedFont = selectedFont @@ -162,19 +199,24 @@ extension ScrollSlidingTabBar { self.borderHeight = borderHeight self.buttonHInset = buttonHInset self.buttonVInset = buttonVInset + self.buttonLeadingPadding = buttonLeadingPadding + self.buttonTrailingPadding = buttonTrailingPadding } public static let `default` = Style( - font: Theme.Fonts.bodyLarge, - selectedFont: Theme.Fonts.titleMedium, + font: Theme.Fonts.titleSmall, + selectedFont: Theme.Fonts.titleSmall, activeAccentColor: Theme.Colors.accentXColor, - inactiveAccentColor: Theme.Colors.textSecondary, - indicatorHeight: 2, - borderColor: .gray.opacity(0.2), + inactiveAccentColor: Theme.Colors.background, + indicatorHeight: 0, + borderColor: Theme.Colors.slidingStrokeColor, borderHeight: 1, - buttonHInset: 16, - buttonVInset: 10 + buttonHInset: 4, + buttonVInset: 2, + buttonLeadingPadding: 8, + buttonTrailingPadding: 8 ) + } } @@ -187,7 +229,14 @@ private struct SlidingTabConsumerView: View { VStack(alignment: .leading) { ScrollSlidingTabBar( selection: $selection, - tabs: ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"], + tabs: [ + ("First", Image(systemName: "1.circle")), + ("Second", Image(systemName: "2.circle")), + ("Third", Image(systemName: "3.circle")), + ("Fourth", Image(systemName: "4.circle")), + ("Fifth", Image(systemName: "5.circle")), + ("Sixth", Image(systemName: "6.circle")) + ], containerWidth: 300 ) TabView(selection: $selection) { diff --git a/Core/Core/View/Base/VisualEffectView.swift b/Core/Core/View/Base/VisualEffectView.swift new file mode 100644 index 000000000..94dcaeae6 --- /dev/null +++ b/Core/Core/View/Base/VisualEffectView.swift @@ -0,0 +1,21 @@ +// +// VisualEffectView.swift +// Core +// +// Created by  Stepanok Ivan on 29.03.2024. +// + +import SwiftUI + +public struct VisualEffectView: UIViewRepresentable { + private var effect: UIVisualEffect? + + public init(effect: UIVisualEffect? = nil) { + self.effect = effect + } + + public func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } + public func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { + uiView.effect = effect + } +} diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 3ab8dfe2b..a118f173b 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDC29252E900051930C /* CourseRouter.swift */; }; 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */; }; 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02F98A8028F8224200DE94C0 /* Discussion.framework */; }; + 02FCB2B32BBEB36600373180 /* CourseHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */; }; 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */; }; 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060E8BC92B5FD68C0080C952 /* UnitStack.swift */; }; 065275352BB1B39C0093BCCA /* PlayerViewControllerHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */; }; @@ -151,6 +152,7 @@ 02F3BFDC29252E900051930C /* CourseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRouter.swift; sourceTree = ""; }; 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerViewModelTests.swift; path = CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02F98A8028F8224200DE94C0 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseHeaderView.swift; sourceTree = ""; }; 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 060E8BC92B5FD68C0080C952 /* UnitStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStack.swift; sourceTree = ""; }; 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewControllerHolder.swift; sourceTree = ""; }; @@ -583,6 +585,7 @@ 02D4FC2C2BBD7C7500C47748 /* MessageSectionView */, BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, + 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */, ); path = Subviews; sourceTree = ""; @@ -901,6 +904,7 @@ 97C99C362B9A08FE004EEDE2 /* CalendarSyncProgressView.swift in Sources */, BAC0E0D82B32EF03006B68A9 /* DownloadsView.swift in Sources */, 97CA95252B875EE200A9EDEA /* DatesSuccessView.swift in Sources */, + 02FCB2B32BBEB36600373180 /* CourseHeaderView.swift in Sources */, DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */, 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 7e4c4cf43..19073d2de 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -138,6 +138,7 @@ public class CourseRepository: CourseRepositoryProtocol { childs: childs, media: course.media, certificate: course.certificate?.domain, + org: course.org ?? "", isSelfPaced: course.isSelfPaced ) } @@ -348,6 +349,7 @@ And there are various ways of describing it-- call it oral poetry or childs: childs, media: course.media, certificate: course.certificate?.domain, + org: course.org ?? "", isSelfPaced: course.isSelfPaced ) } diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index d07c4036e..f2a060e13 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -19,6 +19,7 @@ public extension DataLayer { public let id: String public let media: DataLayer.CourseMedia public let certificate: Certificate? + public let org: String? public let isSelfPaced: Bool enum CodingKeys: String, CodingKey { @@ -27,6 +28,7 @@ public extension DataLayer { case id case media case certificate + case org case isSelfPaced = "is_self_paced" } @@ -36,6 +38,7 @@ public extension DataLayer { id: String, media: DataLayer.CourseMedia, certificate: Certificate?, + org: String?, isSelfPaced: Bool ) { self.rootItem = rootItem @@ -43,6 +46,7 @@ public extension DataLayer { self.id = id self.media = media self.certificate = certificate + self.org = org self.isSelfPaced = isSelfPaced } @@ -54,6 +58,7 @@ public extension DataLayer { id = try values.decode(String.self, forKey: .id) media = try values.decode(DataLayer.CourseMedia.self, forKey: .media) certificate = try values.decode(Certificate.self, forKey: .certificate) + org = try values.decode(String.self, forKey: .org) isSelfPaced = try values.decode(Bool.self, forKey: .isSelfPaced) } } diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents index cd0583f08..dda82f3ae 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 @@ - + @@ -83,6 +83,7 @@ + @@ -100,4 +101,4 @@ - \ No newline at end of file + diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index f30ed47b4..1b01e7a59 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -54,6 +54,7 @@ public class CourseInteractor: CourseInteractorProtocol { childs: newChilds, media: course.media, certificate: course.certificate, + org: course.org, isSelfPaced: course.isSelfPaced ) } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index d08bdbed4..322b37564 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -2,7 +2,7 @@ // CourseScreensView.swift // Course // -// Created by  Stepanok Ivan on 10.10.2022. +// Created by Stepanok Ivan on 10.10.2022. // import SwiftUI @@ -18,6 +18,24 @@ public struct CourseContainerView: View { @State private var isAnimatingForTap: Bool = false public var courseID: String private var title: String + @State private var ignoreOffset: Bool = false + @State private var coordinate: CGFloat = .zero + @State private var lastCoordinate: CGFloat = .zero + @State private var collapsed: Bool = false + @Environment(\.isHorizontal) private var isHorizontal + @Namespace private var animationNamespace + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + private let coordinateBoundaryLower: CGFloat = -115 + private let coordinateBoundaryHigher: CGFloat = 40 + + private struct GeometryName { + static let backButton = "backButton" + static let topTabBar = "topTabBar" + static let blurSecondaryBg = "blurSecondaryBg" + static let blurPrimaryBg = "blurPrimaryBg" + static let blurBg = "blurBg" + } public init( viewModel: CourseContainerViewModel, @@ -43,13 +61,14 @@ public struct CourseContainerView: View { ZStack(alignment: .top) { content } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) .navigationTitle(title) .onChange(of: viewModel.selection, perform: didSelect) + .onChange(of: coordinate, perform: collapseHeader) .background(Theme.Colors.background) } - + @ViewBuilder private var content: some View { if let courseStart = viewModel.courseStart { @@ -60,35 +79,68 @@ public struct CourseContainerView: View { courseID: courseID, isVideo: false, selection: $viewModel.selection, + coordinate: $coordinate, + collapsed: $collapsed, dateTabIndex: CourseTab.dates.rawValue ) } else { - GeometryReader { proxy in - VStack(spacing: 0) { - if viewModel.config.uiComponents.courseTopTabBarEnabled { - topTabBar(containerWidth: proxy.size.width) + ZStack(alignment: .top) { + tabs + GeometryReader { proxy in + VStack(spacing: 0) { + CourseHeaderView( + viewModel: viewModel, + title: title, + collapsed: $collapsed, + containerWidth: proxy.size.width, + animationNamespace: animationNamespace, + isAnimatingForTap: $isAnimatingForTap + ) } - tabs + .offset( + y: ignoreOffset + ? (collapsed ? coordinateBoundaryLower : .zero) + : ((coordinateBoundaryLower...coordinateBoundaryHigher).contains(coordinate) + ? coordinate + : (collapsed ? coordinateBoundaryLower : .zero)) + ) + backButton(containerWidth: proxy.size.width) + } + }.ignoresSafeArea(edges: idiom == .pad ? .leading : .top) + .onAppear { + self.collapsed = isHorizontal } - } } } } - - private func topTabBar(containerWidth: CGFloat) -> some View { - ScrollSlidingTabBar( - selection: $viewModel.selection, - tabs: CourseTab.allCases.map { $0.title }, - containerWidth: containerWidth - ) { newValue in - isAnimatingForTap = true - viewModel.selection = newValue - DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(300))) { - isAnimatingForTap = false + + private func backButton(containerWidth: CGFloat) -> some View { + ZStack(alignment: .topLeading) { + if !collapsed { + HStack { + ZStack(alignment: .bottom) { + VisualEffectView(effect: UIBlurEffect(style: .regular)) + .clipShape(Circle()) + BackNavigationButton( + color: Theme.Colors.textPrimary, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .matchedGeometryEffect(id: GeometryName.backButton, in: animationNamespace) + .offset(y: 7) + } + .frame(width: 30, height: 30) + .padding(.vertical, 8) + .padding(.leading, 12) + .padding(.top, idiom == .pad ? 0 : 55) + Spacer() + } } } } - + private var tabs: some View { TabView(selection: $viewModel.selection) { ForEach(CourseTab.allCases) { tab in @@ -100,6 +152,8 @@ public struct CourseContainerView: View { courseID: courseID, isVideo: false, selection: $viewModel.selection, + coordinate: $coordinate, + collapsed: $collapsed, dateTabIndex: CourseTab.dates.rawValue ) .tabItem { @@ -115,6 +169,8 @@ public struct CourseContainerView: View { courseID: courseID, isVideo: true, selection: $viewModel.selection, + coordinate: $coordinate, + collapsed: $collapsed, dateTabIndex: CourseTab.dates.rawValue ) .tabItem { @@ -126,6 +182,8 @@ public struct CourseContainerView: View { case .dates: CourseDatesView( courseID: courseID, + coordinate: $coordinate, + collapsed: $collapsed, viewModel: Container.shared.resolve(CourseDatesViewModel.self, arguments: courseID, title)! ) @@ -138,6 +196,8 @@ public struct CourseContainerView: View { case .discussion: DiscussionTopicsView( courseID: courseID, + coordinate: $coordinate, + collapsed: $collapsed, viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, argument: title)!, router: Container.shared.resolve(DiscussionRouter.self)! @@ -151,6 +211,8 @@ public struct CourseContainerView: View { case .handounds: HandoutsView( courseID: courseID, + coordinate: $coordinate, + collapsed: $collapsed, viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)! ) .tabItem { @@ -162,19 +224,20 @@ public struct CourseContainerView: View { } } } - .if(viewModel.config.uiComponents.courseTopTabBarEnabled) { view in - view - .tabViewStyle(.page(indexDisplayMode: .never)) - .animation(.default, value: viewModel.selection) - } + .tabViewStyle(.page(indexDisplayMode: .never)) + .introspect(.scrollView, on: .iOS(.v15, .v16, .v17), customize: { tabView in + tabView.isScrollEnabled = false + }) .onFirstAppear { Task { await viewModel.tryToRefreshCookies() } } } - + private func didSelect(_ selection: Int) { + lastCoordinate = .zero + ignoreOffset = true CourseTab(rawValue: selection).flatMap { viewModel.trackSelectedTab( selection: $0, @@ -183,6 +246,51 @@ public struct CourseContainerView: View { ) } } + + private func collapseHeader(_ coordinate: CGFloat) { + guard !isHorizontal else { return collapsed = true } + let lowerBound: CGFloat = -90 + let upperBound: CGFloat = 160 + + switch coordinate { + case lowerBound...upperBound: + if shouldAnimateHeader(coordinate: coordinate) { + withAnimation(.spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0.6)) { + ignoreOffset = false + collapsed = false + } + } else { + lastCoordinate = coordinate + } + default: + if shouldAnimateHeader(coordinate: coordinate) { + withAnimation(.spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0.6)) { + ignoreOffset = false + collapsed = true + } + } else { + lastCoordinate = coordinate + } + } + } + + private func shouldAnimateHeader(coordinate: CGFloat) -> Bool { + let ignoringOffset: CGFloat = 120 + + guard coordinate <= ignoringOffset, lastCoordinate != 0 else { + return false + } + + if collapsed && lastCoordinate > coordinate { + return false + } + + if !collapsed && lastCoordinate < coordinate { + return false + } + + return true + } } #if DEBUG diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index c3259ed8c..82572b60e 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -24,7 +24,7 @@ public enum CourseTab: Int, CaseIterable, Identifiable { public var title: String { switch self { case .course: - return CourseLocalization.CourseContainer.course + return CourseLocalization.CourseContainer.home case .videos: return CourseLocalization.CourseContainer.videos case .dates: @@ -39,15 +39,15 @@ public enum CourseTab: Int, CaseIterable, Identifiable { public var image: Image { switch self { case .course: - return CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) + return CoreAssets.home.swiftUIImage.renderingMode(.template) case .videos: - return CoreAssets.videoCircle.swiftUIImage.renderingMode(.template) + return CoreAssets.videos.swiftUIImage.renderingMode(.template) case .dates: - return Image(systemName: "calendar").renderingMode(.template) + return CoreAssets.dates.swiftUIImage.renderingMode(.template) case .discussion: - return CoreAssets.bubbleLeftCircle.swiftUIImage.renderingMode(.template) + return CoreAssets.discussions.swiftUIImage.renderingMode(.template) case .handounds: - return CoreAssets.docCircle.swiftUIImage.renderingMode(.template) + return CoreAssets.more.swiftUIImage.renderingMode(.template) } } } @@ -55,7 +55,8 @@ public enum CourseTab: Int, CaseIterable, Identifiable { public class CourseContainerViewModel: BaseCourseViewModel { @Published public var selection: Int = CourseTab.course.rawValue - @Published private(set) var isShowProgress = false + @Published var isShowProgress = true + @Published var isShowRefresh = false @Published var courseStructure: CourseStructure? @Published var courseDeadlineInfo: CourseDateBanner? @Published var courseVideosStructure: CourseStructure? @@ -135,10 +136,12 @@ public class CourseContainerViewModel: BaseCourseViewModel { guard let courseStart, courseStart < Date() else { return } isShowProgress = withProgress + isShowRefresh = !withProgress do { if isInternetAvaliable { courseStructure = try await interactor.getCourseBlocks(courseID: courseID) isShowProgress = false + isShowRefresh = false if let courseStructure { let continueWith = try await getResumeBlock( courseID: courseID, @@ -154,9 +157,11 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: courseStructure!) await setDownloadsStates() isShowProgress = false + isShowRefresh = false } catch let error { isShowProgress = false + isShowRefresh = false if error.isInternetError || error is NoCachedDataError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection } else { @@ -184,10 +189,13 @@ public class CourseContainerViewModel: BaseCourseViewModel { @MainActor func shiftDueDates(courseID: String, withProgress: Bool = true, screen: DatesStatusInfoScreen, type: String) async { isShowProgress = withProgress + isShowRefresh = !withProgress + do { try await interactor.shiftDueDates(courseID: courseID) NotificationCenter.default.post(name: .shiftCourseDates, object: courseID) isShowProgress = false + isShowRefresh = false analytics.plsSuccessEvent( .plsShiftDatesSuccess, @@ -200,6 +208,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { } catch let error { isShowProgress = false + isShowRefresh = false analytics.plsSuccessEvent( .plsShiftDatesSuccess, bivalue: .plsShiftDatesSuccess, @@ -263,7 +272,6 @@ public class CourseContainerViewModel: BaseCourseViewModel { videos: blocks.count ) } - await download(state: state, blocks: blocks) } diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 612e0fb32..92dc86a6c 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import Core import Theme +import SwiftUIIntrospect public struct CourseDatesView: View { @@ -16,12 +17,18 @@ public struct CourseDatesView: View { @StateObject private var viewModel: CourseDatesViewModel + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool public init( courseID: String, + coordinate: Binding, + collapsed: Binding, viewModel: CourseDatesViewModel ) { self.courseID = courseID + self._coordinate = coordinate + self._collapsed = collapsed self._viewModel = StateObject(wrappedValue: viewModel) } @@ -35,8 +42,14 @@ public struct CourseDatesView: View { .padding(.horizontal) } } else if let courseDates = viewModel.courseDates, !courseDates.courseDateBlocks.isEmpty { - CourseDateListView(viewModel: viewModel, courseDates: courseDates, courseID: courseID) - .padding(.top, 10) + CourseDateListView( + viewModel: viewModel, + coordinate: $coordinate, + collapsed: $collapsed, + courseDates: courseDates, + courseID: courseID + ) + .padding(.top, 10) } } @@ -139,13 +152,19 @@ struct TimeLineView: View { struct CourseDateListView: View { @ObservedObject var viewModel: CourseDatesViewModel @State private var isExpanded = false + @Binding var coordinate: CGFloat + @Binding var collapsed: Bool var courseDates: CourseDates let courseID: String - + var body: some View { GeometryReader { proxy in VStack { ScrollView { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed + ) VStack(alignment: .leading, spacing: 0) { if !courseDates.hasEnded { CalendarSyncView(courseID: courseID, viewModel: viewModel) @@ -198,6 +217,7 @@ struct CourseDateListView: View { .padding(.horizontal, 16) .padding(.vertical, 5) .frameLimit(width: proxy.size.width) + Spacer(minLength: 200) } .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -209,7 +229,7 @@ struct CompletedBlocks: View { @Binding var isExpanded: Bool let courseDateBlockDict: [Date: [CourseDateBlock]] let viewModel: CourseDatesViewModel - + var body: some View { VStack(alignment: .leading, spacing: 5) { // Toggle button to expand/collapse the cell @@ -281,11 +301,10 @@ struct CompletedBlocks: View { .padding(.bottom, 15) .padding(.leading, 16) } - } .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) ) .background(Theme.Colors.datesSectionBackground) } @@ -490,6 +509,8 @@ struct CourseDatesView_Previews: PreviewProvider { CourseDatesView( courseID: "", + coordinate: .constant(0), + collapsed: .constant(false), viewModel: viewModel) } } diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index b8c83aa0b..2effb2678 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -19,7 +19,7 @@ public class CourseDatesViewModel: ObservableObject { case none } - @Published private(set) var isShowProgress = false + @Published var isShowProgress = true @Published var showError: Bool = false @Published var courseDates: CourseDates? @Published var isOn: Bool = false diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index a7acabc0e..de8bb631f 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -12,16 +12,21 @@ import Theme struct HandoutsView: View { private let courseID: String + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool @StateObject private var viewModel: HandoutsViewModel public init( courseID: String, + coordinate: Binding, + collapsed: Binding, viewModel: HandoutsViewModel ) { self.courseID = courseID -// self.viewModel = viewModel + self._coordinate = coordinate + self._collapsed = collapsed self._viewModel = StateObject(wrappedValue: { viewModel }()) } @@ -29,45 +34,50 @@ struct HandoutsView: View { GeometryReader { proxy in ZStack(alignment: .top) { VStack(alignment: .center) { - // MARK: - Page Body - if viewModel.isShowProgress { - HStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } - } else { - VStack(alignment: .leading) { - HandoutsItemCell(type: .handouts, onTapAction: { - viewModel.router.showHandoutsUpdatesView( - handouts: viewModel.handouts, - announcements: nil, - router: viewModel.router, - cssInjector: viewModel.cssInjector) - viewModel.analytics.trackCourseEvent( - .courseHandouts, - biValue: .courseHandouts, - courseID: courseID - ) - }) - Divider() - HandoutsItemCell(type: .announcements, onTapAction: { - if !viewModel.updates.isEmpty { + ScrollView { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed + ) + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + } else { + VStack(alignment: .leading) { + HandoutsItemCell(type: .handouts, onTapAction: { viewModel.router.showHandoutsUpdatesView( - handouts: nil, - announcements: viewModel.updates, + handouts: viewModel.handouts, + announcements: nil, router: viewModel.router, cssInjector: viewModel.cssInjector) viewModel.analytics.trackCourseEvent( - .courseAnnouncement, - biValue: .courseAnnouncement, + .courseHandouts, + biValue: .courseHandouts, courseID: courseID ) - } - }) - }.padding(.horizontal, 32) - Spacer(minLength: 84) + }) + Divider() + HandoutsItemCell(type: .announcements, onTapAction: { + if !viewModel.updates.isEmpty { + viewModel.router.showHandoutsUpdatesView( + handouts: nil, + announcements: viewModel.updates, + router: viewModel.router, + cssInjector: viewModel.cssInjector) + viewModel.analytics.trackCourseEvent( + .courseAnnouncement, + biValue: .courseAnnouncement, + courseID: courseID + ) + } + }) + }.padding(.horizontal, 32) + Spacer(minLength: 84) + } } } .frameLimit(width: proxy.size.width) @@ -123,8 +133,12 @@ struct HandoutsView_Previews: PreviewProvider { connectivity: Connectivity(), courseID: "", analytics: CourseAnalyticsMock()) - HandoutsView(courseID: "", - viewModel: viewModel) + HandoutsView( + courseID: "", + coordinate: .constant(0), + collapsed: .constant(false), + viewModel: viewModel + ) } } #endif diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 9525a9d7b..1c05c575b 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -31,25 +31,27 @@ struct ContinueWithView: View { var body: some View { VStack(alignment: .leading) { - if idiom == .pad { - HStack(alignment: .top) { - VStack(alignment: .leading) { - ContinueTitle(vertical: courseContinueUnit) - }.foregroundColor(Theme.Colors.textPrimary) - Spacer() - UnitButtonView(type: .continueLesson, action: action) - .frame(width: 200) - } .padding(.horizontal, 24) - .padding(.top, 32) - } else { + if idiom == .pad { + HStack(alignment: .top) { VStack(alignment: .leading) { ContinueTitle(vertical: courseContinueUnit) - .foregroundColor(Theme.Colors.textPrimary) - } + }.foregroundColor(Theme.Colors.textPrimary) + Spacer() UnitButtonView(type: .continueLesson, action: action) + .frame(width: 200) } - }.padding(.horizontal, 24) - .padding(.top, 32) + .padding(.horizontal, 24) + .padding(.top, 32) + } else { + VStack(alignment: .leading) { + ContinueTitle(vertical: courseContinueUnit) + .foregroundColor(Theme.Colors.textPrimary) + } + UnitButtonView(type: .continueLesson, action: action) + } + } + .padding(.horizontal, 24) + .padding(.top, 32) } } @@ -101,7 +103,7 @@ struct ContinueWithView_Previews: PreviewProvider { webUrl: "", encodedVideo: nil, multiDevice: false - + ) ] diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index c22d2219b..18575dc7a 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -9,10 +9,11 @@ import SwiftUI import Core import Kingfisher import Theme +import SwiftUIIntrospect public struct CourseOutlineView: View { - @StateObject private var viewModel: CourseContainerViewModel + @ObservedObject private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -20,25 +21,32 @@ public struct CourseOutlineView: View { @State private var openCertificateView: Bool = false private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - + @State private var showingDownloads: Bool = false @State private var showingVideoDownloadQuality: Bool = false @State private var showingNoWifiMessage: Bool = false + @State private var runOnce: Bool = false @Binding private var selection: Int - + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool + public init( viewModel: CourseContainerViewModel, title: String, courseID: String, isVideo: Bool, selection: Binding, + coordinate: Binding, + collapsed: Binding, dateTabIndex: Int ) { self.title = title - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel//StateObject(wrappedValue: { viewModel }()) self.courseID = courseID self.isVideo = isVideo self._selection = selection + self._coordinate = coordinate + self._collapsed = collapsed self.dateTabIndex = dateTabIndex } @@ -58,6 +66,11 @@ public struct CourseOutlineView: View { } } }) { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed + ) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) VStack(alignment: .leading) { if let courseDeadlineInfo = viewModel.courseDeadlineInfo, courseDeadlineInfo.datesBannerInfo.status == .resetDatesBanner, @@ -70,12 +83,8 @@ public struct CourseOutlineView: View { screen: .courseDashbaord ) .padding(.horizontal, 16) - .padding(.top, 16) - } - if viewModel.config.uiComponents.courseBannerEnabled { - courseBanner(proxy: proxy) } - + downloadQualityBars certificateView @@ -101,7 +110,7 @@ public struct CourseOutlineView: View { viewModel.trackResumeCourseClicked( blockId: continueBlock?.id ?? "" ) - + if let course = viewModel.courseStructure { viewModel.router.showCourseUnit( courseName: course.displayName, @@ -134,7 +143,7 @@ public struct CourseOutlineView: View { viewModel: viewModel ) } - + } else { if let courseStart = viewModel.courseStart { Text(courseStart > Date() ? CourseLocalization.Outline.courseHasntStarted : "") @@ -142,7 +151,7 @@ public struct CourseOutlineView: View { .padding(.top, 100) } } - Spacer(minLength: 84) + Spacer(minLength: 200) } .frameLimit(width: proxy.size.width) } @@ -150,9 +159,8 @@ public struct CourseOutlineView: View { viewModel.router.back() } } - .padding(.top, viewModel.config.uiComponents.courseTopTabBarEnabled ? 0 : 8) .accessibilityAction {} - + if viewModel.dueDatesShifted && !isVideo { DatesSuccessView( title: CourseLocalization.CourseDates.toastSuccessTitle, @@ -239,7 +247,7 @@ public struct CourseOutlineView: View { ) } } - + @ViewBuilder private var downloadQualityBars: some View { if isVideo, @@ -271,7 +279,6 @@ public struct CourseOutlineView: View { } } } - @ViewBuilder private var certificateView: some View { // MARK: - Course Certificate @@ -298,7 +305,7 @@ public struct CourseOutlineView: View { ) } } - + private func courseBanner(proxy: GeometryProxy) -> some View { ZStack { // MARK: - Course Banner @@ -356,7 +363,9 @@ struct CourseOutlineView_Previews: PreviewProvider { title: "Course title", courseID: "", isVideo: false, - selection: $selection, + selection: $selection, + coordinate: .constant(0), + collapsed: .constant(false), dateTabIndex: 2 ) .preferredColorScheme(.light) @@ -368,6 +377,8 @@ struct CourseOutlineView_Previews: PreviewProvider { courseID: "", isVideo: false, selection: $selection, + coordinate: .constant(0), + collapsed: .constant(false), dateTabIndex: 2 ) .preferredColorScheme(.dark) diff --git a/Course/Course/Presentation/Subviews/CourseHeaderView.swift b/Course/Course/Presentation/Subviews/CourseHeaderView.swift new file mode 100644 index 000000000..26e8ad38e --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseHeaderView.swift @@ -0,0 +1,175 @@ +// +// TopHeaderView.swift +// Course +// +// Created by  Stepanok Ivan on 04.04.2024. +// + +import SwiftUI +import Kingfisher +import Core +import Theme + +struct CourseHeaderView: View { + + @ObservedObject var viewModel: CourseContainerViewModel + private var title: String + private var containerWidth: CGFloat + private var animationNamespace: Namespace.ID + @Binding private var collapsed: Bool + @Binding private var isAnimatingForTap: Bool + @Environment(\.isHorizontal) private var isHorizontal + + private let collapsedHorizontalHeight: CGFloat = 230 + private let collapsedVerticalHeight: CGFloat = 260 + private let expandedHeight: CGFloat = 300 + + private enum GeometryName { + case backButton + case topTabBar + case blurSecondaryBg + case blurPrimaryBg + case blurBg + } + + init( + viewModel: CourseContainerViewModel, + title: String, + collapsed: Binding, + containerWidth: CGFloat, + animationNamespace: Namespace.ID, + isAnimatingForTap: Binding + ) { + self.viewModel = viewModel + self.title = title + self._collapsed = collapsed + self.containerWidth = containerWidth + self.animationNamespace = animationNamespace + self._isAnimatingForTap = isAnimatingForTap + } + + var body: some View { + ZStack(alignment: .bottomLeading) { + ScrollView { + if let banner = viewModel.courseStructure?.media.image.raw + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + KFImage(URL(string: viewModel.config.baseURL.absoluteString + banner)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxHeight: expandedHeight, alignment: .center) + .allowsHitTesting(false) + .clipped() + .background(Theme.Colors.background) + } + } + .disabled(true) + .ignoresSafeArea() + VStack(alignment: .leading) { + if collapsed { + VStack { + HStack { + BackNavigationButton( + color: Theme.Colors.textPrimary, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .matchedGeometryEffect(id: GeometryName.backButton, in: animationNamespace) + .frame(width: 30, height: 30) + .offset(y: 10) + Text(title) + .lineLimit(1) + .foregroundStyle(Theme.Colors.textPrimary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .clipped() + .font(Theme.Fonts.bodyLarge) + } + .padding(.top, 46) + .padding(.leading, 12) + courseMenuBar(containerWidth: containerWidth) + .matchedGeometryEffect(id: GeometryName.topTabBar, in: animationNamespace) + .padding(.bottom, 12) + }.background { + ZStack(alignment: .bottom) { + Rectangle() + .padding(.top, 24) + .foregroundStyle(Theme.Colors.primaryHeaderColor) + .matchedGeometryEffect(id: GeometryName.blurPrimaryBg, in: animationNamespace) + Rectangle().frame(height: 36) + .foregroundStyle(Theme.Colors.secondaryHeaderColor) + .matchedGeometryEffect(id: GeometryName.blurSecondaryBg, in: animationNamespace) + VisualEffectView(effect: UIBlurEffect(style: .regular)) + .matchedGeometryEffect(id: GeometryName.blurBg, in: animationNamespace) + .ignoresSafeArea() + } + } + } else { + ZStack(alignment: .bottomLeading) { + VStack { + if let org = viewModel.courseStructure?.org { + Text(org) + .font(Theme.Fonts.labelLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + .padding(.horizontal, 24) + .padding(.top, 16) + .allowsHitTesting(false) + .frameLimit(width: containerWidth) + } + Text(title) + .lineLimit(3) + .font(Theme.Fonts.titleLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + .padding(.horizontal, 24) + .allowsHitTesting(false) + .frameLimit(width: containerWidth) + courseMenuBar(containerWidth: containerWidth) + .matchedGeometryEffect(id: GeometryName.topTabBar, in: animationNamespace) + .padding(.bottom, 12) + }.background { + ZStack(alignment: .bottom) { + Rectangle() + .padding(.top, 24) + .foregroundStyle(Theme.Colors.primaryHeaderColor) + .matchedGeometryEffect(id: GeometryName.blurPrimaryBg, in: animationNamespace) + Rectangle().frame(height: 36) + .foregroundStyle(Theme.Colors.secondaryHeaderColor) + .matchedGeometryEffect(id: GeometryName.blurSecondaryBg, in: animationNamespace) + VisualEffectView(effect: UIBlurEffect(style: .regular)) + .matchedGeometryEffect(id: GeometryName.blurBg, in: animationNamespace) + .allowsHitTesting(false) + .ignoresSafeArea() + } + } + } + } + } + } + .background(Theme.Colors.background) + .frame( + height: collapsed ? ( + isHorizontal ? collapsedHorizontalHeight : collapsedVerticalHeight + ) : expandedHeight + ) + .ignoresSafeArea(edges: .top) + } + + private func courseMenuBar(containerWidth: CGFloat) -> some View { + ScrollSlidingTabBar( + selection: $viewModel.selection, + tabs: CourseTab.allCases.map { ($0.title, $0.image) }, + containerWidth: containerWidth + ) { newValue in + isAnimatingForTap = true + viewModel.selection = newValue + DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(300))) { + isAnimatingForTap = false + } + } + } +} diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index 78e663241..a66bfae07 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -55,8 +55,6 @@ public enum CourseLocalization { public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "You've completed “") } public enum CourseContainer { - /// Course - public static let course = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.COURSE", fallback: "Course") /// Dates public static let dates = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DATES", fallback: "Dates") /// Discussions @@ -65,6 +63,8 @@ public enum CourseLocalization { public static let handouts = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HANDOUTS", fallback: "More") /// Handouts In developing public static let handoutsInDeveloping = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING", fallback: "Handouts In developing") + /// Home + public static let home = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HOME", fallback: "Home") /// Videos public static let videos = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.VIDEOS", fallback: "Videos") } diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index b22fce96b..90c6472f3 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -35,7 +35,7 @@ "ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; "ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; -"COURSE_CONTAINER.COURSE" = "Course"; +"COURSE_CONTAINER.HOME" = "Home"; "COURSE_CONTAINER.VIDEOS" = "Videos"; "COURSE_CONTAINER.DATES" = "Dates"; "COURSE_CONTAINER.DISCUSSIONS" = "Discussions"; diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 788115993..435539972 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -97,6 +97,7 @@ final class CourseContainerViewModelTests: XCTestCase { small: "", large: "")), certificate: nil, + org: "", isSelfPaced: true ) @@ -163,6 +164,7 @@ final class CourseContainerViewModelTests: XCTestCase { small: "", large: "")), certificate: nil, + org: "", isSelfPaced: true ) @@ -420,6 +422,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -555,6 +558,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -675,6 +679,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -796,6 +801,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -910,6 +916,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -1039,6 +1046,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) @@ -1188,6 +1196,7 @@ final class CourseContainerViewModelTests: XCTestCase { large: "" )), certificate: nil, + org: "", isSelfPaced: true ) diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift index 1e8c4b92f..74d006cbc 100644 --- a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift @@ -45,6 +45,7 @@ final class CourseDateViewModelTests: XCTestCase { small: "", large: "")), certificate: nil, + org: "", isSelfPaced: true ) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index bcb5aff38..ad84d6a00 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -15,10 +15,21 @@ public struct DiscussionTopicsView: View { @StateObject private var viewModel: DiscussionTopicsViewModel private let router: DiscussionRouter private let courseID: String + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool + @State private var runOnce: Bool = false - public init(courseID: String, viewModel: DiscussionTopicsViewModel, router: DiscussionRouter) { + public init( + courseID: String, + coordinate: Binding, + collapsed: Binding, + viewModel: DiscussionTopicsViewModel, + router: DiscussionRouter + ) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self.courseID = courseID + self._coordinate = coordinate + self._collapsed = collapsed self.router = router } @@ -26,49 +37,54 @@ public struct DiscussionTopicsView: View { GeometryReader { proxy in ZStack(alignment: .center) { VStack(alignment: .center) { - // MARK: - Search fake field - if viewModel.isBlackedOut { - bannerDiscussionsDisabled - } - - HStack(spacing: 11) { - Image(systemName: "magnifyingglass") - .foregroundColor(Theme.Colors.textInputTextColor) - .padding(.leading, 16) - .padding(.top, 1) - Text(DiscussionLocalization.Topics.search) - .foregroundColor(Theme.Colors.textInputTextColor) - .font(Theme.Fonts.bodyMedium) - Spacer() - } - .frame(minHeight: 48) - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputUnfocusedStroke) - ) - .onTapGesture { - viewModel.router.showDiscussionsSearch( - courseID: courseID, - isBlackedOut: viewModel.isBlackedOut + RefreshableScrollViewCompat(action: { + await viewModel.getTopics(courseID: self.courseID, withProgress: false) + }) { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed + ) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + // MARK: - Search fake field + if viewModel.isBlackedOut { + bannerDiscussionsDisabled + } + + HStack(spacing: 11) { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textInputTextColor) + .padding(.leading, 16) + .padding(.top, 1) + Text(DiscussionLocalization.Topics.search) + .foregroundColor(Theme.Colors.textInputTextColor) + .font(Theme.Fonts.bodyMedium) + Spacer() + } + .frame(minHeight: 48) + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) ) - } - .frameLimit(width: proxy.size.width) - .padding(.horizontal, 24) - .padding(.top, 10) - .accessibilityElement(children: .ignore) - .accessibilityLabel(DiscussionLocalization.Topics.search) - - // MARK: - Page Body - VStack { - ZStack(alignment: .top) { - RefreshableScrollViewCompat(action: { - await viewModel.getTopics(courseID: self.courseID, withProgress: false) - }) { + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputUnfocusedStroke) + ) + .onTapGesture { + viewModel.router.showDiscussionsSearch( + courseID: courseID, + isBlackedOut: viewModel.isBlackedOut + ) + } + .frameLimit(width: proxy.size.width) + .padding(.horizontal, 24) + .padding(.top, 10) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscussionLocalization.Topics.search) + + // MARK: - Page Body + VStack { + ZStack(alignment: .top) { VStack { if let topics = viewModel.discussionTopics { HStack { @@ -142,7 +158,7 @@ public struct DiscussionTopicsView: View { } } - Spacer(minLength: 84) + Spacer(minLength: 200) } .frameLimit(width: proxy.size.width) } @@ -156,6 +172,7 @@ public struct DiscussionTopicsView: View { if viewModel.isShowProgress { ProgressBar(size: 40, lineWidth: 8) .padding(.horizontal) + .padding(.top, 100) } } .onFirstAppear { @@ -172,7 +189,7 @@ public struct DiscussionTopicsView: View { ) } } - + private var bannerDiscussionsDisabled: some View { HStack { Spacer() @@ -197,19 +214,28 @@ struct DiscussionView_Previews: PreviewProvider { interactor: DiscussionInteractor.mock, router: DiscussionRouterMock(), analytics: DiscussionAnalyticsMock(), - config: ConfigMock()) + config: ConfigMock() + ) let router = DiscussionRouterMock() - DiscussionTopicsView(courseID: "", - viewModel: vm, - router: router) + DiscussionTopicsView( + courseID: "", + coordinate: .constant(0), + collapsed: .constant(false), + viewModel: vm, + router: router + ) .preferredColorScheme(.light) .previewDisplayName("DiscussionTopicsView Light") .loadFonts() - DiscussionTopicsView(courseID: "", - viewModel: vm, - router: router) + DiscussionTopicsView( + courseID: "", + coordinate: .constant(0), + collapsed: .constant(false), + viewModel: vm, + router: router + ) .preferredColorScheme(.dark) .previewDisplayName("DiscussionTopicsView Dark") .loadFonts() diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index c627bfbe8..51fde8edd 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -13,7 +13,8 @@ import Core public class DiscussionTopicsViewModel: ObservableObject { @Published var topics: Topics? - @Published private(set) var isShowProgress = false + @Published var isShowProgress = true + @Published var isShowRefresh = false @Published var showError: Bool = false @Published var discussionTopics: [DiscussionTopic]? @Published var courseID: String = "" @@ -172,6 +173,7 @@ public class DiscussionTopicsViewModel: ObservableObject { public func getTopics(courseID: String, withProgress: Bool = true) async { self.courseID = courseID isShowProgress = withProgress + isShowRefresh = !withProgress do { let discussionInfo = try await interactor.getCourseDiscussionInfo(courseID: courseID) isBlackedOut = discussionInfo.isBlackedOut() @@ -179,8 +181,10 @@ public class DiscussionTopicsViewModel: ObservableObject { topics = try await interactor.getTopics(courseID: courseID) discussionTopics = generateTopics(topics: topics) isShowProgress = false + isShowRefresh = false } catch let error { isShowProgress = false + isShowRefresh = false if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection } else { diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index 98687f094..e2fc37e54 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -139,6 +139,7 @@ public class CoursePersistence: CoursePersistenceProtocol { ) ), certificate: DataLayer.Certificate(url: structure.certificate), + org: structure.org ?? "", isSelfPaced: structure.isSelfPaced ) } diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseHeader/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseHeader/primaryHeaderColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/primaryHeaderColor.colorset/Contents.json new file mode 100644 index 000000000..8fef18d07 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/primaryHeaderColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseHeader/secondaryHeaderColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/secondaryHeaderColor.colorset/Contents.json new file mode 100644 index 000000000..58170e4e6 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseHeader/secondaryHeaderColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xBA", + "green" : "0xBA", + "red" : "0xBA" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json new file mode 100644 index 000000000..9151445b8 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.408", + "red" : "0.235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json new file mode 100644 index 000000000..93c1c4d85 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.408", + "red" : "0.235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift index c6204d609..733a02635 100644 --- a/Theme/Theme/SwiftGen/ThemeAssets.swift +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -43,6 +43,8 @@ public enum ThemeAssets { public static let todayTimelineColor = ColorAsset(name: "TodayTimelineColor") public static let upcomingTimelineColor = ColorAsset(name: "UpcomingTimelineColor") public static let pastDueTimelineColor = ColorAsset(name: "pastDueTimelineColor") + public static let primaryHeaderColor = ColorAsset(name: "primaryHeaderColor") + public static let secondaryHeaderColor = ColorAsset(name: "secondaryHeaderColor") public static let infoColor = ColorAsset(name: "InfoColor") public static let irreversibleAlert = ColorAsset(name: "IrreversibleAlert") public static let loginBackground = ColorAsset(name: "LoginBackground") @@ -55,6 +57,8 @@ public enum ThemeAssets { public static let secondaryButtonBorderColor = ColorAsset(name: "SecondaryButtonBorderColor") public static let secondaryButtonTextColor = ColorAsset(name: "SecondaryButtonTextColor") public static let shadowColor = ColorAsset(name: "ShadowColor") + public static let slidingStrokeColor = ColorAsset(name: "slidingStrokeColor") + public static let slidingTextColor = ColorAsset(name: "slidingTextColor") public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor") public static let snackbarInfoColor = ColorAsset(name: "SnackbarInfoColor") public static let snackbarTextColor = ColorAsset(name: "SnackbarTextColor") diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift index a535683d9..0fad87eb9 100644 --- a/Theme/Theme/Theme.swift +++ b/Theme/Theme/Theme.swift @@ -64,6 +64,10 @@ public struct Theme { public private(set) static var textInputPlaceholderColor = ThemeAssets.textInputPlaceholderColor.swiftUIColor public private(set) static var infoColor = ThemeAssets.infoColor.swiftUIColor public private(set) static var irreversibleAlert = ThemeAssets.irreversibleAlert.swiftUIColor + public private(set) static var slidingTextColor = ThemeAssets.slidingTextColor.swiftUIColor + 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 static func update( accentColor: Color = ThemeAssets.accentColor.swiftUIColor, diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 04091f6f7..d7cec08d1 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -4,7 +4,5 @@ FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' UI_COMPONENTS: - COURSE_BANNER_ENABLED: true - COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index 04091f6f7..d7cec08d1 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -4,7 +4,5 @@ FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' UI_COMPONENTS: - COURSE_BANNER_ENABLED: true - COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index 04091f6f7..d7cec08d1 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -4,7 +4,5 @@ FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' UI_COMPONENTS: - COURSE_BANNER_ENABLED: true - COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false