Skip to content

Commit

Permalink
Sequence Units level to Course home view as nested list (openedx#192)
Browse files Browse the repository at this point in the history
* feat: add expand section in detail course

* refactor: add click animation and gray arrow

* refactor: add new paddings

* fix: new ids for expand sections

* chore: add counter and remove banner

* fix:  top offset

* chore: add  course expandable sections enabled and  course banner enabled

* fix: tests

* chore: move flags

* chore: add new flag  course nested list

* fix:  animation

* chore: remove extra code

* chore: change key name and merge with develop

* chore: add arrow to header section

* chore: add flags
  • Loading branch information
eyatsenkoperpetio authored Dec 12, 2023
1 parent a7eb8e1 commit 7647134
Show file tree
Hide file tree
Showing 18 changed files with 510 additions and 143 deletions.
4 changes: 4 additions & 0 deletions Core/Core.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; };
0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; };
07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; };
BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; };
BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; };
BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; };
BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; };
Expand Down Expand Up @@ -288,6 +289,7 @@
3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = "<group>"; };
60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = "<group>"; };
9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = "<group>"; };
BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = "<group>"; };
BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = "<group>"; };
BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = "<group>"; };
BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -628,6 +630,7 @@
027BD3C42909707700392132 /* Shake.swift */,
023A1135291432B200D0D354 /* RegistrationTextField.swift */,
023A1137291432FD00D0D354 /* FieldConfiguration.swift */,
BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */,
BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */,
02E93F862AEBAED4006C4750 /* AppReview */,
);
Expand Down Expand Up @@ -1015,6 +1018,7 @@
DBF6F24A2B0380E00098414B /* FeaturesConfig.swift in Sources */,
02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */,
0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */,
BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */,
BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */,
0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */,
023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */,
Expand Down
11 changes: 7 additions & 4 deletions Core/Core/Configuration/Config/UIComponentsConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
import Foundation

private enum Keys: String {
case isVerticalsMenuEnabled = "VERTICALS_MENU_ENABLED"
case courseNestedListEnabled = "COURSE_NESTED_LIST_ENABLED"
case courseBannerEnabled = "COURSE_BANNER_ENABLED"
}

public class UIComponentsConfig: NSObject {
public var isVerticalsMenuEnabled: Bool = false

public var courseNestedListEnabled: Bool = false
public var courseBannerEnabled: Bool

init(dictionary: [String: Any]) {
isVerticalsMenuEnabled = dictionary[Keys.isVerticalsMenuEnabled.rawValue] as? Bool ?? false
courseNestedListEnabled = dictionary[Keys.courseNestedListEnabled.rawValue] as? Bool ?? false
courseBannerEnabled = dictionary[Keys.courseBannerEnabled.rawValue] as? Bool ?? false
super.init()
}
}
Expand Down
8 changes: 4 additions & 4 deletions Core/Core/Domain/Model/CourseBlockModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ public struct CourseStructure: Equatable {

}

public struct CourseChapter {
public struct CourseChapter: Identifiable {

public let blockId: String
public let id: String
public let displayName: String
Expand All @@ -72,8 +72,8 @@ public struct CourseChapter {
}
}

public struct CourseSequential {
public struct CourseSequential: Identifiable {

public let blockId: String
public let id: String
public let displayName: String
Expand Down
2 changes: 2 additions & 0 deletions Core/Core/SwiftGen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public enum CoreLocalization {
public static let nextSectionDescriptionLast = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST", fallback: "” press “Next section”.")
/// Prev
public static let previous = CoreLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev")
/// Resume
public static let resume = CoreLocalization.tr("Localizable", "COURSEWARE.RESUME", fallback: "Resume")
/// Section “
public static let section = CoreLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “")
}
Expand Down
49 changes: 49 additions & 0 deletions Core/Core/View/Base/CustomDisclosureGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// CustomDisclosureGroup.swift
// Core
//
// Created by Eugene Yatsenko on 09.11.2023.
//

import SwiftUI

public struct CustomDisclosureGroup<Header: View, Content: View>: View {

@Binding var isExpanded: Bool

private var onClick: () -> Void
private var animation: Animation?
private let header: Header
private let content: Content

public init(
animation: Animation?,
isExpanded: Binding<Bool>,
onClick: @escaping () -> Void,
header: (_ isExpanded: Bool) -> Header,
content: () -> Content
) {
self.onClick = onClick
self._isExpanded = isExpanded
self.animation = animation
self.header = header(isExpanded.wrappedValue)
self.content = content()
}

public var body: some View {
VStack(spacing: 0) {
Button {
withAnimation(animation) {
onClick()
}
} label: {
header
.contentShape(Rectangle())
}
if isExpanded {
content
}
}
.clipped()
}
}
2 changes: 1 addition & 1 deletion Core/Core/View/Base/UnitButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public enum UnitButtonType: Equatable {
case .reload:
return CoreLocalization.Error.reload
case .continueLesson:
return CoreLocalization.Courseware.continue
return CoreLocalization.Courseware.resume
case .nextSection:
return CoreLocalization.Courseware.nextSection
case let .custom(text):
Expand Down
1 change: 1 addition & 0 deletions Core/Core/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"COURSEWARE.SECTION" = "Section “";
"COURSEWARE.IS_FINISHED" = "“ is finished.";
"COURSEWARE.CONTINUE" = "Continue";
"COURSEWARE.RESUME" = "Resume";
"COURSEWARE.CONTINUE_WITH" = "Continue with:";
"COURSEWARE.NEXT_SECTION" = "Next section";

Expand Down
1 change: 1 addition & 0 deletions Core/Core/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"COURSEWARE.SECTION" = "Секція “";
"COURSEWARE.IS_FINISHED" = "“ завершена.";
"COURSEWARE.CONTINUE" = "Продовжити";
"COURSEWARE.RESUME" = "Resume";
"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:";
"COURSEWARE.NEXT_SECTION" = "Наступний розділ";

Expand Down
4 changes: 4 additions & 0 deletions Course/Course.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */; };
197FB8EA8F92F00A8F383D82 /* Pods_App_Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */; };
B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50E59D2B81E12610964282C5 /* Pods_App_Course_CourseTests.framework */; };
BAAD62C82AFD00EE000E6103 /* CourseExpandableContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C72AFD00EE000E6103 /* CourseExpandableContentView.swift */; };
DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */; };
DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */; };
DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */; };
Expand Down Expand Up @@ -167,6 +168,7 @@
A47C63D9EB0D866F303D4588 /* Pods-App-Course.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.releasestage.xcconfig"; sourceTree = "<group>"; };
ADC2A1B8183A674705F5F7E2 /* Pods-App-Course.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.debug.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.debug.xcconfig"; sourceTree = "<group>"; };
B196A14555D0E006995A5683 /* Pods-App-CourseDetails.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releaseprod.xcconfig"; sourceTree = "<group>"; };
BAAD62C72AFD00EE000E6103 /* CourseExpandableContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseExpandableContentView.swift; sourceTree = "<group>"; };
DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateViewModelTests.swift; sourceTree = "<group>"; };
DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDatesView.swift; sourceTree = "<group>"; };
DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -395,6 +397,7 @@
0270210128E736E700F54332 /* CourseOutlineView.swift */,
02A8076729474831007F53AB /* CourseVerticalView.swift */,
0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */,
BAAD62C72AFD00EE000E6103 /* CourseExpandableContentView.swift */,
06FD7EDE2B1F29F3008D632B /* CourseVerticalImageView.swift */,
);
path = Outline;
Expand Down Expand Up @@ -753,6 +756,7 @@
073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */,
0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */,
02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */,
BAAD62C82AFD00EE000E6103 /* CourseExpandableContentView.swift in Sources */,
02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */,
0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */,
027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */,
Expand Down
75 changes: 56 additions & 19 deletions Course/Course/Presentation/Container/CourseContainerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class CourseContainerViewModel: BaseCourseViewModel {
@Published var courseVideosStructure: CourseStructure?
@Published private(set) var isShowProgress = false
@Published var showError: Bool = false
@Published var downloadState: [String: DownloadViewState] = [:]
@Published var sequentialsDownloadState: [String: DownloadViewState] = [:]
@Published var verticalsDownloadState: [String: DownloadViewState] = [:]
@Published var continueWith: ContinueWith?

var errorMessage: String? {
Expand Down Expand Up @@ -129,7 +130,16 @@ public class CourseContainerViewModel: BaseCourseViewModel {
courseStructure: courseStructure
)
}


func verticalsBlocksDownloadable(by courseSequential: CourseSequential) -> [String: DownloadViewState] {
verticalsDownloadState.filter { dict in
courseSequential.childs.contains(where: { item in
let state = verticalsDownloadState[dict.key]
return (state == .available || state == .downloading) && dict.key == item.id
})
}
}

func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) {
let blocks = chapter.childs
.first(where: { $0.id == blockId })?.childs
Expand All @@ -140,21 +150,21 @@ public class CourseContainerViewModel: BaseCourseViewModel {
switch state {
case .available:
try manager.addToDownloadQueue(blocks: blocks)
downloadState[blockId] = .downloading
sequentialsDownloadState[blockId] = .downloading
case .downloading:
try manager.cancelDownloading(courseId: courseStructure?.id ?? "", blocks: blocks)
downloadState[blockId] = .available
sequentialsDownloadState[blockId] = .available
case .finished:
manager.deleteFile(blocks: blocks)
downloadState[blockId] = .available
sequentialsDownloadState[blockId] = .available
}
} catch let error {
if error is NoWiFiError {
errorMessage = CoreLocalization.Error.wifi
}
}
}

