()
- return filter { set.insert($0).inserted }
- }
-}
diff --git a/Core/Core/Extensions/StringExtension.swift b/Core/Core/Extensions/StringExtension.swift
index d9e3b57f6..3a5e6c141 100644
--- a/Core/Core/Extensions/StringExtension.swift
+++ b/Core/Core/Extensions/StringExtension.swift
@@ -21,10 +21,12 @@ public extension String {
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return self
}
- return detector.stringByReplacingMatches(in: self,
- options: [],
- range: NSRange(location: 0, length: self.utf16.count),
- withTemplate: "")
+ return detector.stringByReplacingMatches(
+ in: self,
+ options: [],
+ range: NSRange(location: 0, length: self.utf16.count),
+ withTemplate: ""
+ )
.replacingOccurrences(of: "<[^>]+>", with: "", options: String.CompareOptions.regularExpression, range: nil)
.replacingOccurrences(of: "", with: "")
.replacingOccurrences(of: "
", with: "")
@@ -39,12 +41,15 @@ public extension String {
var urls: [URL] = []
do {
let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
- detector.enumerateMatches(in: self, options: [],
- range: NSRange(location: 0, length: self.count), using: { (result, _, _) in
- if let match = result, let url = match.url {
- urls.append(url)
+ detector.enumerateMatches(
+ in: self, options: [],
+ range: NSRange(location: 0, length: self.count),
+ using: { (result, _, _) in
+ if let match = result, let url = match.url {
+ urls.append(url)
+ }
}
- })
+ )
} catch let error as NSError {
print(error.localizedDescription)
}
diff --git a/Core/Core/Extensions/UIApplication+.swift b/Core/Core/Extensions/UIApplication+.swift
deleted file mode 100644
index 94ead3d50..000000000
--- a/Core/Core/Extensions/UIApplication+.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-//
-
-import UIKit
-
-extension UIApplication {
- var keyWindow: UIWindow? {
- UIApplication.shared.windows.first { $0.isKeyWindow }
- }
-
- func endEditing(force: Bool = true) {
- windows.forEach { $0.endEditing(force) }
- }
-}
diff --git a/Core/Core/Extensions/UIApplicationExtension.swift b/Core/Core/Extensions/UIApplicationExtension.swift
new file mode 100644
index 000000000..1c5dec36c
--- /dev/null
+++ b/Core/Core/Extensions/UIApplicationExtension.swift
@@ -0,0 +1,36 @@
+//
+// UIApplicationExtension.swift
+// Core
+//
+// Created by Stepanok Ivan on 15.06.2023.
+//
+
+import UIKit
+
+extension UIApplication {
+
+ public var keyWindow: UIWindow? {
+ UIApplication.shared.windows.first { $0.isKeyWindow }
+ }
+
+ public func endEditing(force: Bool = true) {
+ windows.forEach { $0.endEditing(force) }
+ }
+
+ public class func topViewController(
+ controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
+ ) -> UIViewController? {
+ if let navigationController = controller as? UINavigationController {
+ return topViewController(controller: navigationController.visibleViewController)
+ }
+ if let tabController = controller as? UITabBarController {
+ if let selected = tabController.selectedViewController {
+ return topViewController(controller: selected)
+ }
+ }
+ if let presented = controller?.presentedViewController {
+ return topViewController(controller: presented)
+ }
+ return controller
+ }
+}
diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift
index e5b1256cf..cab274996 100644
--- a/Core/Core/Extensions/ViewExtension.swift
+++ b/Core/Core/Extensions/ViewExtension.swift
@@ -44,12 +44,14 @@ public extension View {
))
}
- func cardStyle(top: CGFloat? = 0,
- bottom: CGFloat? = 0,
- leftLineEnabled: Bool = false,
- bgColor: Color = CoreAssets.background.swiftUIColor,
- strokeColor: Color = CoreAssets.cardViewStroke.swiftUIColor,
- textColor: Color = CoreAssets.textPrimary.swiftUIColor) -> some View {
+ func cardStyle(
+ top: CGFloat? = 0,
+ bottom: CGFloat? = 0,
+ leftLineEnabled: Bool = false,
+ bgColor: Color = CoreAssets.background.swiftUIColor,
+ strokeColor: Color = CoreAssets.cardViewStroke.swiftUIColor,
+ textColor: Color = CoreAssets.textPrimary.swiftUIColor
+ ) -> some View {
return self
.padding(.all, 20)
.padding(.vertical, leftLineEnabled ? 0 : 6)
@@ -81,10 +83,12 @@ public extension View {
.padding(.bottom, bottom)
}
- func shadowCardStyle(top: CGFloat? = 0,
- bottom: CGFloat? = 0,
- bgColor: Color = CoreAssets.cardViewBackground.swiftUIColor,
- textColor: Color = CoreAssets.textPrimary.swiftUIColor) -> some View {
+ func shadowCardStyle(
+ top: CGFloat? = 0,
+ bottom: CGFloat? = 0,
+ bgColor: Color = CoreAssets.cardViewBackground.swiftUIColor,
+ textColor: Color = CoreAssets.textPrimary.swiftUIColor
+ ) -> some View {
return self
.padding(.all, 16)
.padding(.vertical, 6)
@@ -103,8 +107,11 @@ public extension View {
}
- func titleSettings(top: CGFloat? = 10, bottom: CGFloat? = 20,
- color: Color = CoreAssets.textPrimary.swiftUIColor) -> some View {
+ func titleSettings(
+ top: CGFloat? = 10,
+ bottom: CGFloat? = 20,
+ color: Color = CoreAssets.textPrimary.swiftUIColor
+ ) -> some View {
return self
.lineLimit(1)
.truncationMode(.tail)
@@ -123,10 +130,12 @@ public extension View {
}
}
- func roundedBackground(_ color: Color = CoreAssets.background.swiftUIColor,
- strokeColor: Color = CoreAssets.backgroundStroke.swiftUIColor,
- ipadMaxHeight: CGFloat = .infinity,
- maxIpadWidth: CGFloat = 420) -> some View {
+ func roundedBackground(
+ _ color: Color = CoreAssets.background.swiftUIColor,
+ strokeColor: Color = CoreAssets.backgroundStroke.swiftUIColor,
+ ipadMaxHeight: CGFloat = .infinity,
+ maxIpadWidth: CGFloat = 420
+ ) -> some View {
var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
return ZStack {
RoundedCorners(tl: 24, tr: 24)
diff --git a/Core/Core/Network/API.swift b/Core/Core/Network/API.swift
index 19bd2edc0..d891c7af5 100644
--- a/Core/Core/Network/API.swift
+++ b/Core/Core/Network/API.swift
@@ -7,6 +7,7 @@
import Foundation
import Alamofire
+import WebKit
public final class API {
@@ -127,6 +128,12 @@ public final class API {
let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url)
HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) }
HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil)
+ DispatchQueue.main.async {
+ let cookies = HTTPCookieStorage.shared.cookies ?? []
+ for c in cookies {
+ WKWebsiteDataStore.default().httpCookieStore.setCookie(c)
+ }
+ }
}
private func callResponse(
diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift
index 0392c13f5..7166f5b90 100644
--- a/Core/Core/Network/DownloadManager.swift
+++ b/Core/Core/Network/DownloadManager.swift
@@ -188,12 +188,14 @@ public class DownloadManager: DownloadManagerProtocol {
let directoryURL = documentDirectoryURL.appendingPathComponent("Files", isDirectory: true)
if FileManager.default.fileExists(atPath: directoryURL.path) {
- print(directoryURL.path)
return URL(fileURLWithPath: directoryURL.path)
} else {
do {
- try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
- print(directoryURL.path)
+ try FileManager.default.createDirectory(
+ at: directoryURL,
+ withIntermediateDirectories: true,
+ attributes: nil
+ )
return URL(fileURLWithPath: directoryURL.path)
} catch {
print(error.localizedDescription)
diff --git a/Core/Core/Network/HeadersRedirectHandler.swift b/Core/Core/Network/HeadersRedirectHandler.swift
index ccb60e29e..3653392bd 100644
--- a/Core/Core/Network/HeadersRedirectHandler.swift
+++ b/Core/Core/Network/HeadersRedirectHandler.swift
@@ -17,16 +17,17 @@ public class HeadersRedirectHandler: RedirectHandler {
_ task: URLSessionTask,
willBeRedirectedTo request: URLRequest,
for response: HTTPURLResponse,
- completion: @escaping (URLRequest?) -> Void) {
- var redirectedRequest = request
-
- if let originalRequest = task.originalRequest,
- let headers = originalRequest.allHTTPHeaderFields {
- for (key, value) in headers {
- redirectedRequest.setValue(value, forHTTPHeaderField: key)
- }
+ completion: @escaping (URLRequest?) -> Void
+ ) {
+ var redirectedRequest = request
+
+ if let originalRequest = task.originalRequest,
+ let headers = originalRequest.allHTTPHeaderFields {
+ for (key, value) in headers {
+ redirectedRequest.setValue(value, forHTTPHeaderField: key)
}
-
- completion(redirectedRequest)
}
+
+ completion(redirectedRequest)
+ }
}
diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift
index 0915d4f84..3f8e80b9a 100644
--- a/Core/Core/Network/RequestInterceptor.swift
+++ b/Core/Core/Network/RequestInterceptor.swift
@@ -27,10 +27,10 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor {
_ urlRequest: URLRequest,
for session: Session,
completion: @escaping (Result) -> Void) {
- // guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else {
- // /// If the request does not require authentication, we can directly return it as unmodified.
- // return completion(.success(urlRequest))
- // }
+// guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else {
+// // If the request does not require authentication, we can directly return it as unmodified.
+// return completion(.success(urlRequest))
+// }
var urlRequest = urlRequest
// Set the Authorization header value using the access token.
diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift
index e9d8dfc49..887501306 100644
--- a/Core/Core/SwiftGen/Assets.swift
+++ b/Core/Core/SwiftGen/Assets.swift
@@ -57,6 +57,7 @@ public enum CoreAssets {
public static let allPosts = ImageAsset(name: "allPosts")
public static let chapter = ImageAsset(name: "chapter")
public static let discussion = ImageAsset(name: "discussion")
+ public static let discussionIcon = ImageAsset(name: "discussionIcon")
public static let extra = ImageAsset(name: "extra")
public static let filter = ImageAsset(name: "filter")
public static let finished = ImageAsset(name: "finished")
diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift
index 6790017a0..0197b0494 100644
--- a/Core/Core/SwiftGen/Strings.swift
+++ b/Core/Core/SwiftGen/Strings.swift
@@ -24,6 +24,36 @@ public enum CoreLocalization {
/// Log out
public static let logout = CoreLocalization.tr("Localizable", "ALERT.LOGOUT", fallback: "Log out")
}
+ public enum Courseware {
+ /// Back to outline
+ public static let backToOutline = CoreLocalization.tr("Localizable", "COURSEWARE.BACK_TO_OUTLINE", fallback: "Back to outline")
+ /// Continue
+ public static let `continue` = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE", fallback: "Continue")
+ /// Continue with:
+ public static let continueWith = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE_WITH", fallback: "Continue with:")
+ /// Course content
+ public static let courseContent = CoreLocalization.tr("Localizable", "COURSEWARE.COURSE_CONTENT", fallback: "Course content")
+ /// Course units
+ public static let courseUnits = CoreLocalization.tr("Localizable", "COURSEWARE.COURSE_UNITS", fallback: "Course units")
+ /// Finish
+ public static let finish = CoreLocalization.tr("Localizable", "COURSEWARE.FINISH", fallback: "Finish")
+ /// Good Work!
+ public static let goodWork = CoreLocalization.tr("Localizable", "COURSEWARE.GOOD_WORK", fallback: "Good Work!")
+ /// “ is finished.
+ public static let isFinished = CoreLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "“ is finished.")
+ /// Next
+ public static let next = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT", fallback: "Next")
+ /// Next section
+ public static let nextSection = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION", fallback: "Next section")
+ /// To proceed with “
+ public static let nextSectionDescriptionFirst = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST", fallback: "To proceed with “")
+ /// ” press “Next section”.
+ 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")
+ /// Section “
+ public static let section = CoreLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “")
+ }
public enum Date {
/// Ended
public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended")
@@ -53,6 +83,8 @@ public enum CoreLocalization {
public static let invalidCredentials = CoreLocalization.tr("Localizable", "ERROR.INVALID_CREDENTIALS", fallback: "Invalid credentials")
/// No cached data for offline mode
public static let noCachedData = CoreLocalization.tr("Localizable", "ERROR.NO_CACHED_DATA", fallback: "No cached data for offline mode")
+ /// Reload
+ public static let reload = CoreLocalization.tr("Localizable", "ERROR.RELOAD", fallback: "Reload")
/// Slow or no internet connection
public static let slowOrNoInternetConnection = CoreLocalization.tr("Localizable", "ERROR.SLOW_OR_NO_INTERNET_CONNECTION", fallback: "Slow or no internet connection")
/// Something went wrong
@@ -97,6 +129,14 @@ public enum CoreLocalization {
public static let tryAgainBtn = CoreLocalization.tr("Localizable", "VIEW.SNACKBAR.TRY_AGAIN_BTN", fallback: "Try Again")
}
}
+ public enum Webview {
+ public enum Alert {
+ /// Cancel
+ public static let cancel = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CANCEL", fallback: "Cancel")
+ /// Ok
+ public static let ok = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.OK", fallback: "Ok")
+ }
+ }
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
diff --git a/Core/Core/Theme.swift b/Core/Core/Theme.swift
index 57bc2bcd7..d8eb84573 100644
--- a/Core/Core/Theme.swift
+++ b/Core/Core/Theme.swift
@@ -38,6 +38,7 @@ public struct Theme {
public static let cardImageRadius = 10.0
public static let textInputShape = RoundedRectangle(cornerRadius: 8)
public static let buttonShape = RoundedCorners(tl: 8, tr: 8, bl: 8, br: 8)
+ public static let unitButtonShape = RoundedCorners(tl: 21, tr: 21, bl: 21, br: 21)
public static let roundedScreenBackgroundShape = RoundedCorners(
tl: Theme.Shapes.screenBackgroundRadius,
tr: Theme.Shapes.screenBackgroundRadius,
diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift
index 7a51d0bf8..d11ec70db 100644
--- a/Core/Core/View/Base/AlertView.swift
+++ b/Core/Core/View/Base/AlertView.swift
@@ -27,8 +27,10 @@ public struct AlertView: View {
private var alertTitle: String
private var alertMessage: String
+ private var nextSectionName: String?
private var onCloseTapped: (() -> Void) = {}
private var okTapped: (() -> Void) = {}
+ private var nextSectionTapped: (() -> Void) = {}
private let type: AlertViewType
public init(
@@ -49,15 +51,19 @@ public struct AlertView: View {
public init(
alertTitle: String,
alertMessage: String,
+ nextSectionName: String? = nil,
mainAction: String,
image: SwiftUI.Image,
onCloseTapped: @escaping () -> Void,
- okTapped: @escaping () -> Void
+ okTapped: @escaping () -> Void,
+ nextSectionTapped: @escaping () -> Void
) {
self.alertTitle = alertTitle
self.alertMessage = alertMessage
self.onCloseTapped = onCloseTapped
+ self.nextSectionName = nextSectionName
self.okTapped = okTapped
+ self.nextSectionTapped = nextSectionTapped
type = .action(mainAction, image)
}
@@ -99,6 +105,7 @@ public struct AlertView: View {
.font(Theme.Fonts.bodyMedium)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
+ .frame(maxWidth: 250)
}
HStack {
switch type {
@@ -109,8 +116,29 @@ public struct AlertView: View {
.frame(maxWidth: 135)
.saturation(0)
case let .action(action, _):
- StyledButton(action, action: { okTapped() })
- .frame(maxWidth: 160)
+ VStack(spacing: 20) {
+ if let nextSectionName {
+ UnitButtonView(type: .nextSection, action: { nextSectionTapped() })
+ .frame(maxWidth: 215)
+ }
+ UnitButtonView(type: .custom(action),
+ bgColor: .clear,
+ action: { okTapped() })
+ .frame(maxWidth: 215)
+
+ if let nextSectionName {
+ Group {
+ Text(CoreLocalization.Courseware.nextSectionDescriptionFirst) +
+ Text(nextSectionName) +
+ Text(CoreLocalization.Courseware.nextSectionDescriptionLast)
+ }.frame(maxWidth: 215)
+ .padding(.horizontal, 40)
+ .multilineTextAlignment(.center)
+ .font(Theme.Fonts.labelSmall)
+ .foregroundColor(CoreAssets.textSecondary.swiftUIColor)
+ }
+
+ }
case .logOut:
Button(action: {
okTapped()
@@ -133,7 +161,12 @@ public struct AlertView: View {
)
.overlay(
RoundedRectangle(cornerRadius: 8)
- .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
+ .stroke(style: .init(
+ lineWidth: 1,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 1
+ ))
.foregroundColor(.clear)
)
.frame(maxWidth: 215)
@@ -157,7 +190,12 @@ public struct AlertView: View {
)
.overlay(
RoundedRectangle(cornerRadius: 8)
- .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
+ .stroke(style: .init(
+ lineWidth: 1,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 1
+ ))
.foregroundColor(.clear)
)
.frame(maxWidth: 215)
@@ -180,7 +218,12 @@ public struct AlertView: View {
)
.overlay(
RoundedRectangle(cornerRadius: 8)
- .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
+ .stroke(style: .init(
+ lineWidth: 1,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 1
+ ))
.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
)
.frame(maxWidth: 215)
@@ -217,14 +260,23 @@ public struct AlertView: View {
// swiftlint:disable all
struct AlertView_Previews: PreviewProvider {
static var previews: some View {
- AlertView(
- alertTitle: "Warning!",
- alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now",
- positiveAction: "Accept",
- onCloseTapped: {},
- okTapped: {},
- type: .logOut
- )
+// AlertView(
+// alertTitle: "Warning!",
+// alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now",
+// positiveAction: "Accept",
+// onCloseTapped: {},
+// okTapped: {},
+// type: .action("", CoreAssets.goodWork.swiftUIImage)
+// )
+ AlertView(alertTitle: "Warning",
+ alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now",
+ nextSectionName: "Ahmad tea is a power",
+ mainAction: "Back to outline",
+ image: CoreAssets.goodWork.swiftUIImage,
+ onCloseTapped: {},
+ okTapped: {},
+ nextSectionTapped: {})
+
.previewLayout(.sizeThatFits)
.background(Color.gray)
}
diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift
index c3a607e35..a7473e36e 100644
--- a/Core/Core/View/Base/CourseButton.swift
+++ b/Core/Core/View/Base/CourseButton.swift
@@ -59,6 +59,11 @@ public struct CourseButton: View {
struct CourseButton_Previews: PreviewProvider {
static var previews: some View {
- CourseButton(isCompleted: true, image: CoreAssets.pen.swiftUIImage, displayName: "Lets see whats happen", index: 0)
+ CourseButton(
+ isCompleted: true,
+ image: CoreAssets.pen.swiftUIImage,
+ displayName: "Lets see whats happen",
+ index: 0
+ )
}
}
diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift
index 1388d1489..dfb0b6925 100644
--- a/Core/Core/View/Base/CourseCellView.swift
+++ b/Core/Core/View/Base/CourseCellView.swift
@@ -33,7 +33,7 @@ public struct CourseCellView: View {
self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear) ?? ""
self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay) ?? ""
self.courseOrg = model.org
- self.index = Double(index)+1
+ self.index = Double(index) + 1
self.cellsCount = cellsCount
}
diff --git a/Core/Core/View/Base/HTMLFormattedText.swift b/Core/Core/View/Base/HTMLFormattedText.swift
index d29f9e8b0..6276b9f2d 100644
--- a/Core/Core/View/Base/HTMLFormattedText.swift
+++ b/Core/Core/View/Base/HTMLFormattedText.swift
@@ -70,11 +70,14 @@ public struct HTMLFormattedText: UIViewRepresentable {
private func convertHTML(text: String) -> NSAttributedString? {
guard let data = text.data(using: .utf8) else { return nil }
- if let attributedString = try? NSAttributedString(data: data,
- options: [
- .documentType: NSAttributedString.DocumentType.html,
- .characterEncoding: String.Encoding.utf8.rawValue
- ], documentAttributes: nil) {
+ if let attributedString = try? NSAttributedString(
+ data: data,
+ options: [
+ .documentType: NSAttributedString.DocumentType.html,
+ .characterEncoding: String.Encoding.utf8.rawValue
+ ],
+ documentAttributes: nil
+ ) {
return attributedString
} else {
return nil
diff --git a/Core/Core/View/Base/PickerMenu.swift b/Core/Core/View/Base/PickerMenu.swift
index ab2377f89..ea0a78ce5 100644
--- a/Core/Core/View/Base/PickerMenu.swift
+++ b/Core/Core/View/Base/PickerMenu.swift
@@ -32,11 +32,13 @@ public struct PickerMenu: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
private var selected: ((PickerItem) -> Void) = { _ in }
- public init(items: [PickerItem],
- titleText: String,
- router: BaseRouter,
- selectedItem: PickerItem? = nil,
- selected: @escaping (PickerItem) -> Void) {
+ public init(
+ items: [PickerItem],
+ titleText: String,
+ router: BaseRouter,
+ selectedItem: PickerItem? = nil,
+ selected: @escaping (PickerItem) -> Void
+ ) {
self.items = items
self.titleText = titleText
self.router = router
diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift
index 4e1a0b872..65d223447 100644
--- a/Core/Core/View/Base/StyledButton.swift
+++ b/Core/Core/View/Base/StyledButton.swift
@@ -43,7 +43,7 @@ public struct StyledButton: View {
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
}
- .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 48)
+ .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 42)
.background(
Theme.Shapes.buttonShape
.fill(isTransparent ? .clear : buttonColor)
diff --git a/Core/Core/View/Base/TextWithUrls.swift b/Core/Core/View/Base/TextWithUrls.swift
index 70bc6934b..e5e50d19e 100644
--- a/Core/Core/View/Base/TextWithUrls.swift
+++ b/Core/Core/View/Base/TextWithUrls.swift
@@ -62,7 +62,9 @@ public struct TextWithUrls: View {
var text = Text("")
attributedString.enumerateAttributes(in: stringRange, options: []) { attrs, range, _ in
let valueOfString: String = attributedString.attributedSubstring(from: range).string
- text = text + Text(.init((attrs[.underlineStyle] != nil ? getMarkupText(url: valueOfString): valueOfString)))
+ text = text + Text(.init((attrs[.underlineStyle] != nil
+ ? getMarkupText(url: valueOfString)
+ : valueOfString)))
}
return text
diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift
new file mode 100644
index 000000000..52b3900c9
--- /dev/null
+++ b/Core/Core/View/Base/UnitButtonView.swift
@@ -0,0 +1,209 @@
+//
+// UnitButtonView.swift
+// Course
+//
+// Created by Stepanok Ivan on 14.02.2023.
+//
+
+import SwiftUI
+
+public enum UnitButtonType: Equatable {
+ case first
+ case next
+ case nextBig
+ case previous
+ case last
+ case finish
+ case reload
+ case continueLesson
+ case nextSection
+ case custom(String)
+
+ func stringValue() -> String {
+ switch self {
+ case .first:
+ return CoreLocalization.Courseware.next
+ case .next, .nextBig:
+ return CoreLocalization.Courseware.next
+ case .previous:
+ return CoreLocalization.Courseware.previous
+ case .last:
+ return CoreLocalization.Courseware.finish
+ case .finish:
+ return CoreLocalization.Courseware.finish
+ case .reload:
+ return CoreLocalization.Error.reload
+ case .continueLesson:
+ return CoreLocalization.Courseware.continue
+ case .nextSection:
+ return CoreLocalization.Courseware.nextSection
+ case let .custom(text):
+ return text
+ }
+ }
+}
+
+public struct UnitButtonView: View {
+
+ private let action: () -> Void
+ private let type: UnitButtonType
+ private let bgColor: Color?
+
+ public init(type: UnitButtonType, bgColor: Color? = nil, action: @escaping () -> Void) {
+ self.type = type
+ self.bgColor = bgColor
+ self.action = action
+ }
+
+ public var body: some View {
+ HStack {
+ Button(action: action) {
+ VStack {
+ switch type {
+ case .first:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .font(Theme.Fonts.labelLarge)
+ CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .rotationEffect(Angle.degrees(-90))
+ }.padding(.horizontal, 16)
+ case .next, .nextBig:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .padding(.leading, 20)
+ .font(Theme.Fonts.labelLarge)
+ if type != .nextBig {
+ Spacer()
+ }
+ CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .rotationEffect(Angle.degrees(-90))
+ .padding(.trailing, 20)
+ }
+ case .previous:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.accentColor.swiftUIColor)
+ .font(Theme.Fonts.labelLarge)
+ .padding(.leading, 20)
+ CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
+ .rotationEffect(Angle.degrees(90))
+ .padding(.trailing, 20)
+ .foregroundColor(CoreAssets.accentColor.swiftUIColor)
+
+ }
+ case .last:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .padding(.leading, 16)
+ .font(Theme.Fonts.labelLarge)
+ Spacer()
+ CoreAssets.check.swiftUIImage.renderingMode(.template)
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .padding(.trailing, 16)
+ }
+ case .finish:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .font(Theme.Fonts.labelLarge)
+ CoreAssets.check.swiftUIImage.renderingMode(.template)
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ }.padding(.horizontal, 16)
+ case .reload, .custom:
+ VStack(alignment: .center) {
+ Text(type.stringValue())
+ .foregroundColor(bgColor == nil ? .white : CoreAssets.accentColor.swiftUIColor)
+ .font(Theme.Fonts.labelLarge)
+ }.padding(.horizontal, 16)
+ case .continueLesson, .nextSection:
+ HStack {
+ Text(type.stringValue())
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .padding(.leading, 20)
+ .font(Theme.Fonts.labelLarge)
+ CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
+ .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
+ .rotationEffect(Angle.degrees(180))
+ .padding(.trailing, 20)
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, minHeight: 42)
+ .background(
+ VStack {
+ switch self.type {
+ case .first, .next, .nextBig, .previous, .last:
+ Theme.Shapes.buttonShape
+ .fill(type == .previous
+ ? CoreAssets.background.swiftUIColor
+ : CoreAssets.accentColor.swiftUIColor)
+ .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4)
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(style: .init(
+ lineWidth: 1,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 1)
+ )
+ .foregroundColor(CoreAssets.accentColor.swiftUIColor)
+ )
+
+ case .continueLesson, .nextSection, .reload, .finish, .custom:
+ Theme.Shapes.buttonShape
+ .fill(bgColor ?? CoreAssets.accentColor.swiftUIColor)
+
+ .shadow(color: (type == .first
+ || type == .next
+ || type == .previous
+ || type == .last
+ || type == .finish
+ || type == .reload) ? Color.black.opacity(0.25) : .clear,
+ radius: 21, y: 4)
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(style: .init(
+ lineWidth: 1,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 1
+ ))
+ .foregroundColor(CoreAssets.accentColor.swiftUIColor)
+ )
+ }
+ }
+ )
+
+ }
+ .fixedSize(horizontal: (type == .first
+ || type == .next
+ || type == .previous
+ || type == .last
+ || type == .finish
+ || type == .reload)
+ , vertical: false)
+ }
+
+ }
+}
+
+struct UnitButtonView_Previews: PreviewProvider {
+ static var previews: some View {
+ VStack {
+ UnitButtonView(type: .first, action: {})
+ UnitButtonView(type: .previous, action: {})
+ UnitButtonView(type: .next, action: {})
+ UnitButtonView(type: .last, action: {})
+ UnitButtonView(type: .finish, action: {})
+ UnitButtonView(type: .reload, action: {})
+ UnitButtonView(type: .custom("Custom text"), action: {})
+ UnitButtonView(type: .continueLesson, action: {})
+ UnitButtonView(type: .nextSection, action: {})
+ }.padding()
+ }
+}
diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift
index 1919ce173..6d89e4528 100644
--- a/Core/Core/View/Base/WebBrowser.swift
+++ b/Core/Core/View/Base/WebBrowser.swift
@@ -6,6 +6,7 @@
//
import SwiftUI
+import WebKit
public struct WebBrowser: View {
@@ -24,17 +25,23 @@ public struct WebBrowser: View {
// MARK: - Page name
VStack(alignment: .center) {
- NavigationBar(title: pageTitle,
- leftButtonAction: { presentationMode.wrappedValue.dismiss() })
+ NavigationBar(
+ title: pageTitle,
+ leftButtonAction: { presentationMode.wrappedValue.dismiss() }
+ )
// MARK: - Page Body
VStack {
ZStack(alignment: .top) {
NavigationView {
- WebView(viewModel: .init(url: url, baseURL: ""), isLoading: $isShowProgress, refreshCookies: {})
- .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15
- .navigationBarHidden(true)
- .ignoresSafeArea()
+ WebView(
+ viewModel: .init(url: url, baseURL: ""),
+ isLoading: $isShowProgress,
+ refreshCookies: {}
+ )
+ .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15
+ .navigationBarHidden(true)
+ .ignoresSafeArea()
}
}
}
diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift
index b9dc4e772..85285a21b 100644
--- a/Core/Core/View/Base/WebUnitView.swift
+++ b/Core/Core/View/Base/WebUnitView.swift
@@ -17,66 +17,64 @@ public struct WebUnitView: View {
public init(url: String, viewModel: WebUnitViewModel) {
self.viewModel = viewModel
self.url = url
- Task {
- await viewModel.updateCookies()
- }
}
@ViewBuilder
public var body: some View {
- ZStack(alignment: .center) {
- GeometryReader { reader in
- ScrollView {
- if viewModel.cookiesReady {
- WebView(
- viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString),
- isLoading: $isWebViewLoading, refreshCookies: {
- await viewModel.updateCookies(force: true)
+ // MARK: - Error Alert
+ if viewModel.showError {
+ VStack(spacing: 28) {
+ Image(systemName: "nosign")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 64)
+ .foregroundColor(CoreAssets.textPrimary.swiftUIColor)
+ Text(viewModel.errorMessage ?? "")
+ .foregroundColor(CoreAssets.textPrimary.swiftUIColor)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 20)
+ Button(action: {
+ Task {
+ await viewModel.updateCookies(force: true)
+ }
+ }, label: {
+ Text(CoreLocalization.View.Snackbar.tryAgainBtn)
+ .frame(maxWidth: .infinity, minHeight: 48)
+ .background(Theme.Shapes.buttonShape.fill(.clear))
+ .overlay(RoundedRectangle(cornerRadius: 8)
+ .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
+ .foregroundColor(CoreAssets.accentColor.swiftUIColor)
+ )
+ })
+ .frame(width: 100)
+ }.frame(maxWidth: .infinity, maxHeight: .infinity)
+ } else {
+ ZStack(alignment: .center) {
+ GeometryReader { reader in
+ ScrollView {
+ if viewModel.cookiesReady {
+ WebView(
+ viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString),
+ isLoading: $isWebViewLoading, refreshCookies: {
+ await viewModel.updateCookies(force: true)
+ })
+ .introspectScrollView(customize: { scrollView in
+ scrollView.isScrollEnabled = false
})
- .introspectScrollView(customize: { scrollView in
- scrollView.isScrollEnabled = false
- scrollView.alwaysBounceVertical = false
- scrollView.alwaysBounceHorizontal = false
- scrollView.bounces = false
- })
- .frame(width: reader.size.width, height: reader.size.height)
+ .frame(width: reader.size.width, height: reader.size.height)
+ }
}
- }
- if viewModel.updatingCookies || isWebViewLoading {
- VStack {
- ProgressBar(size: 40, lineWidth: 8)
+ if viewModel.updatingCookies || isWebViewLoading {
+ VStack {
+ ProgressBar(size: 40, lineWidth: 8)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
- .frame(maxWidth: .infinity, maxHeight: .infinity)
}
- }
-
- // MARK: - Error Alert
- if viewModel.showError {
- VStack(spacing: 28) {
- Image(systemName: "nosign")
- .resizable()
- .scaledToFit()
- .frame(width: 64)
- .foregroundColor(.black)
- Text(viewModel.errorMessage ?? "")
- .foregroundColor(.black)
- .multilineTextAlignment(.center)
- .padding(.horizontal, 20)
- Button(action: {
- Task {
- await viewModel.updateCookies(force: true)
- }
- }, label: {
- Text(CoreLocalization.View.Snackbar.tryAgainBtn)
- .frame(maxWidth: .infinity, minHeight: 48)
- .background(Theme.Shapes.buttonShape.fill(.clear))
- .overlay(RoundedRectangle(cornerRadius: 8)
- .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
- .foregroundColor(CoreAssets.accentColor.swiftUIColor)
- )
- })
- .frame(width: 100)
- }.frame(maxWidth: .infinity, maxHeight: .infinity)
+ }.onFirstAppear {
+ Task {
+ await viewModel.updateCookies()
+ }
}
}
}
diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift
index 866f5dba4..caa010a93 100644
--- a/Core/Core/View/Base/WebUnitViewModel.swift
+++ b/Core/Core/View/Base/WebUnitViewModel.swift
@@ -33,11 +33,13 @@ public class WebUnitViewModel: ObservableObject {
@MainActor
func updateCookies(force: Bool = false) async {
+ guard !updatingCookies else { return }
do {
updatingCookies = true
try await authInteractor.getCookies(force: force)
cookiesReady = true
updatingCookies = false
+ errorMessage = nil
} catch {
if error.isInternetError {
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/WebView.swift
index 4ad2f9d5c..463a9a315 100644
--- a/Core/Core/View/Base/WebView.swift
+++ b/Core/Core/View/Base/WebView.swift
@@ -32,7 +32,7 @@ public struct WebView: UIViewRepresentable {
self.refreshCookies = refreshCookies
}
- public class Coordinator: NSObject, WKNavigationDelegate {
+ public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
var parent: WebView
init(_ parent: WebView) {
@@ -45,9 +45,36 @@ public struct WebView: UIViewRepresentable {
}
}
- public func webView(_ webView: WKWebView,
- decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
+ public func webView(
+ _ webView: WKWebView,
+ runJavaScriptConfirmPanelWithMessage message: String,
+ initiatedByFrame frame: WKFrameInfo,
+ completionHandler: @escaping (Bool) -> Void
+ ) {
+ let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
+
+ alertController.addAction(UIAlertAction(
+ title: CoreLocalization.Webview.Alert.ok,
+ style: .default,
+ handler: { _ in
+ completionHandler(true)
+ }))
+
+ alertController.addAction(UIAlertAction(
+ title: CoreLocalization.Webview.Alert.cancel,
+ style: .cancel,
+ handler: { _ in
+ completionHandler(false)
+ }))
+
+ UIApplication.topViewController()?.present(alertController, animated: true, completion: nil)
+ }
+
+ public func webView(
+ _ webView: WKWebView,
+ decidePolicyFor navigationAction: WKNavigationAction
+ ) async -> WKNavigationActionPolicy {
guard let url = navigationAction.request.url else {
return .cancel
}
@@ -63,12 +90,17 @@ public struct WebView: UIViewRepresentable {
return .allow
}
- public func webView(_ webView: WKWebView,
- decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
- guard let statusCode = (navigationResponse.response as? HTTPURLResponse)?.statusCode else {
+ public func webView(
+ _ webView: WKWebView,
+ decidePolicyFor navigationResponse: WKNavigationResponse
+ ) async -> WKNavigationResponsePolicy {
+ guard let response = (navigationResponse.response as? HTTPURLResponse),
+ let url = response.url else {
return .cancel
}
- if (401...404).contains(statusCode) {
+ let baseURL = await parent.viewModel.baseURL
+
+ if (401...404).contains(response.statusCode) || url.absoluteString.hasPrefix(baseURL + "/login") {
await parent.refreshCookies()
DispatchQueue.main.async {
if let url = webView.url {
@@ -86,34 +118,35 @@ public struct WebView: UIViewRepresentable {
}
public func makeUIView(context: UIViewRepresentableContext) -> WKWebView {
- let webview = WKWebView()
- webview.navigationDelegate = context.coordinator
+ let webViewConfig = WKWebViewConfiguration()
- webview.scrollView.bounces = false
- webview.scrollView.alwaysBounceHorizontal = false
- webview.scrollView.showsHorizontalScrollIndicator = false
- webview.scrollView.isScrollEnabled = true
- webview.configuration.suppressesIncrementalRendering = true
- webview.isOpaque = false
- webview.backgroundColor = .clear
- webview.scrollView.backgroundColor = UIColor.clear
- webview.scrollView.alwaysBounceVertical = false
+ let webView = WKWebView(frame: .zero, configuration: webViewConfig)
+ webView.navigationDelegate = context.coordinator
+ webView.uiDelegate = context.coordinator
- return webview
+ webView.scrollView.bounces = false
+ webView.scrollView.alwaysBounceHorizontal = false
+ webView.scrollView.showsHorizontalScrollIndicator = false
+ webView.scrollView.isScrollEnabled = true
+ webView.configuration.suppressesIncrementalRendering = true
+ webView.isOpaque = false
+ webView.backgroundColor = .clear
+ webView.scrollView.backgroundColor = .white
+ webView.scrollView.alwaysBounceVertical = false
+ webView.scrollView.layer.cornerRadius = 24
+ webView.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+ webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0)
+
+ return webView
}
public func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext) {
if let url = URL(string: viewModel.url) {
- let cookies = HTTPCookieStorage.shared.cookies ?? []
- for (cookie) in cookies {
- webview.configuration.websiteDataStore.httpCookieStore
- .setCookie(cookie)
- }
- let request = URLRequest(url: url)
if webview.url?.absoluteString != url.absoluteString {
DispatchQueue.main.async {
isLoading = true
}
+ let request = URLRequest(url: url)
webview.load(request)
}
}
diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings
index 6fda66932..f16f781dc 100644
--- a/Core/Core/en.lproj/Localizable.strings
+++ b/Core/Core/en.lproj/Localizable.strings
@@ -21,6 +21,24 @@
"ERROR.UNKNOWN_ERROR" = "Something went wrong";
"ERROR.WIFI" = "You can only download files over Wi-Fi. You can change this in the settings.";
+"COURSEWARE.COURSE_CONTENT" = "Course content";
+"COURSEWARE.COURSE_UNITS" = "Course units";
+"COURSEWARE.NEXT" = "Next";
+"COURSEWARE.PREVIOUS" = "Prev";
+"COURSEWARE.FINISH" = "Finish";
+"COURSEWARE.GOOD_WORK" = "Good Work!";
+"COURSEWARE.BACK_TO_OUTLINE" = "Back to outline";
+"COURSEWARE.SECTION" = "Section “";
+"COURSEWARE.IS_FINISHED" = "“ is finished.";
+"COURSEWARE.CONTINUE" = "Continue";
+"COURSEWARE.CONTINUE_WITH" = "Continue with:";
+"COURSEWARE.NEXT_SECTION" = "Next section";
+
+"COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "To proceed with “";
+"COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST" = "” press “Next section”.";
+
+"ERROR.RELOAD" = "Reload";
+
"DATE.ENDED" = "Ended";
"DATE.START" = "Start";
"DATE.STARTED" = "Started";
@@ -47,3 +65,6 @@
"PICKER.SEARCH" = "Search";
"PICKER.ACCEPT" = "Accept";
+
+"WEBVIEW.ALERT.OK" = "Ok";
+"WEBVIEW.ALERT.CANCEL" = "Cancel";
diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings
index 430249a1d..e06937311 100644
--- a/Core/Core/uk.lproj/Localizable.strings
+++ b/Core/Core/uk.lproj/Localizable.strings
@@ -21,6 +21,24 @@
"ERROR.UNKNOWN_ERROR" = "Щось пішло не так";
"ERROR.WIFI" = "Завантажувати файли можна лише через Wi-Fi. Ви можете змінити це в налаштуваннях.";
+"COURSEWARE.COURSE_CONTENT" = "Зміст курсу";
+"COURSEWARE.COURSE_UNITS" = "Модулі";
+"COURSEWARE.NEXT" = "Далі";
+"COURSEWARE.PREVIOUS" = "Назад";
+"COURSEWARE.FINISH" = "Завершити";
+"COURSEWARE.GOOD_WORK" = "Гарна робота!";
+"COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля";
+"COURSEWARE.SECTION" = "Секція “";
+"COURSEWARE.IS_FINISHED" = "“ завершена.";
+"COURSEWARE.CONTINUE" = "Продовжити";
+"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:";
+"COURSEWARE.NEXT_SECTION" = "Наступний розділ";
+
+"COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "Щоб перейти до “";
+"COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST" = "” натисніть “Наступний розділ”.";
+
+"ERROR.RELOAD" = "Перезавантажити";
+
"DATE.ENDED" = "Кінець";
"DATE.START" = "Початок";
"DATE.STARTED" = "Почався";
@@ -47,3 +65,6 @@
"PICKER.SEARCH" = "Знайти";
"PICKER.ACCEPT" = "Прийняти";
+
+"WEBVIEW.ALERT.OK" = "Так";
+"WEBVIEW.ALERT.CANCEL" = "Скасувати";
diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj
index 7c1c46947..6a9324697 100644
--- a/Course/Course.xcodeproj/project.pbxproj
+++ b/Course/Course.xcodeproj/project.pbxproj
@@ -16,15 +16,22 @@
022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */; };
022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64E129ADEB83000F532B /* CourseUpdate.swift */; };
022EA8CB297AD63B0014A8F7 /* CourseContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022EA8CA297AD63B0014A8F7 /* CourseContainerViewModelTests.swift */; };
+ 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */; };
+ 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */; };
0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124C28EDA804002588FB /* CourseUnitView.swift */; };
0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */; };
023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */; };
023812E8297AC8EB0087098F /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0289F8EE28E1C3510064F8F3 /* Course.framework */; platformFilter = ios; };
023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812F2297AC9EC0087098F /* CourseMock.generated.swift */; };
- 0248C92529C0901200DC8402 /* CourseBlocksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */; };
+ 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454C9F2A2618E70043052A /* YouTubeView.swift */; };
+ 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA12A26190A0043052A /* EncodedVideoView.swift */; };
+ 02454CA42A26193F0043052A /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA32A26193F0043052A /* WebView.swift */; };
+ 02454CA62A26196C0043052A /* UnknownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA52A26196C0043052A /* UnknownView.swift */; };
+ 02454CA82A2619890043052A /* DiscussionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA72A2619890043052A /* DiscussionView.swift */; };
+ 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA92A2619B40043052A /* LessonProgressView.swift */; };
0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */; };
- 02512FEE298EAD770024D438 /* CourseBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FED298EAD770024D438 /* CourseBlocksView.swift */; };
0262148F29AE17C4008BD75A /* HandoutsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */; };
+ 02635AC72A24F181008062F2 /* ContinueWithView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02635AC62A24F181008062F2 /* ContinueWithView.swift */; };
0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0265B4B628E2141D00E6EAFD /* Strings.swift */; };
027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */; };
0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270210128E736E700F54332 /* CourseOutlineView.swift */; };
@@ -32,7 +39,6 @@
0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */; };
0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 0289F90128E1C3E00064F8F3 /* swiftgen.yml */; };
0295B1D9297E6DF8003B0C65 /* CourseUnitViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */; };
- 0295C887299BBDE300ABE571 /* UnitButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C886299BBDE300ABE571 /* UnitButtonView.swift */; };
0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C888299BBE8200ABE571 /* CourseNavigationView.swift */; };
02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A8076729474831007F53AB /* CourseVerticalView.swift */; };
02B6B3B228E1C49400232911 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02B6B3B428E1C49400232911 /* Localizable.strings */; };
@@ -79,15 +85,22 @@
022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_UpdatesResponse.swift; sourceTree = ""; };
022C64E129ADEB83000F532B /* CourseUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUpdate.swift; sourceTree = ""; };
022EA8CA297AD63B0014A8F7 /* CourseContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModelTests.swift; sourceTree = ""; };
+ 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideoPlayerViewModel.swift; sourceTree = ""; };
+ 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoPlayerViewModel.swift; sourceTree = ""; };
0231124C28EDA804002588FB /* CourseUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitView.swift; sourceTree = ""; };
0231124E28EDA811002588FB /* CourseUnitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModel.swift; sourceTree = ""; };
023812E4297AC8EA0087098F /* CourseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CourseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailsViewModelTests.swift; sourceTree = ""; };
023812F2297AC9EC0087098F /* CourseMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseMock.generated.swift; sourceTree = ""; };
- 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlocksViewModel.swift; sourceTree = ""; };
+ 02454C9F2A2618E70043052A /* YouTubeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeView.swift; sourceTree = ""; };
+ 02454CA12A26190A0043052A /* EncodedVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoView.swift; sourceTree = ""; };
+ 02454CA32A26193F0043052A /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; };
+ 02454CA52A26196C0043052A /* UnknownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownView.swift; sourceTree = ""; };
+ 02454CA72A2619890043052A /* DiscussionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionView.swift; sourceTree = ""; };
+ 02454CA92A2619B40043052A /* LessonProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonProgressView.swift; sourceTree = ""; };
0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalViewModel.swift; sourceTree = ""; };
- 02512FED298EAD770024D438 /* CourseBlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlocksView.swift; sourceTree = ""; };
0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsViewModelTests.swift; sourceTree = ""; };
+ 02635AC62A24F181008062F2 /* ContinueWithView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWithView.swift; sourceTree = ""; };
0265B4B628E2141D00E6EAFD /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; };
027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseOutlineResponse.swift; sourceTree = ""; };
0270210128E736E700F54332 /* CourseOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseOutlineView.swift; sourceTree = ""; };
@@ -96,7 +109,6 @@
0289F8EE28E1C3510064F8F3 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0289F90128E1C3E00064F8F3 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; };
0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModelTests.swift; sourceTree = ""; };
- 0295C886299BBDE300ABE571 /* UnitButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitButtonView.swift; sourceTree = ""; };
0295C888299BBE8200ABE571 /* CourseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseNavigationView.swift; sourceTree = ""; };
02A8076729474831007F53AB /* CourseVerticalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalView.swift; sourceTree = ""; };
02B6B3B328E1C49400232911 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
@@ -187,6 +199,19 @@
path = CourseTests;
sourceTree = "";
};
+ 02454C9E2A2618D40043052A /* Subviews */ = {
+ isa = PBXGroup;
+ children = (
+ 02454C9F2A2618E70043052A /* YouTubeView.swift */,
+ 02454CA12A26190A0043052A /* EncodedVideoView.swift */,
+ 02454CA32A26193F0043052A /* WebView.swift */,
+ 02454CA52A26196C0043052A /* UnknownView.swift */,
+ 02454CA72A2619890043052A /* DiscussionView.swift */,
+ 02454CA92A2619B40043052A /* LessonProgressView.swift */,
+ );
+ path = Subviews;
+ sourceTree = "";
+ };
0289F8E428E1C3510064F8F3 = {
isa = PBXGroup;
children = (
@@ -325,11 +350,10 @@
070019A728F6F2D600D5FC78 /* Outline */ = {
isa = PBXGroup;
children = (
+ 02635AC62A24F181008062F2 /* ContinueWithView.swift */,
0270210128E736E700F54332 /* CourseOutlineView.swift */,
02A8076729474831007F53AB /* CourseVerticalView.swift */,
0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */,
- 02512FED298EAD770024D438 /* CourseBlocksView.swift */,
- 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */,
);
path = Outline;
sourceTree = "";
@@ -347,10 +371,10 @@
070019A928F6F59D00D5FC78 /* Unit */ = {
isa = PBXGroup;
children = (
+ 02454C9E2A2618D40043052A /* Subviews */,
0231124C28EDA804002588FB /* CourseUnitView.swift */,
0231124E28EDA811002588FB /* CourseUnitViewModel.swift */,
0295C888299BBE8200ABE571 /* CourseNavigationView.swift */,
- 0295C886299BBDE300ABE571 /* UnitButtonView.swift */,
);
path = Unit;
sourceTree = "";
@@ -360,7 +384,9 @@
children = (
02F066E729DC71750073E13B /* SubtittlesView.swift */,
0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */,
+ 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */,
0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */,
+ 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */,
0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */,
02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */,
);
@@ -645,39 +671,45 @@
buildActionMask = 2147483647;
files = (
02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */,
+ 02454CA42A26193F0043052A /* WebView.swift in Sources */,
022C64DA29ACEC50000F532B /* HandoutsViewModel.swift in Sources */,
+ 02635AC72A24F181008062F2 /* ContinueWithView.swift in Sources */,
022C64DE29AD167A000F532B /* HandoutsUpdatesDetailView.swift in Sources */,
0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */,
022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */,
+ 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */,
+ 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */,
02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */,
02280F60294B50030032823A /* CoursePersistence.swift in Sources */,
+ 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */,
02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */,
0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */,
0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */,
0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */,
02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */,
+ 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */,
0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */,
- 0295C887299BBDE300ABE571 /* UnitButtonView.swift in Sources */,
02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */,
02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */,
073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */,
- 02512FEE298EAD770024D438 /* CourseBlocksView.swift in Sources */,
0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */,
02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */,
02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */,
- 0248C92529C0901200DC8402 /* CourseBlocksViewModel.swift in Sources */,
0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */,
027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */,
02E685C028E4B629000AE015 /* CourseDetailsViewModel.swift in Sources */,
0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */,
02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */,
022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */,
+ 02454CA62A26196C0043052A /* UnknownView.swift in Sources */,
0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */,
022C64DC29ACFDEE000F532B /* Data_HandoutsResponse.swift in Sources */,
022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */,
+ 02454CA82A2619890043052A /* DiscussionView.swift in Sources */,
0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */,
02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */,
0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */,
+ 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */,
02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */,
02B6B3BE28E1D15C00232911 /* CourseDetailsEndpoint.swift in Sources */,
02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */,
@@ -714,7 +746,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -735,7 +767,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -756,7 +788,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -777,7 +809,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -798,7 +830,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -819,7 +851,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -1512,7 +1544,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
@@ -1625,7 +1657,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = KHU94Q2JSS;
+ DEVELOPMENT_TEAM = L8PG7LC3Y3;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.6;
diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift
index e7cbcd9e7..30f3555df 100644
--- a/Course/Course/Data/CourseRepository.swift
+++ b/Course/Course/Data/CourseRepository.swift
@@ -18,7 +18,7 @@ public protocol CourseRepositoryProtocol {
func getHandouts(courseID: String) async throws -> String?
func getUpdates(courseID: String) async throws -> [CourseUpdate]
func resumeBlock(courseID: String) async throws -> ResumeBlock
- func getSubtitles(url: String) async throws -> String
+ func getSubtitles(url: String, selectedLanguage: String) async throws -> String
}
public class CourseRepository: CourseRepositoryProtocol {
@@ -98,13 +98,16 @@ public class CourseRepository: CourseRepositoryProtocol {
.mapResponse(DataLayer.ResumeBlock.self).domain
}
- public func getSubtitles(url: String) async throws -> String {
- if let subtitlesOffline = persistence.loadSubtitles(url: url) {
+ public func getSubtitles(url: String, selectedLanguage: String) async throws -> String {
+ if let subtitlesOffline = persistence.loadSubtitles(url: url + selectedLanguage) {
return subtitlesOffline
} else {
- let result = try await api.requestData(CourseDetailsEndpoint.getSubtitles(url: url))
+ let result = try await api.requestData(CourseDetailsEndpoint.getSubtitles(
+ url: url,
+ selectedLanguage: selectedLanguage
+ ))
let subtitles = String(data: result, encoding: .utf8) ?? ""
- persistence.saveSubtitles(url: url, subtitlesString: subtitles)
+ persistence.saveSubtitles(url: url + selectedLanguage, subtitlesString: subtitles)
return subtitles
}
}
@@ -241,7 +244,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol {
let decoder = JSONDecoder()
let jsonData = Data(courseStructureJson.utf8)
let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData)
- return parseCourseStructure(courseBlocks: courseBlocks)
+ return parseCourseStructure(structure: courseBlocks)
}
public func getCourseDetails(courseID: String) async throws -> CourseDetails {
@@ -263,10 +266,12 @@ class CourseRepositoryMock: CourseRepositoryProtocol {
public func getCourseBlocks(courseID: String) async throws -> CourseStructure {
do {
- let decoder = JSONDecoder()
- let jsonData = Data(courseStructureJson.utf8)
- let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData)
- return parseCourseStructure(courseBlocks: courseBlocks)
+// let decoder = JSONDecoder()
+// let jsonData = Data(courseStructureJson.utf8)
+ let courseBlocks = try courseStructureJson.data(using: .utf8)!.mapResponse(DataLayer.CourseStructure.self)
+
+// let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData)
+ return parseCourseStructure(structure: courseBlocks)
} catch {
throw error
}
@@ -280,7 +285,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol {
}
- public func getSubtitles(url: String) async throws -> String {
+ public func getSubtitles(url: String, selectedLanguage: String) async throws -> String {
return """
0
00:00:00,350 --> 00:00:05,230
@@ -303,8 +308,8 @@ And there are various ways of describing it-- call it oral poetry or
"""
}
- private func parseCourseStructure(courseBlocks: DataLayer.CourseStructure) -> CourseStructure {
- let blocks = Array(courseBlocks.dict.values)
+ private func parseCourseStructure(structure: DataLayer.CourseStructure) -> CourseStructure {
+ let blocks = Array(structure.dict.values)
let course = blocks.first(where: {$0.type == BlockType.course.rawValue })!
let descendants = course.descendants ?? []
var childs: [CourseChapter] = []
@@ -321,8 +326,8 @@ And there are various ways of describing it-- call it oral poetry or
displayName: course.displayName,
topicID: course.userViewData?.topicID,
childs: childs,
- media: courseBlocks.media,
- certificate: courseBlocks.certificate?.domain)
+ media: structure.media,
+ certificate: structure.certificate?.domain)
}
private func parseChapters(id: String, blocks: [DataLayer.CourseBlock]) -> CourseChapter {
@@ -375,11 +380,12 @@ And there are various ways of describing it-- call it oral poetry or
private func parseBlock(id: String, blocks: [DataLayer.CourseBlock]) -> CourseBlock {
let block = blocks.first(where: {$0.id == id })!
let subtitles = block.userViewData?.transcripts?.map {
-// let url = $0.value
+ let url = $0.value
// .replacingOccurrences(of: config.baseURL.absoluteString, with: "")
-// .replacingOccurrences(of: "?lang=en", with: "")
- SubtitleUrl(language: $0.key, url: $0.value)
+// .replacingOccurrences(of: "?lang=\($0.key)", with: "")
+ return SubtitleUrl(language: $0.key, url: url)
}
+
return CourseBlock(blockId: block.blockId,
id: block.id,
topicId: block.userViewData?.topicID,
@@ -393,443 +399,627 @@ And there are various ways of describing it-- call it oral poetry or
youTubeUrl: block.userViewData?.encodedVideo?.youTube?.url)
}
- private let courseStructureJson: String = "{\n" +
- " \"root\": \"block-v1:RG+MC01+2022+type@course+block@course\",\n" +
- " \"blocks\": {\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" +
- " \"block_id\": \"8718fdf95d584d198a3b17c0d2611139\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" +
- " \"type\": \"html\",\n" +
- " \"display_name\": \"Text\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_data\": {\n" +
- " \"enabled\": false,\n" +
- " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" +
- " },\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" +
- " \"block_id\": \"d1bb8c9e6ed44b708ea54cacf67b650a\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" +
- " \"type\": \"video\",\n" +
- " \"display_name\": \"Video\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_data\": {\n" +
- " \"only_on_web\": false,\n" +
- " \"duration\": null,\n" +
- " \"transcripts\": {\n" +
- " \"en\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/xblock/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a/handler_noauth/transcript/download?lang=en\"\n" +
- " },\n" +
- " \"encoded_videos\": {\n" +
- " \"youtube\": {\n" +
- " \"url\": \"https://www.youtube.com/watch?v=3_yD_cEKoCk\",\n" +
- " \"file_size\": 0\n" +
- " }\n" +
- " },\n" +
- " \"all_sources\": []\n" +
- " },\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 1\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 0.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" +
- " \"block_id\": \"8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" +
- " \"type\": \"vertical\",\n" +
- " \"display_name\": \"Welcome!\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 1\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" +
- " \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" +
- " \"block_id\": \"5735347ae4be44d5b184728661d79bb4\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" +
- " \"type\": \"html\",\n" +
- " \"display_name\": \"Text\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_data\": {\n" +
- " \"enabled\": false,\n" +
- " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" +
- " },\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" +
- " \"block_id\": \"0b26805b246c44148a2c02dfbffa2b27\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" +
- " \"type\": \"discussion\",\n" +
- " \"display_name\": \"Discussion\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_data\": {\n" +
- " \"topic_id\": \"035315aac3f889b472c8f051d8fd0abaa99682de\"\n" +
- " },\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": []\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" +
- " \"block_id\": \"890277efe17a42a185c68b8ba8fc5a98\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" +
- " \"type\": \"vertical\",\n" +
- " \"display_name\": \"General Info\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" +
- " \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" +
- " \"block_id\": \"45b174bf007b4d86a3a265d996565883\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" +
- " \"type\": \"sequential\",\n" +
- " \"display_name\": \"Course Intro\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 1\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" +
- " \"block_id\": \"7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" +
- " \"type\": \"chapter\",\n" +
- " \"display_name\": \"Info Section\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 1\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" +
- " \"block_id\": \"376ec419a01449fd86c2d11c8054d0be\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" +
- " \"type\": \"problem\",\n" +
- " \"display_name\": \"Checkboxes\",\n" +
- " \"graded\": true,\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" +
- " \"block_id\": \"ebc2d20fad364992b13fff49fc53d7cf\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" +
- " \"type\": \"problem\",\n" +
- " \"display_name\": \"Dropdown\",\n" +
- " \"graded\": true,\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" +
- " \"block_id\": \"6b822c82f2ca4b049ee380a1cf65396b\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" +
- " \"type\": \"problem\",\n" +
- " \"display_name\": \"Numerical Input with Hints and Feedback\",\n" +
- " \"graded\": true,\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" +
- " \"block_id\": \"009da5f764a04078855d322e205c5863\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" +
- " \"type\": \"problem\",\n" +
- " \"display_name\": \"Multiple Choice\",\n" +
- " \"graded\": true,\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" +
- " \"block_id\": \"e34d9616cbaa45d1a6986a687c49f5c4\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" +
- " \"type\": \"vertical\",\n" +
- " \"display_name\": \"Common Problems\",\n" +
- " \"graded\": true,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" +
- " \"block_id\": \"ac7862e8c3c9481bbe657a82795def56\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" +
- " \"type\": \"sequential\",\n" +
- " \"display_name\": \"Test\",\n" +
- " \"graded\": true,\n" +
- " \"format\": \"Final Exam\",\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" +
- " \"block_id\": \"9355144723fc4270a1081547fd8bdd3d\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" +
- " \"type\": \"problem\",\n" +
- " \"display_name\": \"Image Mapped Input\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 0.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" +
- " \"block_id\": \"50d42e9c9d91451fb50693e01b9e4340\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" +
- " \"type\": \"vertical\",\n" +
- " \"display_name\": \"Advanced Problems\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" +
- " \"block_id\": \"5cdb10d7d0e9498faba55450173e23be\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" +
- " \"type\": \"sequential\",\n" +
- " \"display_name\": \"X-blocks not supported in app\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" +
- " \"block_id\": \"8f208b5d63234ce483f7d6702c46238a\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" +
- " \"type\": \"chapter\",\n" +
- " \"display_name\": \"Problems\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" +
- " \"block_id\": \"4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" +
- " \"type\": \"html\",\n" +
- " \"display_name\": \"Text\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_data\": {\n" +
- " \"enabled\": false,\n" +
- " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" +
- " },\n" +
- " \"student_view_multi_device\": true,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [],\n" +
- " \"completion\": 1.0\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" +
- " \"block_id\": \"b912f9ba42ac43c492bfb423e15b0da1\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" +
- " \"type\": \"vertical\",\n" +
- " \"display_name\": \"Thank you\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" +
- " \"block_id\": \"a6e2101867234019b60607a9b9bf64f9\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" +
- " \"type\": \"sequential\",\n" +
- " \"display_name\": \"Thank you note\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" +
- " \"block_id\": \"29f4043d199e46ef95d437da3be1d222\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" +
- " \"type\": \"chapter\",\n" +
- " \"display_name\": \"Fin\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 0\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\"\n" +
- " ],\n" +
- " \"completion\": 1\n" +
- " },\n" +
- " \"block-v1:RG+MC01+2022+type@course+block@course\": {\n" +
- " \"id\": \"block-v1:RG+MC01+2022+type@course+block@course\",\n" +
- " \"block_id\": \"course\",\n" +
- " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@course+block@course\",\n" +
- " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@course+block@course?experience=legacy\",\n" +
- " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@course+block@course\",\n" +
- " \"type\": \"course\",\n" +
- " \"display_name\": \"Mobile Course Demo\",\n" +
- " \"graded\": false,\n" +
- " \"student_view_multi_device\": false,\n" +
- " \"block_counts\": {\n" +
- " \"video\": 1\n" +
- " },\n" +
- " \"descendants\": [\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" +
- " \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\"\n" +
- " ],\n" +
- " \"completion\": 0\n" +
- " }\n" +
- " }\n" +
- "}"
+ private let courseStructureJson: String = """
+ {"root": "block-v1:QA+comparison+2022+type@course+block@course",
+ "blocks": {
+ "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78": {
+ "id": "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78",
+ "block_id": "be1704c576284ba39753c6f0ea4a4c78",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78",
+ "type": "comparison",
+ "display_name": "Співставлення",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296",
+ "block_id": "93acc543871e4c73bc20a72a64e93296",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296",
+ "type": "problem",
+ "display_name": "Dropdown with Hints and Feedback",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b": {
+ "id": "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b",
+ "block_id": "06c17035106e48328ebcd042babcf47b",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b",
+ "type": "comparison",
+ "display_name": "Співставлення",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58",
+ "block_id": "c19e41b61db14efe9c45f1354332ae58",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58",
+ "type": "problem",
+ "display_name": "Text Input with Hints and Feedback",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb",
+ "block_id": "0d96732f577b4ff68799faf8235d1bfb",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb",
+ "type": "problem",
+ "display_name": "Numerical Input with Hints and Feedback",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96",
+ "block_id": "dd2e22fdf0724bd88c8b2e6b68dedd96",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96",
+ "type": "problem",
+ "display_name": "Blank Common Problem",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd",
+ "block_id": "d1e091aa305741c5bedfafed0d269efd",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd",
+ "type": "problem",
+ "display_name": "Blank Common Problem",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7": {
+ "id": "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7",
+ "block_id": "23e10dea806345b19b77997b4fc0eea7",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7",
+ "type": "comparison",
+ "display_name": "Співставлення",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904": {
+ "id": "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904",
+ "block_id": "29e7eddbe8964770896e4036748c9904",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904",
+ "type": "vertical",
+ "display_name": "Юніт",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78",
+ "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296",
+ "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b",
+ "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58",
+ "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb",
+ "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96",
+ "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd",
+ "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6": {
+ "id": "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6",
+ "block_id": "f468bb5c6e8641179e523c7fcec4e6d6",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6",
+ "type": "sequential",
+ "display_name": "Підрозділ",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234",
+ "block_id": "eaf91d8fc70547339402043ba1a1c234",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234",
+ "type": "problem",
+ "display_name": "Dropdown with Hints and Feedback",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751": {
+ "id": "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751",
+ "block_id": "fac531c3f1f3400cb8e3b97eb2c3d751",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751",
+ "type": "comparison",
+ "display_name": "Comparison",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de": {
+ "id": "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de",
+ "block_id": "74a1074024fe401ea305534f2241e5de",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de",
+ "type": "html",
+ "display_name": "Raw HTML",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-05-04T19:08:07Z",
+ "html_data": "https://s3.eu-central-1.amazonaws.com/vso-dev-edx-sorage/htmlxblock/QA/comparison/html/74a1074024fe401ea305534f2241e5de/content_html.zip",
+ "size": 576,
+ "index_page": "index.html",
+ "icon_class": "other"
+ },
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca": {
+ "id": "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca",
+ "block_id": "e5b2e105f4f947c5b76fb12c35da1eca",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca",
+ "type": "vertical",
+ "display_name": "Юніт",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234",
+ "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751",
+ "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2": {
+ "id": "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2",
+ "block_id": "d37cb0c5c2d24ddaacf3494760a055f2",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2",
+ "type": "sequential",
+ "display_name": "Ще один Підрозділ",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846": {
+ "id": "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846",
+ "block_id": "abecaefe203c4c93b441d16cea3b7846",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846",
+ "type": "chapter",
+ "display_name": "Розділ",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6",
+ "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de",
+ "block_id": "a0c3ac29daab425f92a34b34eb2af9de",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de",
+ "type": "pdf",
+ "display_name": "PDF файл заголовок",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9",
+ "block_id": "bcd1b0f3015b4d3696b12f65a5d682f9",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9",
+ "type": "pdf",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59",
+ "block_id": "67d805daade34bd4b6ace607e6d48f59",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59",
+ "type": "pdf",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974",
+ "block_id": "828606a51f4e44198e92f86a45be7974",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974",
+ "type": "pdf",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee",
+ "block_id": "8646c3bc2184467b86e5ef01ecd452ee",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee",
+ "type": "pdf",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1": {
+ "id": "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1",
+ "block_id": "e2faa0e62223489e91a41700865c5fc1",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1",
+ "type": "vertical",
+ "display_name": "Юніт",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de",
+ "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9",
+ "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59",
+ "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974",
+ "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52": {
+ "id": "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52",
+ "block_id": "0c5e89fa6d7a4fac8f7b26f2ca0bbe52",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52",
+ "type": "problem",
+ "display_name": "Checkboxes with Hints and Feedback",
+ "graded": false,
+ "student_view_multi_device": true,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4": {
+ "id": "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4",
+ "block_id": "8ba437d8b20d416d91a2d362b0c940a4",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4",
+ "type": "vertical",
+ "display_name": "Юніт",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d": {
+ "id": "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d",
+ "block_id": "021f70794f7349998e190b060260b70d",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d",
+ "type": "pdf",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_data": {
+ "last_modified": "2023-04-26T08:43:45Z",
+ "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf",
+ },
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ],
+ "completion": 0.0
+ },
+ "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1": {
+ "id": "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1",
+ "block_id": "2c344115d3554ac58c140ec86e591aa1",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1",
+ "type": "vertical",
+ "display_name": "Юніт",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe": {
+ "id": "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe",
+ "block_id": "6c9c6ba663b54c0eb9cbdcd0c6b4bebe",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe",
+ "type": "sequential",
+ "display_name": "Підрозділ",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1",
+ "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4",
+ "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7": {
+ "id": "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7",
+ "block_id": "d5a4f1f2f5314288aae400c270fb03f7",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7",
+ "type": "chapter",
+ "display_name": "PDF",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe"
+ ],
+ "completion": 0
+ },
+ "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf": {
+ "id": "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf",
+ "block_id": "7ab45affb80f4846a60648ec6aff9fbf",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf",
+ "type": "chapter",
+ "display_name": "Розділ",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+
+ ]
+ },
+ "block-v1:QA+comparison+2022+type@course+block@course": {
+ "id": "block-v1:QA+comparison+2022+type@course+block@course",
+ "block_id": "course",
+ "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course",
+ "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course?experience=legacy",
+ "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@course+block@course",
+ "type": "course",
+ "display_name": "Comparison xblock test coursre",
+ "graded": false,
+ "student_view_multi_device": false,
+ "block_counts": {
+ "video": 0
+ },
+ "descendants": [
+ "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846",
+ "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7",
+ "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf"
+ ],
+ "completion": 0
+ }
+ },
+ "id": "course-v1:QA+comparison+2022",
+ "name": "Comparison xblock test coursre",
+ "number": "comparison",
+ "org": "QA",
+ "start": "2022-01-01T00:00:00Z",
+ "start_display": "01 січня 2022 р.",
+ "start_type": "timestamp",
+ "end": null,
+ "courseware_access": {
+ "has_access": true,
+ "error_code": null,
+ "developer_message": null,
+ "user_message": null,
+ "additional_context_user_message": null,
+ "user_fragment": null
+ },
+ "media": {
+ "image": {
+ "raw": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg",
+ "small": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg",
+ "large": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg"
+ }
+ },
+ "certificate": {
+
+ },
+ "is_self_paced": false
+ }
+ """
}
#endif
diff --git a/Course/Course/Data/Network/CourseDetailsEndpoint.swift b/Course/Course/Data/Network/CourseDetailsEndpoint.swift
index 3207fc4ff..57bb2459f 100644
--- a/Course/Course/Data/Network/CourseDetailsEndpoint.swift
+++ b/Course/Course/Data/Network/CourseDetailsEndpoint.swift
@@ -18,7 +18,7 @@ enum CourseDetailsEndpoint: EndPointType {
case getHandouts(courseID: String)
case getUpdates(courseID: String)
case resumeBlock(userName: String, courseID: String)
- case getSubtitles(url: String)
+ case getSubtitles(url: String, selectedLanguage: String)
var path: String {
switch self {
@@ -38,7 +38,7 @@ enum CourseDetailsEndpoint: EndPointType {
return "/api/mobile/v1/course_info/\(courseID)/updates"
case let .resumeBlock(userName, courseID):
return "/api/mobile/v1/users/\(userName)/course_status_info/\(courseID)"
- case .getSubtitles(url: let url):
+ case let .getSubtitles(url, _):
return url
}
}
@@ -111,10 +111,10 @@ enum CourseDetailsEndpoint: EndPointType {
return .requestParameters(encoding: JSONEncoding.default)
case .resumeBlock:
return .requestParameters(encoding: JSONEncoding.default)
- case .getSubtitles:
- let languageCode = Locale.current.languageCode ?? "en"
+ case let .getSubtitles(_, subtitleLanguage):
+// let languageCode = Locale.current.languageCode ?? "en"
let params: [String: Any] = [
- "lang": languageCode
+ "lang": subtitleLanguage
]
return .requestParameters(parameters: params, encoding: URLEncoding.queryString)
}
diff --git a/Course/Course/Data/Persistence/CoursePersistence.swift b/Course/Course/Data/Persistence/CoursePersistence.swift
index 2d5531b00..1a9731e12 100644
--- a/Course/Course/Data/Persistence/CoursePersistence.swift
+++ b/Course/Course/Data/Persistence/CoursePersistence.swift
@@ -156,14 +156,19 @@ public class CoursePersistence: CoursePersistenceProtocol {
result[block.id] = block
} ?? [:]
- return DataLayer.CourseStructure(rootItem: structure.rootItem ?? "",
- dict: dictionary,
- id: structure.id ?? "",
- media: DataLayer.CourseMedia(image:
- DataLayer.Image(raw: structure.mediaRaw ?? "",
- small: structure.mediaSmall ?? "",
- large: structure.mediaLarge ?? "")),
- certificate: DataLayer.Certificate(url: structure.certificate))
+ return DataLayer.CourseStructure(
+ rootItem: structure.rootItem ?? "",
+ dict: dictionary,
+ id: structure.id ?? "",
+ media: DataLayer.CourseMedia(
+ image: DataLayer.Image(
+ raw: structure.mediaRaw ?? "",
+ small: structure.mediaSmall ?? "",
+ large: structure.mediaLarge ?? ""
+ )
+ ),
+ certificate: DataLayer.Certificate(url: structure.certificate)
+ )
}
public func saveCourseStructure(structure: DataLayer.CourseStructure) {
diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift
index 87eda8281..abdbd40d4 100644
--- a/Course/Course/Domain/CourseInteractor.swift
+++ b/Course/Course/Domain/CourseInteractor.swift
@@ -20,7 +20,7 @@ public protocol CourseInteractorProtocol {
func getHandouts(courseID: String) async throws -> String?
func getUpdates(courseID: String) async throws -> [CourseUpdate]
func resumeBlock(courseID: String) async throws -> ResumeBlock
- func getSubtitles(url: String) async throws -> [Subtitle]
+ func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle]
}
public class CourseInteractor: CourseInteractorProtocol {
@@ -89,8 +89,8 @@ public class CourseInteractor: CourseInteractorProtocol {
return try await repository.resumeBlock(courseID: courseID)
}
- public func getSubtitles(url: String) async throws -> [Subtitle] {
- let result = try await repository.getSubtitles(url: url)
+ public func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] {
+ let result = try await repository.getSubtitles(url: url, selectedLanguage: selectedLanguage)
return parseSubtitles(from: result)
}
diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift
index 78dd8ca7f..9c698850c 100644
--- a/Course/Course/Presentation/Container/CourseContainerView.swift
+++ b/Course/Course/Presentation/Container/CourseContainerView.swift
@@ -98,6 +98,11 @@ public struct CourseContainerView: View {
.introspectViewController { vc in
vc.navigationController?.setNavigationBarHidden(true, animated: false)
}
+ .onFirstAppear {
+ Task {
+ await viewModel.tryToRefreshCookies()
+ }
+ }
}
}
}
@@ -109,6 +114,7 @@ struct CourseScreensView_Previews: PreviewProvider {
CourseContainerView(
viewModel: CourseContainerViewModel(
interactor: CourseInteractor.mock,
+ authInteractor: AuthInteractor.mock,
router: CourseRouterMock(),
config: ConfigMock(),
connectivity: Connectivity(),
diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
index 4ad87108e..c13b68edc 100644
--- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift
+++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
@@ -17,7 +17,7 @@ public class CourseContainerViewModel: BaseCourseViewModel {
@Published private(set) var isShowProgress = false
@Published var showError: Bool = false
@Published var downloadState: [String: DownloadViewState] = [:]
- @Published var returnCourseSequential: CourseSequential?
+ @Published var continueWith: ContinueWith?
var errorMessage: String? {
didSet {
@@ -27,29 +27,33 @@ public class CourseContainerViewModel: BaseCourseViewModel {
}
}
- public let interactor: CourseInteractorProtocol
- public let router: CourseRouter
- public let config: Config
- public let connectivity: ConnectivityProtocol
+ private let interactor: CourseInteractorProtocol
+ private let authInteractor: AuthInteractorProtocol
+ let router: CourseRouter
+ let config: Config
+ let connectivity: ConnectivityProtocol
- public let isActive: Bool?
- public let courseStart: Date?
- public let courseEnd: Date?
- public let enrollmentStart: Date?
- public let enrollmentEnd: Date?
+ let isActive: Bool?
+ let courseStart: Date?
+ let courseEnd: Date?
+ let enrollmentStart: Date?
+ let enrollmentEnd: Date?
- public init(interactor: CourseInteractorProtocol,
- router: CourseRouter,
- config: Config,
- connectivity: ConnectivityProtocol,
- manager: DownloadManagerProtocol,
- isActive: Bool?,
- courseStart: Date?,
- courseEnd: Date?,
- enrollmentStart: Date?,
- enrollmentEnd: Date?
+ public init(
+ interactor: CourseInteractorProtocol,
+ authInteractor: AuthInteractorProtocol,
+ router: CourseRouter,
+ config: Config,
+ connectivity: ConnectivityProtocol,
+ manager: DownloadManagerProtocol,
+ isActive: Bool?,
+ courseStart: Date?,
+ courseEnd: Date?,
+ enrollmentStart: Date?,
+ enrollmentEnd: Date?
) {
self.interactor = interactor
+ self.authInteractor = authInteractor
self.router = router
self.config = config
self.connectivity = connectivity
@@ -72,7 +76,7 @@ public class CourseContainerViewModel: BaseCourseViewModel {
}
@MainActor
- public func getCourseBlocks(courseID: String, withProgress: Bool = true) async {
+ func getCourseBlocks(courseID: String, withProgress: Bool = true) async {
if let courseStart {
if courseStart < Date() {
isShowProgress = withProgress
@@ -81,10 +85,10 @@ public class CourseContainerViewModel: BaseCourseViewModel {
courseStructure = try await interactor.getCourseBlocks(courseID: courseID)
isShowProgress = false
if let courseStructure {
- let returnCourseSequential = try await getResumeBlock(courseID: courseID,
- courseStructure: courseStructure)
+ let continueWith = try await getResumeBlock(courseID: courseID,
+ courseStructure: courseStructure)
withAnimation {
- self.returnCourseSequential = returnCourseSequential
+ self.continueWith = continueWith
}
}
} else {
@@ -107,10 +111,17 @@ public class CourseContainerViewModel: BaseCourseViewModel {
}
@MainActor
- private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> CourseSequential? {
+ func tryToRefreshCookies() async {
+ try? await authInteractor.getCookies(force: false)
+ }
+
+ @MainActor
+ private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> ContinueWith? {
let result = try await interactor.resumeBlock(courseID: courseID)
- return findCourseSequential(blockID: result.blockID,
- courseStructure: courseStructure)
+ return findContinueVertical(
+ blockID: result.blockID,
+ courseStructure: courseStructure
+ )
}
func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) {
@@ -174,14 +185,19 @@ public class CourseContainerViewModel: BaseCourseViewModel {
}
}
- private func findCourseSequential(blockID: String, courseStructure: CourseStructure) -> CourseSequential? {
- for chapter in courseStructure.childs {
- for sequential in chapter.childs {
- for vertical in sequential.childs {
- for block in vertical.childs {
- if block.id == blockID {
- return sequential
- }
+ private func findContinueVertical(blockID: String, courseStructure: CourseStructure) -> ContinueWith? {
+ for chapterIndex in courseStructure.childs.indices {
+ let chapter = courseStructure.childs[chapterIndex]
+ for sequentialIndex in chapter.childs.indices {
+ let sequential = chapter.childs[sequentialIndex]
+ for verticalIndex in sequential.childs.indices {
+ let vertical = sequential.childs[verticalIndex]
+ for block in vertical.childs where block.id == blockID {
+ return ContinueWith(
+ chapterIndex: chapterIndex,
+ sequentialIndex: sequentialIndex,
+ verticalIndex: verticalIndex
+ )
}
}
}
diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift
index 83e1181ba..58f5944c6 100644
--- a/Course/Course/Presentation/CourseRouter.swift
+++ b/Course/Course/Presentation/CourseRouter.swift
@@ -10,32 +10,52 @@ import Core
public protocol CourseRouter: BaseRouter {
- func showCourseScreens(courseID: String,
- isActive: Bool?,
- courseStart: Date?,
- courseEnd: Date?,
- enrollmentStart: Date?,
- enrollmentEnd: Date?,
- title: String)
+ func showCourseScreens(
+ courseID: String,
+ isActive: Bool?,
+ courseStart: Date?,
+ courseEnd: Date?,
+ enrollmentStart: Date?,
+ enrollmentEnd: Date?,
+ title: String
+ )
- func showCourseUnit(blockId: String,
- courseID: String,
- sectionName: String,
- blocks: [CourseBlock])
+ func showCourseUnit(
+ id: String,
+ blockId: String,
+ courseID: String,
+ sectionName: String,
+ verticalIndex: Int,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ )
- func showCourseVerticalView(title: String,
- verticals: [CourseVertical])
+ func replaceCourseUnit(
+ id: String,
+ blockId: String,
+ courseID: String,
+ sectionName: String,
+ verticalIndex: Int,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ )
- func showCourseBlocksView(title: String,
- blocks: [CourseBlock])
+ func showCourseVerticalView(
+ id: String,
+ title: String,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ )
- func showCourseVerticalAndBlocksView(verticals: (String, [CourseVertical]),
- blocks: (String, [CourseBlock]))
-
- func showHandoutsUpdatesView(handouts: String?,
- announcements: [CourseUpdate]?,
- router: Course.CourseRouter,
- cssInjector: CSSInjector)
+ func showHandoutsUpdatesView(
+ handouts: String?,
+ announcements: [CourseUpdate]?,
+ router: Course.CourseRouter,
+ cssInjector: CSSInjector
+ )
}
// Mark - For testing and SwiftUI preview
@@ -44,32 +64,52 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter {
public override init() {}
- public func showCourseScreens(courseID: String,
- isActive: Bool?,
- courseStart: Date?,
- courseEnd: Date?,
- enrollmentStart: Date?,
- enrollmentEnd: Date?,
- title: String) {}
-
- public func showCourseUnit(blockId: String,
- courseID: String,
- sectionName: String,
- blocks: [CourseBlock]) {}
+ public func showCourseScreens(
+ courseID: String,
+ isActive: Bool?,
+ courseStart: Date?,
+ courseEnd: Date?,
+ enrollmentStart: Date?,
+ enrollmentEnd: Date?,
+ title: String
+ ) {}
- public func showCourseVerticalView(title: String,
- verticals: [CourseVertical]) {}
+ public func showCourseUnit(
+ id: String,
+ blockId: String,
+ courseID: String,
+ sectionName: String,
+ verticalIndex: Int,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ ) {}
- public func showCourseBlocksView(title: String,
- blocks: [CourseBlock]) {}
+ public func replaceCourseUnit(
+ id: String,
+ blockId: String,
+ courseID: String,
+ sectionName: String,
+ verticalIndex: Int,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ ) {}
- public func showCourseVerticalAndBlocksView(verticals: (String, [CourseVertical]),
- blocks: (String, [CourseBlock])) {}
+ public func showCourseVerticalView(
+ id: String,
+ title: String,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int
+ ) {}
- public func showHandoutsUpdatesView(handouts: String?,
- announcements: [CourseUpdate]?,
- router: Course.CourseRouter,
- cssInjector: CSSInjector) {}
+ public func showHandoutsUpdatesView(
+ handouts: String?,
+ announcements: [CourseUpdate]?,
+ router: Course.CourseRouter,
+ cssInjector: CSSInjector
+ ) {}
}
#endif
diff --git a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift
index 6f3e7c8d2..d0b0d4216 100644
--- a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift
+++ b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift
@@ -33,13 +33,15 @@ public class CourseDetailsViewModel: ObservableObject {
let router: CourseRouter
let config: Config
let cssInjector: CSSInjector
- public let connectivity: ConnectivityProtocol
+ let connectivity: ConnectivityProtocol
- public init(interactor: CourseInteractorProtocol,
- router: CourseRouter,
- config: Config,
- cssInjector: CSSInjector,
- connectivity: ConnectivityProtocol) {
+ public init(
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ config: Config,
+ cssInjector: CSSInjector,
+ connectivity: ConnectivityProtocol
+ ) {
self.interactor = interactor
self.router = router
self.config = config
@@ -56,7 +58,7 @@ public class CourseDetailsViewModel: ObservableObject {
if let isEnrolled = courseDetails?.isEnrolled {
self.courseDetails?.isEnrolled = isEnrolled
}
-
+
isShowProgress = false
} else {
courseDetails = try await interactor.getCourseDetailsOffline(courseID: courseID)
@@ -98,7 +100,7 @@ public class CourseDetailsViewModel: ObservableObject {
guard let url = URL(string: httpsURL) else { return }
UIApplication.shared.open(url)
}
-
+
@MainActor
func enrollToCourse(id: String) async {
do {
diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift
index 179e6b55f..598018893 100644
--- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift
+++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift
@@ -20,7 +20,12 @@ public struct HandoutsUpdatesDetailView: View {
private let title: String
@State private var height: [Int: CGFloat] = [:]
- public init(handouts: String?, announcements: [CourseUpdate]?, router: CourseRouter, cssInjector: CSSInjector) {
+ public init(
+ handouts: String?,
+ announcements: [CourseUpdate]?,
+ router: CourseRouter,
+ cssInjector: CSSInjector
+ ) {
if handouts != nil {
self.title = CourseLocalization.HandoutsCellHandouts.title
} else {
@@ -66,19 +71,23 @@ public struct HandoutsUpdatesDetailView: View {
GeometryReader { reader in
// MARK: - Page name
VStack(alignment: .center) {
- NavigationBar(title: title,
- leftButtonAction: { router.back() })
+ NavigationBar(
+ title: title,
+ leftButtonAction: { router.back() }
+ )
// MARK: - Page Body
VStack(alignment: .leading) {
// MARK: - Handouts
if let handouts {
- let formattedHandouts = cssInjector.injectCSS(colorScheme: colorScheme,
- html: handouts,
- type: .discovery,
- fontSize: idiom == .pad ? 100 : 300,
- screenWidth: .infinity)
+ let formattedHandouts = cssInjector.injectCSS(
+ colorScheme: colorScheme,
+ html: handouts,
+ type: .discovery,
+ fontSize: idiom == .pad ? 100 : 300,
+ screenWidth: .infinity
+ )
WebViewHtml(fixBrokenLinks(in: formattedHandouts))
} else if let announcements {
@@ -89,15 +98,19 @@ public struct HandoutsUpdatesDetailView: View {
Text(ann.date)
.font(Theme.Fonts.labelSmall)
- let formattedAnnouncements = cssInjector.injectCSS(colorScheme: colorScheme,
- html: ann.content,
- type: .discovery,
- screenWidth: reader.size.width)
- HTMLFormattedText(fixBrokenLinks(in: formattedAnnouncements),
- isScrollEnabled: true,
- textViewHeight: $height[index])
+ let formattedAnnouncements = cssInjector.injectCSS(
+ colorScheme: colorScheme,
+ html: ann.content,
+ type: .discovery,
+ screenWidth: reader.size.width
+ )
+ HTMLFormattedText(
+ fixBrokenLinks(in: formattedAnnouncements),
+ isScrollEnabled: true,
+ textViewHeight: $height[index]
+ )
.frame(height: height[index])
-
+
if index != announcements.count - 1 {
Divider()
}
@@ -135,13 +148,23 @@ Hi! Welcome to the demonstration course. We built this to help you become more f
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollitia animi, id est laborum.
"""
- HandoutsUpdatesDetailView(handouts: nil,
- announcements: [CourseUpdate(id: 1, date: "1 march",
- content: handouts, status: "done"),
- CourseUpdate(id: 2, date: "3 april",
- content: loremIpsumHtml, status: "nice")],
- router: CourseRouterMock(),
- cssInjector: CSSInjectorMock())
+ HandoutsUpdatesDetailView(
+ handouts: nil,
+ announcements: [
+ CourseUpdate(
+ id: 1,
+ date: "1 march",
+ content: handouts,
+ status: "done"
+ ),
+ CourseUpdate(
+ id: 2,
+ date: "3 april",
+ content: loremIpsumHtml,
+ status: "nice")],
+ router: CourseRouterMock(),
+ cssInjector: CSSInjectorMock()
+ )
}
}
#endif
diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift
index e876b9dd9..bbc0752e9 100644
--- a/Course/Course/Presentation/Handouts/HandoutsView.swift
+++ b/Course/Course/Presentation/Handouts/HandoutsView.swift
@@ -12,10 +12,13 @@ struct HandoutsView: View {
private let courseID: String
- @ObservedObject private var viewModel: HandoutsViewModel
+ @ObservedObject
+ private var viewModel: HandoutsViewModel
- public init(courseID: String,
- viewModel: HandoutsViewModel) {
+ public init(
+ courseID: String,
+ viewModel: HandoutsViewModel
+ ) {
self.courseID = courseID
self.viewModel = viewModel
}
@@ -60,13 +63,15 @@ struct HandoutsView: View {
}
// MARK: - Offline mode SnackBar
- OfflineSnackBarView(connectivity: viewModel.connectivity,
- reloadAction: {
- Task {
- await viewModel.getHandouts(courseID: courseID)
- await viewModel.getUpdates(courseID: courseID)
+ OfflineSnackBarView(
+ connectivity: viewModel.connectivity,
+ reloadAction: {
+ Task {
+ await viewModel.getHandouts(courseID: courseID)
+ await viewModel.getUpdates(courseID: courseID)
+ }
}
- })
+ )
// MARK: - Error Alert
if viewModel.showError {
diff --git a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift
index 2759406cb..2055e4adb 100644
--- a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift
+++ b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift
@@ -25,15 +25,17 @@ public class HandoutsViewModel: ObservableObject {
}
private let interactor: CourseInteractorProtocol
- public let cssInjector: CSSInjector
- public let router: CourseRouter
- public let connectivity: ConnectivityProtocol
+ let cssInjector: CSSInjector
+ let router: CourseRouter
+ let connectivity: ConnectivityProtocol
- public init(interactor: CourseInteractorProtocol,
- router: CourseRouter,
- cssInjector: CSSInjector,
- connectivity: ConnectivityProtocol,
- courseID: String) {
+ public init(
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ cssInjector: CSSInjector,
+ connectivity: ConnectivityProtocol,
+ courseID: String
+ ) {
self.interactor = interactor
self.router = router
self.cssInjector = cssInjector
diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift
new file mode 100644
index 000000000..9498ec471
--- /dev/null
+++ b/Course/Course/Presentation/Outline/ContinueWithView.swift
@@ -0,0 +1,137 @@
+//
+// ContinueWithView.swift
+// Course
+//
+// Created by Stepanok Ivan on 29.05.2023.
+//
+
+import SwiftUI
+import Core
+
+struct ContinueWith {
+ let chapterIndex: Int
+ let sequentialIndex: Int
+ let verticalIndex: Int
+}
+
+struct ContinueWithView: View {
+ let data: ContinueWith
+ let courseStructure: CourseStructure
+ let router: CourseRouter
+
+ private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
+
+ init(data: ContinueWith, courseStructure: CourseStructure, router: CourseRouter) {
+ self.data = data
+ self.courseStructure = courseStructure
+ self.router = router
+ }
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ let chapter = courseStructure.childs[data.chapterIndex]
+ if let vertical = chapter.childs[data.sequentialIndex].childs.first {
+ if idiom == .pad {
+ HStack(alignment: .top) {
+ VStack(alignment: .leading) {
+ ContinueTitle(vertical: vertical)
+ }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
+ Spacer()
+ UnitButtonView(type: .continueLesson, action: {
+ router.showCourseVerticalView(id: courseStructure.id,
+ title: chapter.childs[data.sequentialIndex].displayName,
+ chapters: courseStructure.childs,
+ chapterIndex: data.chapterIndex,
+ sequentialIndex: data.sequentialIndex)
+ }).frame(width: 200)
+ } .padding(.horizontal, 24)
+ .padding(.top, 32)
+ } else {
+ VStack(alignment: .leading) {
+ ContinueTitle(vertical: vertical)
+ .foregroundColor(CoreAssets.textPrimary.swiftUIColor)
+ }
+ UnitButtonView(type: .continueLesson, action: {
+ router.showCourseVerticalView(id: courseStructure.id,
+ title: chapter.childs[data.sequentialIndex].displayName,
+ chapters: courseStructure.childs,
+ chapterIndex: data.chapterIndex,
+ sequentialIndex: data.sequentialIndex)
+ })
+ }
+
+ }
+ } .padding(.horizontal, 24)
+ .padding(.top, 32)
+ }
+}
+
+private struct ContinueTitle: View {
+
+ let vertical: CourseVertical
+
+ var body: some View {
+ Text(CoreLocalization.Courseware.continueWith)
+ .font(Theme.Fonts.labelMedium)
+ .foregroundColor(CoreAssets.textSecondary.swiftUIColor)
+ HStack {
+ vertical.type.image
+ Text(vertical.displayName)
+ .multilineTextAlignment(.leading)
+ .font(Theme.Fonts.titleMedium)
+ .multilineTextAlignment(.leading)
+ }
+ }
+
+}
+
+#if DEBUG
+struct ContinueWithView_Previews: PreviewProvider {
+ static var previews: some View {
+
+ let childs = [
+ CourseChapter(
+ blockId: "123",
+ id: "123",
+ displayName: "Continue lesson",
+ type: .chapter,
+ childs: [
+ CourseSequential(
+ blockId: "1",
+ id: "1",
+ displayName: "Name",
+ type: .sequential,
+ completion: 0,
+ childs: [
+ CourseVertical(
+ blockId: "1",
+ id: "1",
+ displayName: "Vertical",
+ type: .vertical,
+ completion: 0,
+ childs: [
+ CourseBlock(
+ blockId: "2", id: "2",
+ graded: true,
+ completion: 0,
+ type: .html,
+ displayName: "Continue lesson",
+ studentUrl: "")
+ ])])])
+ ]
+
+ ContinueWithView(data: ContinueWith(chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0),
+ courseStructure: CourseStructure(id: "123",
+ graded: true,
+ completion: 0,
+ viewYouTubeUrl: "",
+ encodedVideo: "",
+ displayName: "Namaste",
+ childs: childs,
+ media: DataLayer.CourseMedia.init(image:
+ .init(raw: "", small: "", large: "")),
+ certificate: nil),
+ router: CourseRouterMock())
+ }
+}
+#endif
diff --git a/Course/Course/Presentation/Outline/CourseBlocksView.swift b/Course/Course/Presentation/Outline/CourseBlocksView.swift
deleted file mode 100644
index bf44cd0bb..000000000
--- a/Course/Course/Presentation/Outline/CourseBlocksView.swift
+++ /dev/null
@@ -1,217 +0,0 @@
-//
-// CourseBlocksView.swift
-// Course
-//
-// Created by Stepanok Ivan on 04.02.2023.
-//
-
-import SwiftUI
-
-import Core
-import Kingfisher
-
-public struct CourseBlocksView: View {
-
- private var title: String
- @ObservedObject
- private var viewModel: CourseBlocksViewModel
- private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
-
- public init(title: String,
- viewModel: CourseBlocksViewModel) {
- self.title = title
- self.viewModel = viewModel
- }
-
- public var body: some View {
- ZStack(alignment: .top) {
-
- // MARK: - Page name
- GeometryReader { proxy in
- VStack(alignment: .center) {
- NavigationBar(title: title,
- leftButtonAction: { viewModel.router.back() })
-
- // MARK: - Page Body
- ScrollView {
- VStack(alignment: .leading) {
- // MARK: - Lessons list
- ForEach(viewModel.blocks, id: \.id) { block in
- let index = viewModel.blocks.firstIndex(where: { $0.id == block.id })
- Button(action: {
- viewModel.router.showCourseUnit(blockId: block.id,
- courseID: block.blockId,
- sectionName: title,
- blocks: viewModel.blocks)
- }, label: {
- HStack {
- Group {
- if block.completion == 1 {
- CoreAssets.finished.swiftUIImage
- .renderingMode(.template)
- .foregroundColor(.accentColor)
- } else {
- block.type.image
- }
- Text(block.displayName)
- .multilineTextAlignment(.leading)
- .font(Theme.Fonts.titleMedium)
- .lineLimit(1)
- .frame(maxWidth: idiom == .pad
- ? proxy.size.width * 0.5
- : proxy.size.width * 0.6,
- alignment: .leading)
- }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
- Spacer()
- if let state = viewModel.downloadState[block.id] {
- switch state {
- case .available:
- DownloadAvailableView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: block.id, state: state)
- }
- .onForeground {
- viewModel.onForeground()
- }
- case .downloading:
- DownloadProgressView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: block.id, state: state)
- }
- .onBackground {
- viewModel.onBackground()
- }
- case .finished:
- DownloadFinishedView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: block.id, state: state)
- }
- }
- }
- Image(systemName: "chevron.right")
- .padding(.vertical, 8)
- }
- }).padding(.horizontal, 36)
- .padding(.vertical, 14)
- if index != viewModel.blocks.count - 1 {
- Divider()
- .frame(height: 1)
- .overlay(CoreAssets.cardViewStroke.swiftUIColor)
- .padding(.horizontal, 24)
- }
- }
- }
- Spacer(minLength: 84)
- }.frameLimit()
- .onRightSwipeGesture {
- viewModel.router.back()
- }
- }
- }
-
- // MARK: - Offline mode SnackBar
- OfflineSnackBarView(connectivity: viewModel.connectivity,
- reloadAction: { })
-
- // MARK: - Error Alert
- if viewModel.showError {
- VStack {
- Spacer()
- SnackBarView(message: viewModel.errorMessage)
- }
- .padding(.bottom, viewModel.connectivity.isInternetAvaliable
- ? 0 : OfflineSnackBarView.height)
- .transition(.move(edge: .bottom))
- .onAppear {
- doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
- viewModel.errorMessage = nil
- }
- }
- }
-
- }
- .background(
- CoreAssets.background.swiftUIColor
- .ignoresSafeArea()
- )
- }
-}
-
-#if DEBUG
-struct CourseBlocksView_Previews: PreviewProvider {
- static var previews: some View {
- let blocks: [CourseBlock] = [
- CourseBlock(
- blockId: "block_1",
- id: "1",
- topicId: nil,
- graded: true,
- completion: 0,
- type: .html,
- displayName: "HTML Block",
- studentUrl: "",
- videoUrl: nil,
- youTubeUrl: nil
- ),
- CourseBlock(
- blockId: "block_2",
- id: "2",
- topicId: nil,
- graded: true,
- completion: 0,
- type: .problem,
- displayName: "Problem Block",
- studentUrl: "",
- videoUrl: nil,
- youTubeUrl: nil
- ),
- CourseBlock(
- blockId: "block_3",
- id: "3",
- topicId: nil,
- graded: true,
- completion: 1,
- type: .problem,
- displayName: "Completed Problem Block",
- studentUrl: "",
- videoUrl: nil,
- youTubeUrl: nil
- ),
- CourseBlock(
- blockId: "block_4",
- id: "4",
- topicId: nil,
- graded: true,
- completion: 0,
- type: .video,
- displayName: "Video Block",
- studentUrl: "",
- videoUrl: "some_data",
- youTubeUrl: nil
- )
- ]
-
- let viewModel = CourseBlocksViewModel(blocks: blocks,
- manager: DownloadManagerMock(),
- router: CourseRouterMock(),
- connectivity: Connectivity())
-
- return Group {
- CourseBlocksView(
- title: "Course title",
- viewModel: viewModel
- )
- .preferredColorScheme(.light)
- .previewDisplayName("CourseBlocksView Light")
-
- CourseBlocksView(
- title: "Course title",
- viewModel: viewModel
- )
- .preferredColorScheme(.dark)
- .previewDisplayName("CourseBlocksView Dark")
- }
-
- }
-}
-#endif
diff --git a/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift b/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift
deleted file mode 100644
index 9dba81c5a..000000000
--- a/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift
+++ /dev/null
@@ -1,90 +0,0 @@
-//
-// CourseBlocksViewModel.swift
-// Course
-//
-// Created by Stepanok Ivan on 14.03.2023.
-//
-
-import SwiftUI
-import Core
-import Combine
-
-public class CourseBlocksViewModel: BaseCourseViewModel {
- let router: CourseRouter
- let connectivity: ConnectivityProtocol
- @Published var blocks: [CourseBlock]
- @Published var downloadState: [String: DownloadViewState] = [:]
- @Published var showError: Bool = false
-
- var errorMessage: String? {
- didSet {
- withAnimation {
- showError = errorMessage != nil
- }
- }
- }
-
- public init(blocks: [CourseBlock],
- manager: DownloadManagerProtocol,
- router: CourseRouter,
- connectivity: ConnectivityProtocol) {
- self.blocks = blocks
- self.router = router
- self.connectivity = connectivity
-
- super.init(manager: manager)
-
- manager.publisher()
- .sink(receiveValue: { [weak self] _ in
- guard let self else { return }
- DispatchQueue.main.async {
- self.setDownloadsStates()
- }
- })
- .store(in: &cancellables)
-
- setDownloadsStates()
- }
-
- func onDownloadViewTap(blockId: String, state: DownloadViewState) {
- if let block = blocks.first(where: { $0.id == blockId }) {
- do {
- switch state {
- case .available:
- try manager.addToDownloadQueue(blocks: [block])
- downloadState[block.id] = .downloading
- case .downloading:
- try manager.cancelDownloading(blocks: [block])
- downloadState[block.id] = .available
- case .finished:
- manager.deleteFile(blocks: [block])
- downloadState[block.id] = .available
- }
- } catch let error {
- if error is NoWiFiError {
- errorMessage = CoreLocalization.Error.wifi
- }
- }
- }
- }
-
- private func setDownloadsStates() {
- let downloads = manager.getAllDownloads()
- var states: [String: DownloadViewState] = [:]
- for block in blocks where block.isDownloadable {
- if let download = downloads.first(where: { $0.id == block.id }) {
- switch download.state {
- case .waiting, .inProgress:
- states[download.id] = .downloading
- case .paused:
- states[download.id] = .available
- case .finished:
- states[download.id] = .finished
- }
- } else {
- states[block.id] = .available
- }
- }
- downloadState = states
- }
-}
diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift
index 8cc925146..4e3b309a6 100644
--- a/Course/Course/Presentation/Outline/CourseOutlineView.swift
+++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift
@@ -38,7 +38,7 @@ public struct CourseOutlineView: View {
GeometryReader { proxy in
VStack(alignment: .center) {
NavigationBar(title: title,
- leftButtonAction: { viewModel.router.back() })
+ leftButtonAction: { viewModel.router.back() })
// MARK: - Page Body
RefreshableScrollViewCompat(action: {
@@ -90,17 +90,24 @@ public struct CourseOutlineView: View {
.fixedSize(horizontal: false, vertical: true)
if !isVideo {
- if let sequential = viewModel.returnCourseSequential {
- ContinueWithView(sequential: sequential, viewModel: viewModel)
+ if let continueWith = viewModel.continueWith,
+ let courseStructure = viewModel.courseStructure {
+ ContinueWithView(
+ data: continueWith,
+ courseStructure: courseStructure,
+ router: viewModel.router
+ )
}
}
- if let courseStructure = isVideo ? viewModel.courseVideosStructure : viewModel.courseStructure {
+ if let courseStructure = isVideo
+ ? viewModel.courseVideosStructure
+ : viewModel.courseStructure {
// MARK: - Sections list
let chapters = courseStructure.childs
ForEach(chapters, id: \.id) { chapter in
- let index = chapters.firstIndex(where: {$0.id == chapter.id })
+ let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id })
Text(chapter.displayName)
.font(Theme.Fonts.titleMedium)
.multilineTextAlignment(.leading)
@@ -108,10 +115,18 @@ public struct CourseOutlineView: View {
.padding(.horizontal, 24)
.padding(.top, 40)
ForEach(chapter.childs, id: \.id) { child in
+ let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id })
VStack(alignment: .leading) {
Button(action: {
- viewModel.router.showCourseVerticalView(title: child.displayName,
- verticals: child.childs)
+ if let chapterIndex, let sequentialIndex {
+ viewModel.router.showCourseVerticalView(
+ id: courseID,
+ title: child.displayName,
+ chapters: chapters,
+ chapterIndex: chapterIndex,
+ sequentialIndex: sequentialIndex
+ )
+ }
}, label: {
Group {
child.type.image
@@ -119,10 +134,12 @@ public struct CourseOutlineView: View {
.font(Theme.Fonts.titleMedium)
.multilineTextAlignment(.leading)
.lineLimit(1)
- .frame(maxWidth: idiom == .pad
- ? proxy.size.width * 0.5
- : proxy.size.width * 0.6,
- alignment: .leading)
+ .frame(
+ maxWidth: idiom == .pad
+ ? proxy.size.width * 0.5
+ : proxy.size.width * 0.6,
+ alignment: .leading
+ )
}.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
Spacer()
if let state = viewModel.downloadState[child.id] {
@@ -166,7 +183,7 @@ public struct CourseOutlineView: View {
.foregroundColor(CoreAssets.accentColor.swiftUIColor)
}).padding(.horizontal, 36)
.padding(.vertical, 20)
- if index != chapters.count - 1 {
+ if chapterIndex != chapters.count - 1 {
Divider()
.frame(height: 1)
.overlay(CoreAssets.cardViewStroke.swiftUIColor)
@@ -191,10 +208,12 @@ public struct CourseOutlineView: View {
}
// MARK: - Offline mode SnackBar
- OfflineSnackBarView(connectivity: viewModel.connectivity,
- reloadAction: {
- await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14)
- })
+ OfflineSnackBarView(
+ connectivity: viewModel.connectivity,
+ reloadAction: {
+ await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14)
+ }
+ )
// MARK: - Error Alert
if viewModel.showError {
@@ -228,73 +247,20 @@ public struct CourseOutlineView: View {
}
}
-struct ContinueWithView: View {
- let sequential: CourseSequential
- let viewModel: CourseContainerViewModel
- private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
-
- var body: some View {
- if idiom == .pad {
- HStack(alignment: .top) {
- if let vertical = sequential.childs.first {
- VStack(alignment: .leading) {
- Text(CourseLocalization.Courseware.continueWith)
- .font(Theme.Fonts.labelMedium)
- .foregroundColor(CoreAssets.textSecondary.swiftUIColor)
- HStack {
- vertical.type.image
- Text(vertical.displayName)
- .multilineTextAlignment(.leading)
- .font(Theme.Fonts.titleMedium)
- .multilineTextAlignment(.leading)
- }
- }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
- Spacer()
- UnitButtonView(type: .continueLesson, action: {
- viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs),
- blocks: (vertical.displayName, vertical.childs))
- }).frame(width: 200)
- }
- } .padding(.horizontal, 24)
- .padding(.top, 32)
- } else {
- VStack(alignment: .leading) {
- if let vertical = sequential.childs.first {
- Text(CourseLocalization.Courseware.continueWith)
- .font(Theme.Fonts.labelMedium)
- .foregroundColor(CoreAssets.textSecondary.swiftUIColor)
- HStack {
- vertical.type.image
- Text(vertical.displayName)
- .multilineTextAlignment(.leading)
- .font(Theme.Fonts.titleMedium)
- .multilineTextAlignment(.leading)
- }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
- UnitButtonView(type: .continueLesson, action: {
- viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs),
- blocks: (vertical.displayName, vertical.childs))
- })
- }
- }
- .padding(.horizontal, 24)
- .padding(.top, 32)
- }
- }
-}
-
#if DEBUG
struct CourseOutlineView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = CourseContainerViewModel(
interactor: CourseInteractor.mock,
+ authInteractor: AuthInteractor.mock,
router: CourseRouterMock(),
config: ConfigMock(),
connectivity: Connectivity(),
manager: DownloadManagerMock(),
- isActive: nil,
+ isActive: true,
courseStart: Date(),
courseEnd: nil,
- enrollmentStart: nil,
+ enrollmentStart: Date(),
enrollmentEnd: nil
)
Task {
diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVerticalView.swift
index f68e7b314..653f78e76 100644
--- a/Course/Course/Presentation/Outline/CourseVerticalView.swift
+++ b/Course/Course/Presentation/Outline/CourseVerticalView.swift
@@ -13,15 +13,18 @@ import Kingfisher
public struct CourseVerticalView: View {
private var title: String
+ private let id: String
@ObservedObject
private var viewModel: CourseVerticalViewModel
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
public init(
title: String,
+ id: String,
viewModel: CourseVerticalViewModel
) {
self.title = title
+ self.id = id
self.viewModel = viewModel
}
@@ -29,7 +32,7 @@ public struct CourseVerticalView: View {
ZStack(alignment: .top) {
VStack(alignment: .center) {
NavigationBar(title: title,
- leftButtonAction: { viewModel.router.back() })
+ leftButtonAction: { viewModel.router.back() })
// MARK: - Page Body
GeometryReader { proxy in
@@ -37,68 +40,84 @@ public struct CourseVerticalView: View {
VStack(alignment: .leading) {
// MARK: - Lessons list
ForEach(viewModel.verticals, id: \.id) { vertical in
- let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id})
- Button(action: {
- viewModel.router.showCourseBlocksView(
- title: vertical.displayName,
- blocks: vertical.childs
- )
- }, label: {
- HStack {
- Group {
- if vertical.completion == 1 {
- CoreAssets.finished.swiftUIImage
- .renderingMode(.template)
- .foregroundColor(.accentColor)
- } else {
- vertical.type.image
- }
- Text(vertical.displayName)
- .font(Theme.Fonts.titleMedium)
- .lineLimit(1)
- .frame(maxWidth: idiom == .pad
- ? proxy.size.width * 0.5
- : proxy.size.width * 0.6,
- alignment: .leading)
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity, alignment: .leading)
- }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
- Spacer()
- if let state = viewModel.downloadState[vertical.id] {
- switch state {
- case .available:
- DownloadAvailableView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: vertical.id, state: state)
- }
- .onForeground {
- viewModel.onForeground()
- }
- case .downloading:
- DownloadProgressView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: vertical.id, state: state)
- }
- .onBackground {
- viewModel.onBackground()
- }
- case .finished:
- DownloadFinishedView()
- .onTapGesture {
- viewModel.onDownloadViewTap(blockId: vertical.id, state: state)
- }
+ if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) {
+ Button(action: {
+ if let block = viewModel.verticals[index].childs.first {
+ viewModel.router.showCourseUnit(id: id,
+ blockId: block.id,
+ courseID: block.blockId,
+ sectionName: block.displayName,
+ verticalIndex: index,
+ chapters: viewModel.chapters,
+ chapterIndex: viewModel.chapterIndex,
+ sequentialIndex: viewModel.sequentialIndex)
+ }
+ }, label: {
+ HStack {
+ Group {
+ if vertical.completion == 1 {
+ CoreAssets.finished.swiftUIImage
+ .renderingMode(.template)
+ .foregroundColor(.accentColor)
+ } else {
+ vertical.type.image
+ }
+ Text(vertical.displayName)
+ .font(Theme.Fonts.titleMedium)
+ .lineLimit(1)
+ .frame(maxWidth: idiom == .pad
+ ? proxy.size.width * 0.5
+ : proxy.size.width * 0.6,
+ alignment: .leading)
+ .multilineTextAlignment(.leading)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }.foregroundColor(CoreAssets.textPrimary.swiftUIColor)
+ Spacer()
+ if let state = viewModel.downloadState[vertical.id] {
+ switch state {
+ case .available:
+ DownloadAvailableView()
+ .onTapGesture {
+ viewModel.onDownloadViewTap(
+ blockId: vertical.id,
+ state: state
+ )
+ }
+ .onForeground {
+ viewModel.onForeground()
+ }
+ case .downloading:
+ DownloadProgressView()
+ .onTapGesture {
+ viewModel.onDownloadViewTap(
+ blockId: vertical.id,
+ state: state
+ )
+ }
+ .onBackground {
+ viewModel.onBackground()
+ }
+ case .finished:
+ DownloadFinishedView()
+ .onTapGesture {
+ viewModel.onDownloadViewTap(
+ blockId: vertical.id,
+ state: state
+ )
+ }
+ }
}
+ Image(systemName: "chevron.right")
+ .padding(.vertical, 8)
}
- Image(systemName: "chevron.right")
- .padding(.vertical, 8)
+ }).padding(.horizontal, 36)
+ .padding(.vertical, 14)
+ if index != viewModel.verticals.count - 1 {
+ Divider()
+ .frame(height: 1)
+ .overlay(CoreAssets.cardViewStroke.swiftUIColor)
+ .padding(.horizontal, 24)
}
- }).padding(.horizontal, 36)
- .padding(.vertical, 14)
- if index != viewModel.verticals.count - 1 {
- Divider()
- .frame(height: 1)
- .overlay(CoreAssets.cardViewStroke.swiftUIColor)
- .padding(.horizontal, 24)
}
}
}
@@ -112,7 +131,7 @@ public struct CourseVerticalView: View {
// MARK: - Offline mode SnackBar
OfflineSnackBarView(connectivity: viewModel.connectivity,
- reloadAction: { })
+ reloadAction: { })
// MARK: - Error Alert
if viewModel.showError {
@@ -140,47 +159,48 @@ public struct CourseVerticalView: View {
#if DEBUG
struct CourseVerticalView_Previews: PreviewProvider {
static var previews: some View {
-
- let verticals: [CourseVertical] = [
- CourseVertical(
- blockId: "block_1",
+ let chapters = [
+ CourseChapter(
+ blockId: "1",
id: "1",
- displayName: "Some vertical",
- type: .vertical,
- completion: 0,
- childs: []
- ),
- CourseVertical(
- blockId: "block_2",
- id: "2",
- displayName: "Comleted vertical",
- type: .vertical,
- completion: 1,
- childs: []
- ),
- CourseVertical(
- blockId: "block_3",
- id: "3",
- displayName: "Another vertical",
- type: .vertical,
- completion: 0,
- childs: []
- )
+ displayName: "Chapter 1",
+ type: .chapter,
+ childs: [
+ CourseSequential(
+ blockId: "3",
+ id: "3",
+ displayName: "Sequential",
+ type: .sequential,
+ completion: 1,
+ childs: [
+ CourseVertical(
+ blockId: "4",
+ id: "4",
+ displayName: "Vertical",
+ type: .vertical,
+ completion: 0,
+ childs: [])
+ ])
+ ])
]
- let viewModel = CourseVerticalViewModel(verticals: verticals,
- manager: DownloadManagerMock(),
- router: CourseRouterMock(),
- connectivity: Connectivity())
+ let viewModel = CourseVerticalViewModel(
+ chapters: chapters,
+ chapterIndex: 0,
+ sequentialIndex: 0,
+ manager: DownloadManagerMock(),
+ router: CourseRouterMock(),
+ connectivity: Connectivity()
+ )
return Group {
- CourseVerticalView(title: "Course title", viewModel: viewModel)
- .preferredColorScheme(.light)
- .previewDisplayName("CourseVerticalView Light")
+ CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel)
+ .preferredColorScheme(.light)
+ .previewDisplayName("CourseVerticalView Light")
- CourseVerticalView(title: "Course title", viewModel: viewModel)
- .preferredColorScheme(.dark)
- .previewDisplayName("CourseVerticalView Dark")
+ CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel)
+ .preferredColorScheme(.dark)
+ .previewDisplayName("CourseVerticalView Dark")
}
}
diff --git a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift
index 54fc1ca01..9d0bd77c5 100644
--- a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift
+++ b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift
@@ -15,7 +15,10 @@ public class CourseVerticalViewModel: BaseCourseViewModel {
@Published var verticals: [CourseVertical]
@Published var downloadState: [String: DownloadViewState] = [:]
@Published var showError: Bool = false
-
+ let chapters: [CourseChapter]
+ let chapterIndex: Int
+ let sequentialIndex: Int
+
var errorMessage: String? {
didSet {
withAnimation {
@@ -24,14 +27,20 @@ public class CourseVerticalViewModel: BaseCourseViewModel {
}
}
- public init(verticals: [CourseVertical],
- manager: DownloadManagerProtocol,
- router: CourseRouter,
- connectivity: ConnectivityProtocol) {
- self.verticals = verticals
+ public init(
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int,
+ manager: DownloadManagerProtocol,
+ router: CourseRouter,
+ connectivity: ConnectivityProtocol
+ ) {
+ self.chapters = chapters
+ self.chapterIndex = chapterIndex
+ self.sequentialIndex = sequentialIndex
self.router = router
self.connectivity = connectivity
-
+ self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs
super.init(manager: manager)
manager.publisher()
@@ -68,7 +77,7 @@ public class CourseVerticalViewModel: BaseCourseViewModel {
}
}
}
-
+
private func setDownloadsStates() {
let downloads = manager.getAllDownloads()
var states: [String: DownloadViewState] = [:]
diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift
index 9692e14fe..a4b9f9d85 100644
--- a/Course/Course/Presentation/Unit/CourseNavigationView.swift
+++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift
@@ -7,80 +7,153 @@
import SwiftUI
import Core
+import Combine
struct CourseNavigationView: View {
- @ObservedObject private var viewModel: CourseUnitViewModel
+ @ObservedObject
+ private var viewModel: CourseUnitViewModel
private let sectionName: String
- @Binding var killPlayer: Bool
-
- init(sectionName: String, viewModel: CourseUnitViewModel, killPlayer: Binding) {
+ private let playerStateSubject: CurrentValueSubject
+
+ init(
+ sectionName: String,
+ viewModel: CourseUnitViewModel,
+ playerStateSubject: CurrentValueSubject
+ ) {
self.viewModel = viewModel
self.sectionName = sectionName
- self._killPlayer = killPlayer
+ self.playerStateSubject = playerStateSubject
}
var body: some View {
- HStack(alignment: .top, spacing: 24) {
- if viewModel.selectedLesson() == viewModel.blocks.first
- && viewModel.blocks.count != 1 {
- UnitButtonView(type: .first, action: {
- killPlayer.toggle()
+ HStack(alignment: .top, spacing: 7) {
+ if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.first
+ && viewModel.verticals[viewModel.verticalIndex].childs.count != 1 {
+ UnitButtonView(type: .nextBig, action: {
+ playerStateSubject.send(VideoPlayerState.pause)
viewModel.select(move: .next)
- viewModel.createLessonType()
- })
+ }).frame(width: 215)
} else {
-
- if viewModel.previousLesson != "" {
- UnitButtonView(type: .previous, action: {
- killPlayer.toggle()
- viewModel.select(move: .previous)
- viewModel.createLessonType()
- })
- }
- if viewModel.nextLesson != "" {
- UnitButtonView(type: .next, action: {
- killPlayer.toggle()
- viewModel.select(move: .next)
- viewModel.createLessonType()
- })
- }
- if viewModel.selectedLesson() == viewModel.blocks.last {
- UnitButtonView(type: viewModel.blocks.count == 1 ? .finish : .last, action: {
+ if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.last {
+ if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first {
+ UnitButtonView(type: .previous, action: {
+ playerStateSubject.send(VideoPlayerState.pause)
+ viewModel.select(move: .previous)
+ })
+ }
+ UnitButtonView(type: .last, action: {
+ let sequentials = viewModel.chapters[viewModel.chapterIndex].childs
+ let verticals = viewModel
+ .chapters[viewModel.chapterIndex]
+ .childs[viewModel.sequentialIndex]
+ .childs
+ let chapters = viewModel.chapters
+ let currentVertical = viewModel.verticals[viewModel.verticalIndex]
+
viewModel.router.presentAlert(
alertTitle: CourseLocalization.Courseware.goodWork,
alertMessage: (CourseLocalization.Courseware.section
- + " " + sectionName + " " + CourseLocalization.Courseware.isFinished),
+ + currentVertical.displayName + CourseLocalization.Courseware.isFinished),
+ nextSectionName: {
+ if viewModel.verticals.count > viewModel.verticalIndex + 1 {
+ return viewModel.verticals[viewModel.verticalIndex + 1].displayName
+ } else if sequentials.count > viewModel.sequentialIndex + 1 {
+ return sequentials[viewModel.sequentialIndex + 1].childs.first?.displayName
+ } else if chapters.count > viewModel.chapterIndex + 1 {
+ return chapters[viewModel.chapterIndex + 1].childs.first?.childs.first?.displayName
+ } else {
+ return nil
+ }
+ }(),
action: CourseLocalization.Courseware.backToOutline,
image: CoreAssets.goodWork.swiftUIImage,
- onCloseTapped: {},
+ onCloseTapped: { viewModel.router.dismiss(animated: false) },
okTapped: {
- killPlayer.toggle()
+ playerStateSubject.send(VideoPlayerState.pause)
+ playerStateSubject.send(VideoPlayerState.kill)
+ viewModel.router.dismiss(animated: false)
+ viewModel.router.back(animated: true)
+ },
+ nextSectionTapped: {
+ playerStateSubject.send(VideoPlayerState.pause)
+ playerStateSubject.send(VideoPlayerState.kill)
viewModel.router.dismiss(animated: false)
- viewModel.router.removeLastView(controllers: 2)
+
+ let chapterIndex: Int
+ let sequentialIndex: Int
+ let verticalIndex: Int
+
+ // Switch to the next Vertical
+ if verticals.count - 1 > viewModel.verticalIndex {
+ chapterIndex = viewModel.chapterIndex
+ sequentialIndex = viewModel.sequentialIndex
+ verticalIndex = viewModel.verticalIndex + 1
+ // Switch to the next Sequential
+ } else if sequentials.count - 1 > viewModel.sequentialIndex {
+ chapterIndex = viewModel.chapterIndex
+ sequentialIndex = viewModel.sequentialIndex + 1
+ verticalIndex = 0
+ } else {
+ // Switch to the next Chapter
+ chapterIndex = viewModel.chapterIndex + 1
+ sequentialIndex = 0
+ verticalIndex = 0
+ }
+
+ viewModel.router.replaceCourseUnit(
+ id: viewModel.id,
+ blockId: viewModel.lessonID,
+ courseID: viewModel.courseID,
+ sectionName: viewModel.selectedLesson().displayName,
+ verticalIndex: verticalIndex,
+ chapters: viewModel.chapters,
+ chapterIndex: chapterIndex,
+ sequentialIndex: sequentialIndex)
}
)
})
+ } else {
+ if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first {
+ UnitButtonView(type: .previous, action: {
+ playerStateSubject.send(VideoPlayerState.pause)
+ viewModel.select(move: .previous)
+ })
+ }
+
+ UnitButtonView(type: .next, action: {
+ playerStateSubject.send(VideoPlayerState.pause)
+ viewModel.select(move: .next)
+ })
}
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal, 24)
-
}
}
#if DEBUG
struct CourseNavigationView_Previews: PreviewProvider {
static var previews: some View {
- let viewModel = CourseUnitViewModel(lessonID: "1",
- courseID: "1",
- blocks: [],
- interactor: CourseInteractor.mock,
- router: CourseRouterMock(),
- connectivity: Connectivity(),
- manager: DownloadManagerMock())
+ let viewModel = CourseUnitViewModel(
+ lessonID: "1",
+ courseID: "1",
+ id: "1",
+ chapters: [],
+ chapterIndex: 1,
+ sequentialIndex: 1,
+ verticalIndex: 1,
+ interactor: CourseInteractor.mock,
+ router: CourseRouterMock(),
+ connectivity: Connectivity(),
+ manager: DownloadManagerMock()
+ )
- CourseNavigationView(sectionName: "Name", viewModel: viewModel, killPlayer: .constant(false))
+ CourseNavigationView(
+ sectionName: "Name",
+ viewModel: viewModel,
+ playerStateSubject: CurrentValueSubject(nil)
+ )
}
}
#endif
diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift
index 92af45d6c..56338ea7b 100644
--- a/Course/Course/Presentation/Unit/CourseUnitView.swift
+++ b/Course/Course/Presentation/Unit/CourseUnitView.swift
@@ -10,6 +10,8 @@ import SwiftUI
import Core
import Discussion
import Swinject
+import Introspect
+import Combine
public struct CourseUnitView: View {
@@ -22,223 +24,307 @@ public struct CourseUnitView: View {
}
}
}
- @State var killPlayer: Bool = false
+ @State var offsetView: CGFloat = 0
+ @State var showDiscussion: Bool = false
+
private let sectionName: String
+ public let playerStateSubject = CurrentValueSubject(nil)
public init(viewModel: CourseUnitViewModel,
sectionName: String) {
self.viewModel = viewModel
self.sectionName = sectionName
viewModel.loadIndex()
- viewModel.createLessonType()
viewModel.nextTitles()
}
public var body: some View {
ZStack(alignment: .top) {
-
- // MARK: - Page name
- VStack(alignment: .center) {
- NavigationBar(title: "",
- leftButtonAction: {
- viewModel.router.back()
- killPlayer.toggle()
- })
-
- // MARK: - Page Body
- VStack {
- ZStack(alignment: .top) {
- VStack(alignment: .leading) {
- if viewModel.connectivity.isInternetAvaliable
- || viewModel.lessonType != .video(videoUrl: "", blockID: "") {
- switch viewModel.lessonType {
- case let .youtube(url, blockID):
- VStack(alignment: .leading) {
- Text(viewModel.selectedLesson().displayName)
- .font(Theme.Fonts.titleLarge)
- .padding(.horizontal, 24)
- YouTubeVideoPlayer(url: url,
- blockID: blockID,
- courseID: viewModel.courseID,
- languages: viewModel.languages())
- Spacer()
-
- }.background(CoreAssets.background.swiftUIColor)
- case let .video(encodedUrl, blockID):
- Text(viewModel.selectedLesson().displayName)
- .font(Theme.Fonts.titleLarge)
- .padding(.horizontal, 24)
- EncodedVideoPlayer(
- url: viewModel.urlForVideoFileOrFallback(blockId: blockID, url: encodedUrl),
- blockID: blockID,
- courseID: viewModel.courseID,
- languages: viewModel.languages(),
- killPlayer: $killPlayer
- )
- Spacer()
- case .web(let url):
- VStack {
- WebUnitView(url: url, viewModel: Container.shared.resolve(WebUnitViewModel.self)!)
- }.background(Color.white)
- .contrast(1.08)
- .padding(.horizontal, -12)
- .roundedBackground(strokeColor: .clear, maxIpadWidth: .infinity)
-
- case .unknown(let url):
- Spacer()
+ // MARK: - Page Body
+ ZStack(alignment: .bottom) {
+ GeometryReader { reader in
+ VStack(spacing: 0) {
+ if viewModel.connectivity.isInternetAvaliable {
+ NavigationBar(title: "",
+ leftButtonAction: {
+ viewModel.router.back()
+ playerStateSubject.send(VideoPlayerState.kill)
+ }).padding(.top, 50)
+
+ LazyVStack(spacing: 0) {
+ let data = Array(viewModel.verticals[viewModel.verticalIndex].childs.enumerated())
+ ForEach(data, id: \.offset) { index, block in
VStack(spacing: 0) {
- CoreAssets.notAvaliable.swiftUIImage
- Text(CourseLocalization.NotAvaliable.title)
- .font(Theme.Fonts.titleLarge)
- .multilineTextAlignment(.center)
- .frame(maxWidth: .infinity)
- .padding(.top, 40)
- Text(CourseLocalization.NotAvaliable.description)
- .font(Theme.Fonts.bodyLarge)
- .multilineTextAlignment(.center)
- .frame(maxWidth: .infinity)
- .padding(.top, 12)
- StyledButton(CourseLocalization.NotAvaliable.button, action: {
- if let url = URL(string: url) {
- UIApplication.shared.open(url)
+ if index >= viewModel.index - 1 && index <= viewModel.index + 1 {
+ switch LessonType.from(block) {
+ // MARK: YouTube
+ case let .youtube(url, blockID):
+ YouTubeView(
+ name: block.displayName,
+ url: url,
+ courseID: viewModel.courseID,
+ blockID: blockID,
+ playerStateSubject: playerStateSubject,
+ languages: block.subtitles ?? [],
+ isOnScreen: index == viewModel.index
+ ).frameLimit()
+ Spacer(minLength: 100)
+
+ // MARK: Encoded Video
+ case let .video(encodedUrl, blockID):
+ EncodedVideoView(
+ name: block.displayName,
+ url: viewModel.urlForVideoFileOrFallback(
+ blockId: blockID,
+ url: encodedUrl
+ ),
+ courseID: viewModel.courseID,
+ blockID: blockID,
+ playerStateSubject: playerStateSubject,
+ languages: block.subtitles ?? [],
+ isOnScreen: index == viewModel.index
+ ).frameLimit()
+ Spacer(minLength: 100)
+ // MARK: Web
+ case .web(let url):
+ WebView(url: url, viewModel: viewModel)
+ // MARK: Unknown
+ case .unknown(let url):
+ UnknownView(url: url, viewModel: viewModel)
+ Spacer()
+ // MARK: Discussion
+ case let .discussion(blockID, blockKey, title):
+ VStack {
+ if showDiscussion {
+ DiscussionView(
+ id: viewModel.id,
+ blockID: blockID,
+ blockKey: blockKey,
+ title: title,
+ viewModel: viewModel
+ )
+ Spacer(minLength: 100)
+ } else {
+ DiscussionView(
+ id: viewModel.id,
+ blockID: blockID,
+ blockKey: blockKey,
+ title: title,
+ viewModel: viewModel
+ ).drawingGroup()
+ Spacer(minLength: 100)
+ }
+ }.frameLimit()
}
- }).frame(width: 215).padding(.top, 40)
- }.padding(24)
- Spacer()
- case let .discussion(blockID):
- let id = "course-v1:"
- + (viewModel.lessonID.find(from: "block-v1:", to: "+type").first ?? "")
- PostsView(courseID: id,
- currentBlockID: blockID,
- topics: Topics(coursewareTopics: [],
- nonCoursewareTopics: []),
- title: "", type: .courseTopics(topicID: blockID),
- viewModel: Container.shared.resolve(PostsViewModel.self)!,
- router: Container.shared.resolve(DiscussionRouter.self)!,
- showTopMenu: false)
- .onAppear {
- Task {
- await viewModel.blockCompletionRequest(blockID: blockID)
+ } else {
+ EmptyView()
}
}
- default:
- VStack {}
+ .frame(height: reader.size.height)
+ .id(index)
}
- } else {
- VStack(spacing: 28) {
- Image(systemName: "wifi").resizable()
- .scaledToFit()
- .frame(width: 100)
- Text(CourseLocalization.Error.noInternet)
- .multilineTextAlignment(.center)
- .padding(.horizontal, 20)
- UnitButtonView(type: .reload, action: {
- self.viewModel.createLessonType()
- killPlayer.toggle()
- }).frame(width: 100)
- }.frame(maxWidth: .infinity, maxHeight: .infinity)
- }
-
- // MARK: - Alert
- if showAlert {
- ZStack(alignment: .bottomLeading) {
- Spacer()
- HStack(spacing: 6) {
- CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template)
- .onAppear {
- alertMessage = CourseLocalization.Alert.rotateDevice
- }
- Text(alertMessage ?? "")
- }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor,
- textColor: .white)
- .transition(.move(edge: .bottom))
- .onAppear {
- doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
- alertMessage = nil
- showAlert = false
+ }
+ .offset(y: offsetView)
+ .clipped()
+ .onChange(of: viewModel.index, perform: { index in
+ DispatchQueue.main.async {
+ withAnimation(Animation.easeInOut(duration: 0.2)) {
+ offsetView = -(reader.size.height * CGFloat(index))
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ showDiscussion = viewModel.selectedLesson().type == .discussion
}
}
}
- }
-
- // MARK: - Course Navigation
- CourseNavigationView(
- sectionName: sectionName,
- viewModel: viewModel,
- killPlayer: $killPlayer
- ).padding(.vertical, 12)
- .frameLimit(sizePortrait: 420)
- .background(
- CoreAssets.background.swiftUIColor
- .ignoresSafeArea()
- .shadow(color: CoreAssets.shadowColor.swiftUIColor, radius: 4, y: -2)
- )
- }.frame(maxWidth: .infinity)
+
+ })
+ } else {
+
+ // MARK: No internet view
+ VStack(spacing: 28) {
+ Image(systemName: "wifi").resizable()
+ .scaledToFit()
+ .frame(width: 100)
+ Text(CourseLocalization.Error.noInternet)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 20)
+ UnitButtonView(type: .reload, action: {
+ playerStateSubject.send(VideoPlayerState.kill)
+ }).frame(width: 100)
+ }.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}.frame(maxWidth: .infinity)
- .onRightSwipeGesture {
- viewModel.router.back()
- killPlayer.toggle()
+ .clipped()
+
+ // MARK: Progress Dots
+ if viewModel.verticals[viewModel.verticalIndex].childs.count > 1 {
+ LessonProgressView(viewModel: viewModel)
+ }
+ }
+ // MARK: - Alert
+ if showAlert {
+ ZStack(alignment: .bottomLeading) {
+ Spacer()
+ HStack(spacing: 6) {
+ CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template)
+ .onAppear {
+ alertMessage = CourseLocalization.Alert.rotateDevice
+ }
+ Text(alertMessage ?? "")
+ }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor,
+ textColor: .white)
+ .transition(.move(edge: .bottom))
+ .onAppear {
+ doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
+ alertMessage = nil
+ showAlert = false
+ }
}
-
+ }
}
- }
- .background(
- CoreAssets.background.swiftUIColor
- .ignoresSafeArea()
- )
+
+ // MARK: - Course Navigation
+ VStack {
+ NavigationBar(
+ title: "",
+ leftButtonAction: {
+ viewModel.router.back()
+ playerStateSubject.send(VideoPlayerState.kill)
+ }).padding(.top, 50)
+ Spacer()
+ CourseNavigationView(
+ sectionName: sectionName,
+ viewModel: viewModel,
+ playerStateSubject: playerStateSubject
+ ).padding(.bottom, 30)
+ .frameLimit(sizePortrait: 420)
+ }.frame(maxWidth: .infinity)
+ .onRightSwipeGesture {
+ playerStateSubject.send(VideoPlayerState.kill)
+ viewModel.router.back()
+ }
+ }
+ }.ignoresSafeArea()
+ .background(
+ CoreAssets.background.swiftUIColor
+ .ignoresSafeArea()
+ )
}
}
#if DEBUG
//swiftlint:disable all
-struct LessonView_Previews: PreviewProvider {
+struct CourseUnitView_Previews: PreviewProvider {
static var previews: some View {
let blocks = [
- CourseBlock(blockId: "1",
- id: "1",
- topicId: "1",
- graded: false,
- completion: 0,
- type: .vertical,
- displayName: "Lesson 1",
- studentUrl: "1",
- videoUrl: nil,
- youTubeUrl: nil),
- CourseBlock(blockId: "2",
- id: "2",
- topicId: "2",
- graded: false,
+ CourseBlock(
+ blockId: "1",
+ id: "1",
+ topicId: "1",
+ graded: false,
+ completion: 0,
+ type: .video,
+ displayName: "Lesson 1",
+ studentUrl: "",
+ videoUrl: nil,
+ youTubeUrl: nil
+ ),
+ CourseBlock(
+ blockId: "2",
+ id: "2",
+ topicId: "2",
+ graded: false,
+ completion: 0,
+ type: .video,
+ displayName: "Lesson 2",
+ studentUrl: "2",
+ videoUrl: nil,
+ youTubeUrl: nil
+ ),
+ CourseBlock(
+ blockId: "3",
+ id: "3",
+ topicId: "3",
+ graded: false,
+ completion: 0,
+ type: .unknown,
+ displayName: "Lesson 3",
+ studentUrl: "3",
+ videoUrl: nil,
+ youTubeUrl: nil
+ ),
+ CourseBlock(
+ blockId: "4",
+ id: "4",
+ topicId: "4",
+ graded: false,
+ completion: 0,
+ type: .unknown,
+ displayName: "4",
+ studentUrl: "4",
+ videoUrl: nil,
+ youTubeUrl: nil
+ ),
+ ]
+
+ let chapters = [
+ CourseChapter(
+ blockId: "0",
+ id: "0",
+ displayName: "0",
+ type: .chapter,
+ childs: [
+ CourseSequential(
+ blockId: "5",
+ id: "5",
+ displayName: "5",
+ type: .sequential,
completion: 0,
- type: .chapter,
- displayName: "Lesson 2",
- studentUrl: "2",
- videoUrl: nil,
- youTubeUrl: nil),
- CourseBlock(blockId: "3",
+ childs: [
+ CourseVertical(
+ blockId: "6", id: "6",
+ displayName: "6",
+ type: .vertical,
+ completion: 0,
+ childs: blocks
+ )
+ ]
+ )
+
+ ]),
+ CourseChapter(
+ blockId: "2",
+ id: "2",
+ displayName: "2",
+ type: .chapter,
+ childs: [
+ CourseSequential(
+ blockId: "3",
id: "3",
- topicId: "3",
- graded: false,
- completion: 0,
- type: .vertical,
- displayName: "Lesson 3",
- studentUrl: "3",
- videoUrl: nil,
- youTubeUrl: nil),
- CourseBlock(blockId: "4",
- id: "4",
- topicId: "4",
- graded: false,
+ displayName: "3",
+ type: .sequential,
completion: 0,
- type: .vertical,
- displayName: "4",
- studentUrl: "4",
- videoUrl: nil,
- youTubeUrl: nil),
+ childs: [
+ CourseVertical(
+ blockId: "4", id: "4",
+ displayName: "4",
+ type: .vertical,
+ completion: 0,
+ childs: blocks
+ )
+ ]
+ )
+
+ ])
]
return CourseUnitView(viewModel: CourseUnitViewModel(
- lessonID: "", courseID: "", blocks: blocks,
+ lessonID: "",
+ courseID: "",
+ id: "1",
+ chapters: chapters,
+ chapterIndex: 0,
+ sequentialIndex: 0,
+ verticalIndex: 0,
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
connectivity: Connectivity(),
diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift
index 2d859ae5c..2705f2e3a 100644
--- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift
+++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift
@@ -5,7 +5,7 @@
// Created by Stepanok Ivan on 05.10.2022.
//
-import Foundation
+import SwiftUI
import Core
public enum LessonType: Equatable {
@@ -13,7 +13,7 @@ public enum LessonType: Equatable {
case youtube(viewYouTubeUrl: String, blockID: String)
case video(videoUrl: String, blockID: String)
case unknown(String)
- case discussion(String)
+ case discussion(String, String, String)
static func from(_ block: CourseBlock) -> Self {
switch block.type {
@@ -22,7 +22,7 @@ public enum LessonType: Equatable {
case .html:
return .web(block.studentUrl)
case .discussion:
- return .discussion(block.topicId ?? "")
+ return .discussion(block.topicId ?? "", block.id, block.displayName)
case .video:
if block.youTubeUrl != nil, let encodedVideo = block.videoUrl {
return .video(videoUrl: encodedVideo, blockID: block.id)
@@ -42,11 +42,17 @@ public enum LessonType: Equatable {
public class CourseUnitViewModel: ObservableObject {
- public var blocks: [CourseBlock]
+ enum LessonAction {
+ case next
+ case previous
+ }
+
+ var verticals: [CourseVertical]
+ var verticalIndex: Int
+
@Published var index: Int = 0
- @Published var previousLesson: String = ""
- @Published var nextLesson: String = ""
- @Published var lessonType: LessonType?
+ var previousLesson: String = ""
+ var nextLesson: String = ""
@Published var showError: Bool = false
var errorMessage: String? {
didSet {
@@ -54,64 +60,65 @@ public class CourseUnitViewModel: ObservableObject {
}
}
- public var lessonID: String
- public var courseID: String
-
+ var lessonID: String
+ var courseID: String
+ var id: String
+
private let interactor: CourseInteractorProtocol
- public let router: CourseRouter
- public let connectivity: ConnectivityProtocol
+ let router: CourseRouter
+ let connectivity: ConnectivityProtocol
private let manager: DownloadManagerProtocol
private var subtitlesDownloaded: Bool = false
+ let chapters: [CourseChapter]
+ let chapterIndex: Int
+ let sequentialIndex: Int
func loadIndex() {
index = selectLesson()
}
- public init(lessonID: String,
- courseID: String,
- blocks: [CourseBlock],
- interactor: CourseInteractorProtocol,
- router: CourseRouter,
- connectivity: ConnectivityProtocol,
- manager: DownloadManagerProtocol
+ public init(
+ lessonID: String,
+ courseID: String,
+ id: String,
+ chapters: [CourseChapter],
+ chapterIndex: Int,
+ sequentialIndex: Int,
+ verticalIndex: Int,
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ connectivity: ConnectivityProtocol,
+ manager: DownloadManagerProtocol
) {
self.lessonID = lessonID
self.courseID = courseID
- self.blocks = blocks
+ self.id = id
+ self.chapters = chapters
+ self.chapterIndex = chapterIndex
+ self.sequentialIndex = sequentialIndex
+ self.verticalIndex = verticalIndex
+ self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs
self.interactor = interactor
self.router = router
self.connectivity = connectivity
self.manager = manager
}
- public func languages() -> [SubtitleUrl] {
- return blocks.first(where: { $0.id == lessonID })?.subtitles ?? []
- }
-
private func selectLesson() -> Int {
- guard blocks.count > 0 else { return 0 }
- let index = blocks.firstIndex(where: { $0.id == lessonID }) ?? 0
+ guard verticals[verticalIndex].childs.count > 0 else { return 0 }
+ let index = verticals[verticalIndex].childs.firstIndex(where: { $0.id == lessonID }) ?? 0
nextTitles()
return index
}
func selectedLesson() -> CourseBlock {
- return blocks[index]
- }
-
- func createLessonType() {
- self.lessonType = LessonType.from(blocks[index])
- }
-
- enum LessonAction {
- case next
- case previous
+ return verticals[verticalIndex].childs[index]
}
func select(move: LessonAction) {
switch move {
case .next:
- if index != blocks.count - 1 { index += 1 }
+ if index != verticals[verticalIndex].childs.count - 1 { index += 1 }
nextTitles()
case .previous:
if index != 0 { index -= 1 }
@@ -121,9 +128,8 @@ public class CourseUnitViewModel: ObservableObject {
@MainActor
func blockCompletionRequest(blockID: String) async {
- let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@discussion+block@\(blockID)"
do {
- try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID)
+ try await interactor.blockCompletionRequest(courseID: self.id, blockID: blockID)
} catch let error {
if error.isInternetError || error is NoCachedDataError {
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
@@ -135,20 +141,18 @@ public class CourseUnitViewModel: ObservableObject {
func nextTitles() {
if index != 0 {
- previousLesson = blocks[index - 1].displayName
+ previousLesson = verticals[verticalIndex].childs[index - 1].displayName
} else {
previousLesson = ""
}
- if index != blocks.count - 1 {
- nextLesson = blocks[index + 1].displayName
+ if index != verticals[verticalIndex].childs.count - 1 {
+ nextLesson = verticals[verticalIndex].childs[index + 1].displayName
} else {
nextLesson = ""
}
}
- public func urlForVideoFileOrFallback(blockId: String, url: String) -> URL? {
- guard let block = blocks.first(where: { $0.id == blockId }) else { return nil }
-
+ func urlForVideoFileOrFallback(blockId: String, url: String) -> URL? {
if let fileURL = manager.fileUrl(for: blockId) {
return fileURL
} else {
diff --git a/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift b/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift
new file mode 100644
index 000000000..ed46e69b8
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift
@@ -0,0 +1,37 @@
+//
+// DiscussionView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Core
+import Discussion
+import Swinject
+
+struct DiscussionView: View {
+ let id: String
+ let blockID: String
+ let blockKey: String
+ let title: String
+ let viewModel: CourseUnitViewModel
+
+ var body: some View {
+ PostsView(
+ courseID: id,
+ currentBlockID: blockID,
+ topics: Topics(coursewareTopics: [], nonCoursewareTopics: []),
+ title: title,
+ type: .courseTopics(topicID: blockID),
+ viewModel: Container.shared.resolve(PostsViewModel.self)!,
+ router: Container.shared.resolve(DiscussionRouter.self)!,
+ showTopMenu: false
+ )
+ .onAppear {
+ Task {
+ await viewModel.blockCompletionRequest(blockID: blockKey)
+ }
+ }
+ }
+}
diff --git a/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift
new file mode 100644
index 000000000..1bdc629fa
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift
@@ -0,0 +1,41 @@
+//
+// EncodedVideoView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Core
+import Combine
+import Swinject
+
+struct EncodedVideoView: View {
+
+ let name: String
+ let url: URL?
+ let courseID: String
+ let blockID: String
+ let playerStateSubject: CurrentValueSubject
+ let languages: [SubtitleUrl]
+ let isOnScreen: Bool
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(name)
+ .font(Theme.Fonts.titleLarge)
+ .padding(.horizontal, 24)
+
+ let vm = Container.shared.resolve(
+ EncodedVideoPlayerViewModel.self,
+ arguments: url,
+ blockID,
+ courseID,
+ languages,
+ playerStateSubject
+ )!
+ EncodedVideoPlayer(viewModel: vm, isOnScreen: isOnScreen)
+ Spacer(minLength: 100)
+ }
+ }
+}
diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift
new file mode 100644
index 000000000..37dcb67d3
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift
@@ -0,0 +1,42 @@
+//
+// LessonProgressView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Core
+
+struct LessonProgressView: View {
+ @ObservedObject var viewModel: CourseUnitViewModel
+
+ init(viewModel: CourseUnitViewModel) {
+ self.viewModel = viewModel
+ }
+
+ var body: some View {
+ HStack {
+ Spacer()
+ VStack {
+ Spacer()
+ let childs = viewModel.verticals[viewModel.verticalIndex].childs
+ ForEach(Array(childs.enumerated()), id: \.offset) { index, _ in
+ let selected = viewModel.verticals[viewModel.verticalIndex].childs[index]
+ Circle()
+ .frame(
+ width: selected == viewModel.selectedLesson() ? 5 : 3,
+ height: selected == viewModel.selectedLesson() ? 5 : 3
+ )
+ .foregroundColor(
+ selected == viewModel.selectedLesson()
+ ? .accentColor
+ : CoreAssets.textSecondary.swiftUIColor
+ )
+ }
+ Spacer()
+ }
+ .padding(.trailing, 6)
+ }
+ }
+}
diff --git a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift
new file mode 100644
index 000000000..4f25de9da
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift
@@ -0,0 +1,38 @@
+//
+// UnknownView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Core
+
+struct UnknownView: View {
+ let url: String
+ let viewModel: CourseUnitViewModel
+
+ var body: some View {
+ VStack(spacing: 0) {
+ CoreAssets.notAvaliable.swiftUIImage
+ Text(CourseLocalization.NotAvaliable.title)
+ .font(Theme.Fonts.titleLarge)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity)
+ .padding(.top, 40)
+ Text(CourseLocalization.NotAvaliable.description)
+ .font(Theme.Fonts.bodyLarge)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity)
+ .padding(.top, 12)
+ StyledButton(CourseLocalization.NotAvaliable.button, action: {
+ if let url = URL(string: url) {
+ UIApplication.shared.open(url)
+ }
+ })
+ .frame(width: 215)
+ .padding(.top, 40)
+ }
+ .padding(24)
+ }
+}
diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift
new file mode 100644
index 000000000..9cdc59269
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift
@@ -0,0 +1,23 @@
+//
+// WebView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Swinject
+import Core
+
+struct WebView: View {
+ let url: String
+ let viewModel: CourseUnitViewModel
+
+ var body: some View {
+ VStack(spacing: 0) {
+ WebUnitView(url: url, viewModel: Container.shared.resolve(WebUnitViewModel.self)!)
+ Spacer(minLength: 5)
+ }
+ .roundedBackground(strokeColor: .clear, maxIpadWidth: .infinity)
+ }
+}
diff --git a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift
new file mode 100644
index 000000000..8aeab7f13
--- /dev/null
+++ b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift
@@ -0,0 +1,43 @@
+//
+// YouTubeView.swift
+// Course
+//
+// Created by Stepanok Ivan on 30.05.2023.
+//
+
+import SwiftUI
+import Core
+import Combine
+import Swinject
+
+struct YouTubeView: View {
+
+ let name: String
+ let url: String
+ let courseID: String
+ let blockID: String
+ let playerStateSubject: CurrentValueSubject
+ let languages: [SubtitleUrl]
+ let isOnScreen: Bool
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ VStack(alignment: .leading) {
+ Text(name)
+ .font(Theme.Fonts.titleLarge)
+ .padding(.horizontal, 24)
+
+ let vm = Container.shared.resolve(
+ YouTubeVideoPlayerViewModel.self,
+ arguments: url,
+ blockID,
+ courseID,
+ languages,
+ playerStateSubject
+ )!
+ YouTubeVideoPlayer(viewModel: vm, isOnScreen: isOnScreen)
+ Spacer(minLength: 100)
+ }.background(CoreAssets.background.swiftUIColor)
+ }
+ }
+}
diff --git a/Course/Course/Presentation/Unit/UnitButtonView.swift b/Course/Course/Presentation/Unit/UnitButtonView.swift
deleted file mode 100644
index b2494aa80..000000000
--- a/Course/Course/Presentation/Unit/UnitButtonView.swift
+++ /dev/null
@@ -1,159 +0,0 @@
-//
-// UnitButtonView.swift
-// Course
-//
-// Created by Stepanok Ivan on 14.02.2023.
-//
-
-import SwiftUI
-import Core
-
-struct UnitButtonView: View {
-
- enum UnitButtonType {
- case first
- case next
- case previous
- case last
- case finish
- case reload
- case continueLesson
-
- func stringValue() -> String {
- switch self {
- case .first:
- return CourseLocalization.Courseware.next
- case .next:
- return CourseLocalization.Courseware.next
- case .previous:
- return CourseLocalization.Courseware.previous
- case .last:
- return CourseLocalization.Courseware.finish
- case .finish:
- return CourseLocalization.Courseware.finish
- case .reload:
- return CourseLocalization.Error.reload
- case .continueLesson:
- return CourseLocalization.Courseware.continue
- }
- }
- }
-
- private let action: () -> Void
- private let type: UnitButtonType
-
- init(type: UnitButtonType, action: @escaping () -> Void) {
- self.action = action
- self.type = type
- }
-
- var body: some View {
- HStack {
- Button(action: action) {
- VStack {
- switch type {
- case .first:
- HStack {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .font(Theme.Fonts.labelLarge)
- CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .rotationEffect(Angle.degrees(180))
- }
- case .next:
- HStack {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .padding(.leading, 20)
- .font(Theme.Fonts.labelLarge)
- Spacer()
- CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .rotationEffect(Angle.degrees(180))
- .padding(.trailing, 20)
- }
- case .previous:
- HStack {
- CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
- .padding(.leading, 20)
- .foregroundColor(CoreAssets.accentColor.swiftUIColor)
- Spacer()
- Text(type.stringValue())
- .foregroundColor(CoreAssets.accentColor.swiftUIColor)
- .font(Theme.Fonts.labelLarge)
- .padding(.trailing, 20)
- }
- case .last:
- HStack {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .padding(.leading, 16)
- .font(Theme.Fonts.labelLarge)
- Spacer()
- CoreAssets.check.swiftUIImage.renderingMode(.template)
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .padding(.trailing, 16)
- }
- case .finish:
- HStack {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .font(Theme.Fonts.labelLarge)
- CoreAssets.check.swiftUIImage.renderingMode(.template)
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- }
- case .reload:
- VStack(alignment: .center) {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.accentColor.swiftUIColor)
- .font(Theme.Fonts.labelLarge)
- }
- case .continueLesson:
- HStack {
- Text(type.stringValue())
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .padding(.leading, 20)
- .font(Theme.Fonts.labelLarge)
- CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template)
- .foregroundColor(CoreAssets.styledButtonText.swiftUIColor)
- .rotationEffect(Angle.degrees(180))
- .padding(.trailing, 20)
- }
- }
- }
- .frame(maxWidth: .infinity, minHeight: 48)
- .background(
- VStack {
- if self.type == .reload {
- Theme.Shapes.buttonShape
- .fill(.clear)
- } else {
- Theme.Shapes.buttonShape
- .fill(type == .previous ? .clear : CoreAssets.accentColor.swiftUIColor)
- }
- }
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1))
- .foregroundColor(CoreAssets.accentColor.swiftUIColor)
- )
- }
- }
- }
-}
-
-struct UnitButtonView_Previews: PreviewProvider {
- static var previews: some View {
- VStack {
- UnitButtonView(type: .first, action: {})
- UnitButtonView(type: .previous, action: {})
- UnitButtonView(type: .next, action: {})
- UnitButtonView(type: .last, action: {})
- UnitButtonView(type: .finish, action: {})
- UnitButtonView(type: .reload, action: {})
- UnitButtonView(type: .continueLesson, action: {})
- }
- }
-}
diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift
index 0eaaad718..a3ddda18f 100644
--- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift
+++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift
@@ -9,17 +9,20 @@ import SwiftUI
import _AVKit_SwiftUI
import Core
import Swinject
+import Combine
+
+public enum VideoPlayerState {
+ case pause
+ case kill
+}
public struct EncodedVideoPlayer: View {
- @ObservedObject
- private var viewModel = Container.shared.resolve(VideoPlayerViewModel.self)!
+ @StateObject
+ private var viewModel: EncodedVideoPlayerViewModel
- private var blockID: String
- private var courseID: String
- private let languages: [SubtitleUrl]
+ private var isOnScreen: Bool
- private var controller = AVPlayerViewController()
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
@State private var isLoading: Bool = true
@@ -27,7 +30,7 @@ public struct EncodedVideoPlayer: View {
@State private var isViewedOnce: Bool = false
@State private var currentTime: Double = 0
@State private var isOrientationChanged: Bool = false
- @Binding private var killPlayer: Bool
+
@State var showAlert = false
@State var alertMessage: String? {
didSet {
@@ -36,33 +39,26 @@ public struct EncodedVideoPlayer: View {
}
}
}
- private let url: URL?
public init(
- url: URL?,
- blockID: String,
- courseID: String,
- languages: [SubtitleUrl],
- killPlayer: Binding
+ viewModel: EncodedVideoPlayerViewModel,
+ isOnScreen: Bool
) {
- self.url = url
- self.blockID = blockID
- self.courseID = courseID
- self.languages = languages
- self._killPlayer = killPlayer
+ self._viewModel = StateObject(wrappedValue: { viewModel }())
+ self.isOnScreen = isOnScreen
}
public var body: some View {
ZStack {
VStack(alignment: .leading) {
PlayerViewController(
- videoURL: url,
- controller: controller,
+ videoURL: viewModel.url,
+ controller: viewModel.controller,
progress: { progress in
if progress >= 0.8 {
if !isViewedOnce {
Task {
- await viewModel.blockCompletionRequest(blockID: blockID, courseID: courseID)
+ await viewModel.blockCompletionRequest()
}
isViewedOnce = true
}
@@ -75,23 +71,26 @@ public struct EncodedVideoPlayer: View {
.padding(.horizontal, 6)
.onReceive(NotificationCenter.Publisher(
center: .default,
- name: UIDevice.orientationDidChangeNotification)) { _ in
+ name: UIDevice.orientationDidChangeNotification)
+ ) { _ in
+ if isOnScreen {
self.orientation = UIDevice.current.orientation
if self.orientation.isLandscape {
- controller.enterFullScreen(animated: true)
- controller.player?.play()
+ viewModel.controller.enterFullScreen(animated: true)
+ viewModel.controller.player?.play()
isOrientationChanged = true
} else {
if isOrientationChanged {
- controller.exitFullScreen(animated: true)
- controller.player?.pause()
+ viewModel.controller.exitFullScreen(animated: true)
+ viewModel.controller.player?.pause()
isOrientationChanged = false
}
}
}
- SubtittlesView(languages: languages,
- currentTime: $currentTime,
- viewModel: viewModel)
+ }
+ SubtittlesView(languages: viewModel.languages,
+ currentTime: $currentTime,
+ viewModel: viewModel)
Spacer()
if !orientation.isLandscape || idiom != .pad {
VStack {}.onAppear {
@@ -99,23 +98,21 @@ public struct EncodedVideoPlayer: View {
alertMessage = CourseLocalization.Alert.rotateDevice
}
}
- }.onChange(of: killPlayer, perform: { _ in
- controller.player?.pause()
- controller.player?.replaceCurrentItem(with: nil)
- })
+ }
+
// MARK: - Alert
- if showAlert {
+ if showAlert, let alertMessage {
VStack(alignment: .center) {
Spacer()
HStack(spacing: 6) {
CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template)
- Text(alertMessage ?? "")
+ Text(alertMessage)
}.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor,
textColor: .white)
.transition(.move(edge: .bottom))
.onAppear {
doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
- alertMessage = nil
+ self.alertMessage = nil
showAlert = false
}
}
@@ -125,8 +122,22 @@ public struct EncodedVideoPlayer: View {
}
}
+#if DEBUG
struct EncodedVideoPlayer_Previews: PreviewProvider {
static var previews: some View {
- EncodedVideoPlayer(url: nil, blockID: "", courseID: "", languages: [], killPlayer: .constant(false))
+ EncodedVideoPlayer(
+ viewModel: EncodedVideoPlayerViewModel(
+ url: URL(string: "")!,
+ blockID: "",
+ courseID: "",
+ languages: [],
+ playerStateSubject: CurrentValueSubject(nil),
+ interactor: CourseInteractor(repository: CourseRepositoryMock()),
+ router: CourseRouterMock(),
+ connectivity: Connectivity()
+ ),
+ isOnScreen: true
+ )
}
}
+#endif
diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift
new file mode 100644
index 000000000..b75a57384
--- /dev/null
+++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift
@@ -0,0 +1,49 @@
+//
+// EncodedVideoPlayerViewModel.swift
+// Course
+//
+// Created by Stepanok Ivan on 24.05.2023.
+//
+
+import _AVKit_SwiftUI
+import Core
+import Combine
+
+public class EncodedVideoPlayerViewModel: VideoPlayerViewModel {
+
+ let url: URL?
+
+ let controller = AVPlayerViewController()
+ private var subscription = Set()
+
+ public init(
+ url: URL?,
+ blockID: String,
+ courseID: String,
+ languages: [SubtitleUrl],
+ playerStateSubject: CurrentValueSubject,
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ connectivity: ConnectivityProtocol
+ ) {
+ self.url = url
+
+ super.init(blockID: blockID,
+ courseID: courseID,
+ languages: languages,
+ interactor: interactor,
+ router: router,
+ connectivity: connectivity)
+
+ playerStateSubject.sink(receiveValue: { [weak self] state in
+ switch state {
+ case .pause:
+ self?.controller.player?.pause()
+ case .kill:
+ self?.controller.player?.replaceCurrentItem(with: nil)
+ case .none:
+ break
+ }
+ }).store(in: &subscription)
+ }
+}
diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift
index e67143ca8..ef856ff04 100644
--- a/Course/Course/Presentation/Video/PlayerViewController.swift
+++ b/Course/Course/Presentation/Video/PlayerViewController.swift
@@ -12,12 +12,14 @@ struct PlayerViewController: UIViewControllerRepresentable {
var videoURL: URL?
var controller: AVPlayerViewController
- public var progress: ((Float) -> Void)
- public var seconds: ((Double) -> Void)
+ var progress: ((Float) -> Void)
+ var seconds: ((Double) -> Void)
- init(videoURL: URL?, controller: AVPlayerViewController,
- progress: @escaping ((Float) -> Void),
- seconds: @escaping ((Double) -> Void)) {
+ init(
+ videoURL: URL?, controller: AVPlayerViewController,
+ progress: @escaping ((Float) -> Void),
+ seconds: @escaping ((Double) -> Void)
+ ) {
self.videoURL = videoURL
self.controller = controller
self.progress = progress
@@ -27,33 +29,45 @@ struct PlayerViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> AVPlayerViewController {
controller.modalPresentationStyle = .fullScreen
controller.allowsPictureInPicturePlayback = true
+ controller.player = AVPlayer()
- addPeriodicTimeObserver(controller, currentProgress: { progress, seconds in
- self.progress(progress)
- self.seconds(seconds)
- })
+ addPeriodicTimeObserver(
+ controller,
+ currentProgress: { progress, seconds in
+ self.progress(progress)
+ self.seconds(seconds)
+ }
+ )
return controller
}
- private func addPeriodicTimeObserver(_ controller: AVPlayerViewController,
- currentProgress: @escaping ((Float, Double) -> Void)) {
- let interval = CMTime(seconds: 0.1,
- preferredTimescale: CMTimeScale(NSEC_PER_SEC))
+ private func addPeriodicTimeObserver(
+ _ controller: AVPlayerViewController,
+ currentProgress: @escaping ((Float, Double) -> Void)
+ ) {
+ let interval = CMTime(
+ seconds: 0.1,
+ preferredTimescale: CMTimeScale(NSEC_PER_SEC)
+ )
self.controller.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
var progress: Float = .zero
let currentSeconds = CMTimeGetSeconds(time)
guard let duration = controller.player?.currentItem?.duration else { return }
let totalSeconds = CMTimeGetSeconds(duration)
- progress = Float(currentSeconds/totalSeconds)
+ progress = Float(currentSeconds / totalSeconds)
currentProgress(progress, currentSeconds)
}
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
DispatchQueue.main.async {
- if (playerController.player?.currentItem?.asset as? AVURLAsset)?.url.absoluteString != videoURL?.absoluteString {
- playerController.player = AVPlayer(url: videoURL!)
+ let asset = playerController.player?.currentItem?.asset as? AVURLAsset
+ if asset?.url.absoluteString != videoURL?.absoluteString {
+ if playerController.player == nil {
+ playerController.player = AVPlayer()
+ }
+ playerController.player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!))
addPeriodicTimeObserver(playerController, currentProgress: { progress, seconds in
self.progress(progress)
self.seconds(seconds)
diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtittlesView.swift
index 493ab4f98..d09e89967 100644
--- a/Course/Course/Presentation/Video/SubtittlesView.swift
+++ b/Course/Course/Presentation/Video/SubtittlesView.swift
@@ -50,40 +50,41 @@ public struct SubtittlesView: View {
})
}
}
- ScrollView {
- if viewModel.subtitles.count > 0 {
- VStack(alignment: .leading, spacing: 0) {
- ForEach(viewModel.subtitles, id: \.id) { subtitle in
- HStack {
- Text(subtitle.text)
- .padding(.vertical, 16)
- .font(Theme.Fonts.bodyMedium)
- .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime))
- ? CoreAssets.textPrimary.swiftUIColor
- : CoreAssets.textSecondary.swiftUIColor)
- .onChange(of: currentTime, perform: { _ in
- if subtitle.fromTo.contains(Date(milliseconds: currentTime)) {
- if id != subtitle.id {
- withAnimation {
- scroll.scrollTo(subtitle.id, anchor: .top)
+ ZStack {
+ ScrollView {
+ if viewModel.subtitles.count > 0 {
+ VStack(alignment: .leading, spacing: 0) {
+ ForEach(viewModel.subtitles, id: \.id) { subtitle in
+ HStack {
+ Text(subtitle.text)
+ .padding(.vertical, 16)
+ .font(Theme.Fonts.bodyMedium)
+ .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime))
+ ? CoreAssets.textPrimary.swiftUIColor
+ : CoreAssets.textSecondary.swiftUIColor)
+ .onChange(of: currentTime, perform: { _ in
+ if subtitle.fromTo.contains(Date(milliseconds: currentTime)) {
+ if id != subtitle.id {
+ withAnimation {
+ scroll.scrollTo(subtitle.id, anchor: .top)
+ }
}
+ self.id = subtitle.id
}
- self.id = subtitle.id
- }
- })
- }.id(subtitle.id)
+ })
+ }.id(subtitle.id)
+ }
}
+ .introspectScrollView(customize: { scroll in
+ scroll.isScrollEnabled = false
+ })
}
}
- }.introspectScrollView(customize: { scroll in
- scroll.isScrollEnabled = false
- })
+ // Forced disable scrolling for iOS 14, 15
+ Color.white.opacity(0)
+ }
}.padding(.horizontal, 24)
.padding(.top, 34)
- .onAppear {
- viewModel.languages = languages
- viewModel.prepareLanguages()
- }
}
}
}
@@ -92,12 +93,18 @@ public struct SubtittlesView: View {
struct SubtittlesView_Previews: PreviewProvider {
static var previews: some View {
- SubtittlesView(languages: [SubtitleUrl(language: "fr", url: "url"),
- SubtitleUrl(language: "uk", url: "url2")],
- currentTime: .constant(0),
- viewModel: VideoPlayerViewModel(interactor: CourseInteractor(repository: CourseRepositoryMock()),
- router: CourseRouterMock(),
- connectivity: Connectivity()))
+ SubtittlesView(
+ languages: [SubtitleUrl(language: "fr", url: "url"),
+ SubtitleUrl(language: "uk", url: "url2")],
+ currentTime: .constant(0),
+ viewModel: VideoPlayerViewModel(
+ blockID: "", courseID: "",
+ languages: [],
+ interactor: CourseInteractor(repository: CourseRepositoryMock()),
+ router: CourseRouterMock(),
+ connectivity: Connectivity()
+ )
+ )
}
}
#endif
diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift
index 021730358..7aab1c567 100644
--- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift
+++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift
@@ -7,16 +7,20 @@
import Foundation
import Core
+import _AVKit_SwiftUI
public class VideoPlayerViewModel: ObservableObject {
+ private var blockID: String
+ private var courseID: String
+
private let interactor: CourseInteractorProtocol
public let connectivity: ConnectivityProtocol
public let router: CourseRouter
private var subtitlesDownloaded: Bool = false
@Published var subtitles: [Subtitle] = []
- @Published var languages: [SubtitleUrl] = []
+ var languages: [SubtitleUrl]
@Published var items: [PickerItem] = []
@Published var selectedLanguage: String?
@@ -27,16 +31,25 @@ public class VideoPlayerViewModel: ObservableObject {
}
}
- public init(interactor: CourseInteractorProtocol,
- router: CourseRouter,
- connectivity: ConnectivityProtocol) {
+ public init(
+ blockID: String,
+ courseID: String,
+ languages: [SubtitleUrl],
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ connectivity: ConnectivityProtocol
+ ) {
+ self.blockID = blockID
+ self.courseID = courseID
+ self.languages = languages
self.interactor = interactor
self.router = router
self.connectivity = connectivity
+ self.prepareLanguages()
}
@MainActor
- func blockCompletionRequest(blockID: String, courseID: String) async {
+ func blockCompletionRequest() async {
let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@discussion+block@\(blockID)"
do {
try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID)
@@ -51,8 +64,15 @@ public class VideoPlayerViewModel: ObservableObject {
@MainActor
public func getSubtitles(subtitlesUrl: String) async {
- guard let result = try? await interactor.getSubtitles(url: subtitlesUrl) else { return }
- subtitles = result
+ do {
+ let result = try await interactor.getSubtitles(
+ url: subtitlesUrl,
+ selectedLanguage: self.selectedLanguage ?? "en"
+ )
+ subtitles = result
+ } catch {
+ print(">>>>> ⛔️⛔️⛔️⛔️⛔️⛔️⛔️⛔️", error)
+ }
}
public func prepareLanguages() {
diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift
index 3662ba357..b8cc5d335 100644
--- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift
+++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift
@@ -13,161 +13,83 @@ import Swinject
public struct YouTubeVideoPlayer: View {
- private let viewModel = Container.shared.resolve(VideoPlayerViewModel.self)!
+ @StateObject
+ private var viewModel: YouTubeVideoPlayerViewModel
+ private var isOnScreen: Bool
- private var blockID: String
- private var courseID: String
- private let languages: [SubtitleUrl]
-
- private let youtubePlayer: YouTubePlayer
- private var timePublisher: AnyPublisher
- private var durationPublisher: AnyPublisher
- private var currentTimePublisher: AnyPublisher
- private var currentStatePublisher: AnyPublisher
- private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
-
- @State private var duration: Double?
- @State private var play = false
- @State private var orientation = UIDevice.current.orientation
- @State private var isLoading: Bool = true
- @State private var isViewedOnce: Bool = false
- @State private var currentTime: Double = 0
- @State private var showAlert = false
- @State private var alertMessage: String? {
+ @State
+ private var showAlert = false
+ @State
+ private var alertMessage: String? {
didSet {
withAnimation {
showAlert = alertMessage != nil
}
}
}
-
- public init(url: String,
- blockID: String,
- courseID: String,
- languages: [SubtitleUrl]
- ) {
- self.blockID = blockID
- self.courseID = courseID
- self.languages = languages
-
- let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "")
- let configuration = YouTubePlayer.Configuration(configure: {
- $0.autoPlay = false
- $0.playInline = true
- $0.showFullscreenButton = true
- $0.allowsPictureInPictureMediaPlayback = false
- $0.showControls = true
- $0.useModestBranding = false
- $0.progressBarColor = .white
- $0.showRelatedVideos = false
- $0.showCaptions = false
- $0.showAnnotations = false
- $0.customUserAgent = """
- Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp)
- AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5
- """
- })
- self.youtubePlayer = YouTubePlayer(source: .video(id: videoID),
- configuration: configuration)
- self.timePublisher = youtubePlayer.currentTimePublisher()
- self.durationPublisher = youtubePlayer.durationPublisher
- self.currentTimePublisher = youtubePlayer.currentTimePublisher(updateInterval: 0.1)
- self.currentStatePublisher = youtubePlayer.playbackStatePublisher
+
+ public init(viewModel: YouTubeVideoPlayerViewModel, isOnScreen: Bool) {
+ self._viewModel = StateObject(wrappedValue: { viewModel }())
+ self.isOnScreen = isOnScreen
}
-
+
public var body: some View {
ZStack {
VStack {
YouTubePlayerView(
- youtubePlayer,
+ viewModel.youtubePlayer,
transaction: .init(animation: .easeIn),
- overlay: { state in
- if state == .ready {
- if idiom == .pad {
- VStack {}.onAppear {
- isLoading = false
- }
- } else {
- VStack {}.onAppear {
- isLoading = false
- alertMessage = CourseLocalization.Alert.rotateDevice
- }
- }
- }
- })
+ overlay: { _ in })
+ .onAppear {
+ alertMessage = CourseLocalization.Alert.rotateDevice
+ }
.cornerRadius(12)
.padding(.horizontal, 6)
- .aspectRatio(16/8.8, contentMode: .fit)
- .onReceive(NotificationCenter
- .Publisher(center: .default,
- name: UIDevice.orientationDidChangeNotification)) { _ in
- self.orientation = UIDevice.current.orientation
- if self.orientation.isPortrait {
- youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: {
- $0.playInline = true
- $0.autoPlay = play
- $0.startTime = Int(currentTime)
- }))
- } else {
- youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: {
- $0.playInline = false
- $0.autoPlay = true
- $0.startTime = Int(currentTime)
- }))
- }
- }
- SubtittlesView(languages: languages,
- currentTime: $currentTime,
- viewModel: viewModel)
-
- }.onReceive(durationPublisher, perform: { duration in
- self.duration = duration
- })
- .onReceive(currentTimePublisher, perform: { time in
- currentTime = time
- })
- .onReceive(currentStatePublisher, perform: { state in
- switch state {
- case .unstarted:
- self.play = false
- case .ended:
- self.play = false
- case .playing:
- self.play = true
- case .paused:
- self.play = false
- case .buffering, .cued:
- break
- }
- })
- .onReceive(timePublisher, perform: { time in
- if let duration {
- if (time / duration) >= 0.8 {
- if !isViewedOnce {
- Task {
- await viewModel.blockCompletionRequest(blockID: blockID, courseID: courseID)
- }
- isViewedOnce = true
+ .aspectRatio(16 / 8.8, contentMode: .fit)
+ .onReceive(NotificationCenter.Publisher(
+ center: .default, name: UIDevice.orientationDidChangeNotification
+ )) { _ in
+ if isOnScreen {
+ let orientation = UIDevice.current.orientation
+ if orientation.isPortrait {
+ viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: {
+ $0.playInline = true
+ $0.autoPlay = viewModel.play
+ $0.startTime = Int(viewModel.currentTime)
+ }))
+ } else {
+ viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: {
+ $0.playInline = false
+ $0.autoPlay = true
+ $0.startTime = Int(viewModel.currentTime)
+ }))
}
}
}
- })
- if isLoading {
+ SubtittlesView(
+ languages: viewModel.languages,
+ currentTime: $viewModel.currentTime,
+ viewModel: viewModel
+ )
+ }
+
+ if viewModel.isLoading {
ProgressBar(size: 40, lineWidth: 8)
}
+
// MARK: - Alert
- if showAlert {
+ if showAlert, let alertMessage {
VStack(alignment: .center) {
Spacer()
HStack(spacing: 6) {
CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template)
- Text(alertMessage ?? "")
+ Text(alertMessage)
}.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor,
textColor: .white)
.transition(.move(edge: .bottom))
.onAppear {
doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
- alertMessage = nil
+ self.alertMessage = nil
showAlert = false
}
}
@@ -177,11 +99,20 @@ public struct YouTubeVideoPlayer: View {
}
}
+#if DEBUG
struct YouTubeVideoPlayer_Previews: PreviewProvider {
static var previews: some View {
- YouTubeVideoPlayer(url: "",
- blockID: "",
- courseID: "",
- languages: [])
+ YouTubeVideoPlayer(
+ viewModel: YouTubeVideoPlayerViewModel(
+ url: "",
+ blockID: "",
+ courseID: "",
+ languages: [],
+ playerStateSubject: CurrentValueSubject(nil),
+ interactor: CourseInteractor(repository: CourseRepositoryMock()),
+ router: CourseRouterMock(),
+ connectivity: Connectivity()),
+ isOnScreen: true)
}
}
+#endif
diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift
new file mode 100644
index 000000000..5aaacf7ff
--- /dev/null
+++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift
@@ -0,0 +1,124 @@
+//
+// YouTubeVideoPlayerViewModel.swift
+// Course
+//
+// Created by Stepanok Ivan on 24.05.2023.
+//
+
+import SwiftUI
+import Core
+import YouTubePlayerKit
+import Combine
+import Swinject
+
+public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {
+
+ @Published var youtubePlayer: YouTubePlayer
+ private (set) var play = false
+ @Published var isLoading: Bool = true
+ @Published var currentTime: Double = 0
+
+ private var subscription = Set()
+ private var duration: Double?
+ private var isViewedOnce: Bool = false
+ private var url: String
+
+ public init(
+ url: String,
+ blockID: String,
+ courseID: String,
+ languages: [SubtitleUrl],
+ playerStateSubject: CurrentValueSubject,
+ interactor: CourseInteractorProtocol,
+ router: CourseRouter,
+ connectivity: ConnectivityProtocol
+ ) {
+ self.url = url
+
+ let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "")
+ let configuration = YouTubePlayer.Configuration(configure: {
+ $0.autoPlay = false
+ $0.playInline = true
+ $0.showFullscreenButton = true
+ $0.allowsPictureInPictureMediaPlayback = false
+ $0.showControls = true
+ $0.useModestBranding = false
+ $0.progressBarColor = .white
+ $0.showRelatedVideos = false
+ $0.showCaptions = false
+ $0.showAnnotations = false
+ $0.customUserAgent = """
+ Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp)
+ AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5
+ """
+ })
+ self.youtubePlayer = YouTubePlayer(source: .video(id: videoID), configuration: configuration)
+
+ super.init(
+ blockID: blockID,
+ courseID: courseID,
+ languages: languages,
+ interactor: interactor,
+ router: router,
+ connectivity: connectivity
+ )
+
+ self.youtubePlayer.pause()
+
+ subscrube(playerStateSubject: playerStateSubject)
+ }
+
+ private func subscrube(playerStateSubject: CurrentValueSubject) {
+ playerStateSubject.sink(receiveValue: { [weak self] state in
+ switch state {
+ case .pause:
+ self?.youtubePlayer.pause()
+ case .kill, .none:
+ break
+ }
+ }).store(in: &subscription)
+
+ youtubePlayer.durationPublisher.sink(receiveValue: { [weak self] duration in
+ self?.duration = duration
+ }).store(in: &subscription)
+
+ youtubePlayer.currentTimePublisher(updateInterval: 0.1).sink(receiveValue: { [weak self] time in
+ guard let self else { return }
+ self.currentTime = time
+
+ if let duration = self.duration {
+ if (time / duration) >= 0.8 {
+ if !isViewedOnce {
+ Task {
+ await self.blockCompletionRequest()
+ }
+ isViewedOnce = true
+ }
+ }
+ }
+ }).store(in: &subscription)
+
+ youtubePlayer.playbackStatePublisher.sink(receiveValue: { [weak self] state in
+ guard let self else { return }
+ switch state {
+ case .unstarted:
+ self.play = false
+ case .ended:
+ self.play = false
+ case .playing:
+ self.play = true
+ case .paused:
+ self.play = false
+ case .buffering, .cued:
+ break
+ }
+ }).store(in: &subscription)
+
+ youtubePlayer.statePublisher.sink(receiveValue: { [weak self] state in
+ guard let self else { return }
+ if state == .ready {
+ self.isLoading = false
+ }
+ }).store(in: &subscription)
+ }
+}
diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift
index 85d149d8b..f5719bf9d 100644
--- a/Course/Course/SwiftGen/Strings.swift
+++ b/Course/Course/SwiftGen/Strings.swift
@@ -29,14 +29,14 @@ public enum CourseLocalization {
public static let finish = CourseLocalization.tr("Localizable", "COURSEWARE.FINISH", fallback: "Finish")
/// Good Work!
public static let goodWork = CourseLocalization.tr("Localizable", "COURSEWARE.GOOD_WORK", fallback: "Good Work!")
- /// is finished.
- public static let isFinished = CourseLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "is finished.")
+ /// “ is finished.
+ public static let isFinished = CourseLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "“ is finished.")
/// Next
public static let next = CourseLocalization.tr("Localizable", "COURSEWARE.NEXT", fallback: "Next")
- /// Previous
- public static let previous = CourseLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Previous")
- /// Section
- public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section")
+ /// Prev
+ public static let previous = CourseLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev")
+ /// Section “
+ public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “")
}
public enum CourseContainer {
/// Course
diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings
index 0db063144..0f5edc88f 100644
--- a/Course/Course/en.lproj/Localizable.strings
+++ b/Course/Course/en.lproj/Localizable.strings
@@ -21,12 +21,12 @@
"COURSEWARE.COURSE_CONTENT" = "Course content";
"COURSEWARE.COURSE_UNITS" = "Course units";
"COURSEWARE.NEXT" = "Next";
-"COURSEWARE.PREVIOUS" = "Previous";
+"COURSEWARE.PREVIOUS" = "Prev";
"COURSEWARE.FINISH" = "Finish";
"COURSEWARE.GOOD_WORK" = "Good Work!";
"COURSEWARE.BACK_TO_OUTLINE" = "Back to outline";
-"COURSEWARE.SECTION" = "Section";
-"COURSEWARE.IS_FINISHED" = "is finished.";
+"COURSEWARE.SECTION" = "Section “";
+"COURSEWARE.IS_FINISHED" = "“ is finished.";
"COURSEWARE.CONTINUE" = "Continue";
"COURSEWARE.CONTINUE_WITH" = "Continue with:";
diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings
index f3cb347c4..cedc987f1 100644
--- a/Course/Course/uk.lproj/Localizable.strings
+++ b/Course/Course/uk.lproj/Localizable.strings
@@ -24,8 +24,8 @@
"COURSEWARE.FINISH" = "Завершити";
"COURSEWARE.GOOD_WORK" = "Гарна робота!";
"COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля";
-"COURSEWARE.SECTION" = "Секція";
-"COURSEWARE.IS_FINISHED" = "завершена.";
+"COURSEWARE.SECTION" = "Секція “";
+"COURSEWARE.IS_FINISHED" = "“ завершена.";
"COURSEWARE.CONTINUE" = "Продовжити";
"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:";
diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift
index 7993fb40e..e909b96b1 100644
--- a/Course/CourseTests/CourseMock.generated.swift
+++ b/Course/CourseTests/CourseMock.generated.swift
@@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock {
perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`)
}
- open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) {
- addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`)))
- let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void
- perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`)
+ open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) {
+ addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`)))
+ let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void
+ perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`)
}
open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) {
@@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock {
case m_showRegisterScreen
case m_showForgotPasswordScreen
case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter)
- case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>)
+ case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter