Skip to content

Commit

Permalink
Course bars: Download videos to device and Select download quality ba…
Browse files Browse the repository at this point in the history
…rs (#239)

* chore: add videos downloading bar view

* chore: add view and logic

* chore: add progress to download data

* chore: downloads view

* chore: add strings and fix delete video notif

* chore: add total progress for bar

* chore: changes for no nested list flow

* fix: showing all downloaded item if open from current course

* fix: bar progress

* chore: add large file alert and show all downloads in download view

* chore: change logic and remove extra code

* refactor:  remove extra code

* chore: remove extra code

* chore:  add course storage, refactor

* chore: add new video formats

* chore: and video to course block

* chore: add new core data models

* chore: download manager to async await

* chore: add calculate total size download bar

* chore: hide total if zero

* chore:  clean up and fix transparent downloads bar when scroll

* chore: add new logic check is downloadable video

* chore: add download quality view

* chore: add select quality for download

* chore: show alert about change download quality when downloading all videos

* chore:  remove extra and add strings

* chore: remove empty line

* chore: add cancel all download when move to background

* chore: add resume downloading

* fix: remaining count in bar

* chore: add new logic get quality video

* chore: remove extra code

* chore: improve download quality names

* chore: new logic

* chore: update logic

* chore: remove extra code

* chore: remove extra

* chore: change strings

* chore: add string

* chore: show remaining files in download bar

* chore: verticals blocks downloadable count nested list

* chore: count of files

* chore: add disable download when offline

* chore: remove empty line

* chore: show delete file when offline

* chore: add Untitled title to download cell view

* fix: tests

* chore: PR issues and add accessibility labels

* chore: add accessibility identifiers

* chore: move video download quality view and refactor

* chore: add cancel all for course

* chore: show large alert

* chore: resolve PR comments

* chore: resolve PR comments

* chore: resolve PR comments

* fix:  tests

* chore: resolve PR comments

* chore: resolve PR commnets

* chore: rename DownloadData to DownloadDataTask

* chore: use app alert and tests

* chore: add confirmation alert when deleting video

* chore: remove empty line

* chore: add strings

* fix:  line length

* fix: repeat download

* chore: add alert when disable downloading

* chore: resolve PR commnets

* chore: update pod version

---------

Co-authored-by: Anton Yarmolenko <[email protected]>
  • Loading branch information
eyatsenkoperpetio and rnr authored Feb 1, 2024
1 parent f4a19e0 commit a050bf0
Show file tree
Hide file tree
Showing 64 changed files with 5,432 additions and 1,216 deletions.
289 changes: 221 additions & 68 deletions Authorization/AuthorizationTests/AuthorizationMock.generated.swift

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Core/Core.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */; };
A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; };
BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; };
BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */; };
BA593F1C2AF8E498009ADB51 /* ScrollSlidingTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */; };
BA593F1E2AF8E4A0009ADB51 /* FrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */; };
BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; };
Expand Down Expand Up @@ -295,6 +296,7 @@
9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = "<group>"; };
A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = "<group>"; };
BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = "<group>"; };
BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityView.swift; sourceTree = "<group>"; };
BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollSlidingTabBar.swift; sourceTree = "<group>"; };
BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameReader.swift; sourceTree = "<group>"; };
BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -673,6 +675,7 @@
027BD3C42909707700392132 /* Shake.swift */,
023A1135291432B200D0D354 /* RegistrationTextField.swift */,
023A1137291432FD00D0D354 /* FieldConfiguration.swift */,
BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */,
BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */,
BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */,
BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */,
Expand Down Expand Up @@ -978,6 +981,7 @@
0241666B28F5A78B00082765 /* HTMLFormattedText.swift in Sources */,
02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */,
027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */,
BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */,
0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */,
064987972B4D69FF0071642A /* WebView.swift in Sources */,
0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */,
Expand Down
19 changes: 16 additions & 3 deletions Core/Core/Data/Model/UserSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

import Foundation

public struct UserSettings: Codable {
public struct UserSettings: Codable, Hashable {
public var wifiOnly: Bool
public var streamingQuality: StreamingQuality

public init(wifiOnly: Bool, streamingQuality: StreamingQuality) {
public var downloadQuality: DownloadQuality

public init(
wifiOnly: Bool,
streamingQuality: StreamingQuality,
downloadQuality: DownloadQuality
) {
self.wifiOnly = wifiOnly
self.streamingQuality = streamingQuality
self.downloadQuality = downloadQuality
}
}

Expand All @@ -23,3 +29,10 @@ public enum StreamingQuality: Codable {
case medium
case high
}

public enum DownloadQuality: Codable, CaseIterable {
case auto
case low_360
case medium_540
case high_720
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CDDownloadData" representedClassName="CDDownloadData" syncable="YES" codeGenerationType="class">
<attribute name="courseId" optional="YES" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="fileName" optional="YES" attributeType="String"/>
<attribute name="fileSize" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="progress" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="resumeData" optional="YES" attributeType="Binary"/>
Expand Down
15 changes: 8 additions & 7 deletions Core/Core/Data/Persistence/CorePersistenceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import Combine

public protocol CorePersistenceProtocol {
func publisher() -> AnyPublisher<Int, Never>
func getAllDownloadData() -> [DownloadData]
func addToDownloadQueue(blocks: [CourseBlock])
func getNextBlockForDownloading() -> DownloadData?
func getDownloadsForCourse(_ courseId: String) -> [DownloadData]
func downloadData(by blockId: String) -> DownloadData?
func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality)
func getNextBlockForDownloading() -> DownloadDataTask?
func updateDownloadState(id: String, state: DownloadState, resumeData: Data?)
func deleteDownloadData(id: String) throws
func saveDownloadData(data: DownloadData)
func deleteDownloadDataTask(id: String) throws
func saveDownloadDataTask(data: DownloadDataTask)
func downloadDataTask(for blockId: String) -> DownloadDataTask?
func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void)
func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void)
func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void)
}

public final class CoreBundle {
Expand Down
163 changes: 147 additions & 16 deletions Core/Core/Domain/Model/CourseBlockModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import Foundation

public struct CourseStructure: Equatable {
public static func == (lhs: CourseStructure, rhs: CourseStructure) -> Bool {
return lhs.id == rhs.id
}

public let id: String
public let graded: Bool
public let completion: Double
Expand Down Expand Up @@ -42,11 +46,24 @@ public struct CourseStructure: Equatable {
self.media = media
self.certificate = certificate
}

public static func == (lhs: CourseStructure, rhs: CourseStructure) -> Bool {
return lhs.id == rhs.id

public func totalVideosSizeInBytes(downloadQuality: DownloadQuality) -> Int {
childs.flatMap {
$0.childs.flatMap { $0.childs.flatMap { $0.childs.compactMap { $0 } } }
}
.filter { $0.isDownloadable }
.compactMap { $0.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize }
.reduce(.zero) { $0 + $1 }
}


public func totalVideosSizeInMb(downloadQuality: DownloadQuality) -> Double {
Double(totalVideosSizeInBytes(downloadQuality: downloadQuality)) / 1024.0 / 1024.0
}

public func totalVideosSizeInGb(downloadQuality: DownloadQuality) -> Double {
Double(totalVideosSizeInBytes(downloadQuality: downloadQuality)) / 1024.0 / 1024.0 / 1024.0
}

}

public struct CourseChapter: Identifiable {
Expand Down Expand Up @@ -102,7 +119,11 @@ public struct CourseSequential: Identifiable {
}
}

public struct CourseVertical {
public struct CourseVertical: Identifiable, Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

public let blockId: String
public let id: String
public let courseId: String
Expand All @@ -114,7 +135,7 @@ public struct CourseVertical {
public var isDownloadable: Bool {
return childs.first(where: { $0.isDownloadable }) != nil
}

public init(
blockId: String,
id: String,
Expand Down Expand Up @@ -144,7 +165,17 @@ public struct SubtitleUrl: Equatable {
}
}

public struct CourseBlock: Equatable {
public struct CourseBlock: Hashable {
public static func == (lhs: CourseBlock, rhs: CourseBlock) -> Bool {
lhs.id == rhs.id &&
lhs.blockId == rhs.blockId &&
lhs.completion == rhs.completion
}

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

public let blockId: String
public let id: String
public let courseId: String
Expand All @@ -155,13 +186,12 @@ public struct CourseBlock: Equatable {
public let displayName: String
public let studentUrl: String
public let subtitles: [SubtitleUrl]?
public let videoUrl: String?
public let youTubeUrl: String?

public let encodedVideo: CourseBlockEncodedVideo?

public var isDownloadable: Bool {
return videoUrl != nil
encodedVideo?.isDownloadable ?? false
}

public init(
blockId: String,
id: String,
Expand All @@ -173,8 +203,7 @@ public struct CourseBlock: Equatable {
displayName: String,
studentUrl: String,
subtitles: [SubtitleUrl]? = nil,
videoUrl: String? = nil,
youTubeUrl: String? = nil
encodedVideo: CourseBlockEncodedVideo?
) {
self.blockId = blockId
self.id = id
Expand All @@ -186,7 +215,109 @@ public struct CourseBlock: Equatable {
self.displayName = displayName
self.studentUrl = studentUrl
self.subtitles = subtitles
self.videoUrl = videoUrl
self.youTubeUrl = youTubeUrl
self.encodedVideo = encodedVideo
}
}

public struct CourseBlockEncodedVideo {

public let fallback: CourseBlockVideo?
public let desktopMP4: CourseBlockVideo?
public let mobileHigh: CourseBlockVideo?
public let mobileLow: CourseBlockVideo?
public let hls: CourseBlockVideo?
public let youtube: CourseBlockVideo?

public init(
fallback: CourseBlockVideo?,
youtube: CourseBlockVideo?,
desktopMP4: CourseBlockVideo?,
mobileHigh: CourseBlockVideo?,
mobileLow: CourseBlockVideo?,
hls: CourseBlockVideo?
) {
self.fallback = fallback
self.youtube = youtube
self.desktopMP4 = desktopMP4
self.mobileHigh = mobileHigh
self.mobileLow = mobileLow
self.hls = hls
}

public var isDownloadable: Bool {
[hls, desktopMP4, mobileHigh, mobileLow, fallback]
.contains { $0?.isDownloadable == true }
}

public func video(downloadQuality: DownloadQuality) -> CourseBlockVideo? {
switch downloadQuality {
case .auto:
[mobileLow, mobileHigh, desktopMP4, fallback, hls]
.first(where: { $0?.isDownloadable == true })?
.flatMap { $0 }
case .high_720:
[desktopMP4, mobileHigh, mobileLow, fallback, hls]
.first(where: { $0?.isDownloadable == true })?
.flatMap { $0 }
case .medium_540:
[mobileHigh, mobileLow, desktopMP4, fallback, hls]
.first(where: { $0?.isDownloadable == true })?
.flatMap { $0 }
case .low_360:
[mobileLow, mobileHigh, desktopMP4, fallback, hls]
.first(where: { $0?.isDownloadable == true })?
.flatMap { $0 }
}
}

public func video(streamingQuality: StreamingQuality) -> CourseBlockVideo? {
switch streamingQuality {
case .auto:
[mobileLow, mobileHigh, desktopMP4, fallback, hls]
.compactMap { $0 }
.sorted(by: { ($0?.streamPriority ?? 0) < ($1?.streamPriority ?? 0) })
.first?
.flatMap { $0 }
case .high:
[desktopMP4, mobileHigh, mobileLow, fallback, hls]
.compactMap { $0 }
.first?
.flatMap { $0 }
case .medium:
[mobileHigh, mobileLow, desktopMP4, fallback, hls]
.compactMap { $0 }
.first?
.flatMap { $0 }
case .low:
[mobileLow, mobileHigh, desktopMP4, fallback, hls]
.compactMap { $0 }
.first(where: { $0?.isDownloadable == true })?
.flatMap { $0 }
}
}

public var youtubeVideoUrl: String? {
youtube?.url
}

}

public struct CourseBlockVideo: Equatable {
public let url: String?
public let fileSize: Int?
public let streamPriority: Int?

public init(url: String?, fileSize: Int?, streamPriority: Int?) {
self.url = url
self.fileSize = fileSize
self.streamPriority = streamPriority
}

public var isVideoURL: Bool {
[".mp4", ".m3u8"].contains(where: { url?.contains($0) == true })
}

public var isDownloadable: Bool {
[".mp4"].contains(where: { url?.contains($0) == true })
}
}
Loading

0 comments on commit a050bf0

Please sign in to comment.