func trackSelectedTab(
selection: CourseContainerView.CourseTab,
courseId: String,
Expand All @@ -173,7 +183,20 @@ public class CourseContainerViewModel: BaseCourseViewModel {
analytics.courseOutlineHandoutsTabClicked(courseId: courseId, courseName: courseName)
}
}


func trackVerticalClicked(
courseId: String,
courseName: String,
vertical: CourseVertical
) {
analytics.verticalClicked(
courseId: courseId,
courseName: courseName,
blockId: vertical.blockId,
blockName: vertical.displayName
)
}

func trackSequentialClicked(_ sequential: CourseSequential) {
guard let course = courseStructure else { return }
analytics.sequentialClicked(
Expand All @@ -197,35 +220,49 @@ public class CourseContainerViewModel: BaseCourseViewModel {
private func setDownloadsStates() {
guard let course = courseStructure else { return }
let downloads = manager.getDownloadsForCourse(course.id)
var states: [String: DownloadViewState] = [:]
var sequentialsStates: [String: DownloadViewState] = [:]
var verticalsStates: [String: DownloadViewState] = [:]
for chapter in course.childs {
for sequential in chapter.childs where sequential.isDownloadable {
var childs: [DownloadViewState] = []
var sequentialsChilds: [DownloadViewState] = []
for vertical in sequential.childs where vertical.isDownloadable {
var verticalsChilds: [DownloadViewState] = []
for block in vertical.childs where block.isDownloadable {
if let download = downloads.first(where: { $0.id == block.id }) {
switch download.state {
case .waiting, .inProgress:
childs.append(.downloading)
sequentialsChilds.append(.downloading)
verticalsChilds.append(.downloading)
case .paused:
childs.append(.available)
sequentialsChilds.append(.available)
verticalsChilds.append(.available)
case .finished:
childs.append(.finished)
sequentialsChilds.append(.finished)
verticalsChilds.append(.finished)
}
} else {
childs.append(.available)
sequentialsChilds.append(.available)
verticalsChilds.append(.available)
}
}
if verticalsChilds.first(where: { $0 == .downloading }) != nil {
verticalsStates[vertical.id] = .downloading
} else if verticalsChilds.allSatisfy({ $0 == .finished }) {
verticalsStates[vertical.id] = .finished
} else {
verticalsStates[vertical.id] = .available
}
}
if childs.first(where: { $0 == .downloading }) != nil {
states[sequential.id] = .downloading
} else if childs.allSatisfy({ $0 == .finished }) {
states[sequential.id] = .finished
if sequentialsChilds.first(where: { $0 == .downloading }) != nil {
sequentialsStates[sequential.id] = .downloading
} else if sequentialsChilds.allSatisfy({ $0 == .finished }) {
sequentialsStates[sequential.id] = .finished
} else {
states[sequential.id] = .available
sequentialsStates[sequential.id] = .available
}
}
self.downloadState = states
self.sequentialsDownloadState = sequentialsStates
self.verticalsDownloadState = verticalsStates
}
}

Expand Down
Loading

0 comments on commit 7647134

Please sign in to comment.