diff --git a/Core/Core/Data/Model/Data_Media.swift b/Core/Core/Data/Model/Data_Media.swift index 3e969add..8509573a 100644 --- a/Core/Core/Data/Model/Data_Media.swift +++ b/Core/Core/Data/Model/Data_Media.swift @@ -10,7 +10,11 @@ import Foundation public extension DataLayer { // MARK: - CourseMedia - struct CourseMedia: Decodable, Sendable { + struct CourseMedia: Decodable, Sendable, Equatable { + public static func == (lhs: DataLayer.CourseMedia, rhs: DataLayer.CourseMedia) -> Bool { + lhs.image == rhs.image + } + public let image: DataLayer.Image public init(image: DataLayer.Image) { @@ -61,7 +65,7 @@ public extension DataLayer { public extension DataLayer { // MARK: - Image - struct Image: Codable, Sendable { + struct Image: Codable, Sendable, Equatable { public let raw: String public let small: String public let large: String diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index 9a635804..91ff9801 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -91,7 +91,11 @@ public struct CourseProgress: Sendable { } } -public struct CourseChapter: Identifiable, Sendable { +public struct CourseChapter: Identifiable, Sendable, Equatable { + public static func == (lhs: CourseChapter, rhs: CourseChapter) -> Bool { + lhs.id == rhs.id && + lhs.blockId == rhs.blockId + } public let blockId: String public let id: String @@ -114,7 +118,11 @@ public struct CourseChapter: Identifiable, Sendable { } } -public struct CourseSequential: Identifiable, Sendable { +public struct CourseSequential: Identifiable, Sendable, Equatable { + public static func == (lhs: CourseSequential, rhs: CourseSequential) -> Bool { + lhs.id == rhs.id && + lhs.blockId == rhs.blockId + } public let blockId: String public let id: String @@ -154,7 +162,7 @@ public struct CourseSequential: Identifiable, Sendable { } } -public struct CourseVertical: Identifiable, Hashable, Sendable { +public struct CourseVertical: Identifiable, Hashable, Sendable, Equatable { public func hash(into hasher: inout Hasher) { hasher.combine(id) } @@ -215,7 +223,7 @@ public struct SequentialProgress: Sendable { } } -public struct CourseBlock: Hashable, Identifiable, Sendable { +public struct CourseBlock: Hashable, Identifiable, Sendable, Equatable { public static func == (lhs: CourseBlock, rhs: CourseBlock) -> Bool { lhs.id == rhs.id && lhs.blockId == rhs.blockId && diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 1b036809..bf6aa130 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -668,6 +668,45 @@ public final class CourseContainerViewModel: BaseCourseViewModel { ) } + @MainActor + func collectBlocks( + chapter: CourseChapter, + blockId: String, + state: DownloadViewState, + videoOnly: Bool = false + ) async -> [CourseBlock] { + let sequentials = chapter.childs.filter { $0.id == blockId } + guard !sequentials.isEmpty else { return [] } + + let blocks = sequentials.flatMap { $0.childs.flatMap { $0.childs } } + .filter { $0.isDownloadable && (!videoOnly || $0.type == .video) } + + if state == .available, isShowedAllowLargeDownloadAlert(blocks: blocks) { + return [] + } + + guard let sequential = chapter.childs.first(where: { $0.id == blockId }) else { + return [] + } + + if state == .available { + analytics.bulkDownloadVideosSubsection( + courseID: courseStructure?.id ?? "", + sectionID: chapter.id, + subSectionID: sequential.id, + videos: blocks.count + ) + } else if state == .finished { + analytics.bulkDeleteVideosSubsection( + courseID: courseStructure?.id ?? "", + subSectionID: sequential.id, + videos: blocks.count + ) + } + + return blocks + } + @MainActor func isShowedAllowLargeDownloadAlert(blocks: [CourseBlock]) -> Bool { waitingDownloads = nil diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index cbd334b4..2fbcb904 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -52,7 +52,7 @@ struct CustomDisclosureGroup: View { let state = downloadAllButtonState(for: chapter, videoOnly: isVideo) { Button( action: { - downloadAllSubsections(in: chapter, state: state) + downloadAllSubsections(in: chapter, state: state) }, label: { switch state { case .available: @@ -84,7 +84,7 @@ struct CustomDisclosureGroup: View { viewModel.router.showGatedContentError(url: courseVertical.webUrl) return } - + viewModel.trackSequentialClicked(sequential) if viewModel.config.uiComponents.courseDropDownNavigationEnabled { viewModel.router.showCourseUnit( @@ -142,9 +142,9 @@ struct CustomDisclosureGroup: View { \(numPointsPossible) """ ) - .font(Theme.Fonts.bodySmall) - .multilineTextAlignment(.leading) - .lineLimit(2) + .font(Theme.Fonts.bodySmall) + .multilineTextAlignment(.leading) + .lineLimit(2) } } .foregroundColor(Theme.Colors.textPrimary) @@ -162,7 +162,7 @@ struct CustomDisclosureGroup: View { } } } - + } } .padding(.horizontal, 16) @@ -212,11 +212,11 @@ struct CustomDisclosureGroup: View { for sequential in chapter.childs { if videoOnly { let isDownloadable = sequential.childs.flatMap { - $0.childs.filter({ $0.type == .video }) + $0.childs.filter { $0.type == .video } }.contains(where: { $0.isDownloadable }) - guard isDownloadable else { return false } + guard isDownloadable else { continue } } - if viewModel.sequentialsDownloadState[sequential.id] != nil { + if sequentialDownloadState(sequential, videoOnly: videoOnly) != nil { return true } } @@ -225,17 +225,39 @@ struct CustomDisclosureGroup: View { private func downloadAllSubsections(in chapter: CourseChapter, state: DownloadViewState) { Task { - await viewModel.onDownloadViewTap(chapter: chapter, state: state) + var allBlocks: [CourseBlock] = [] + var sequentialsToDownload: [CourseSequential] = [] + for sequential in chapter.childs { + let blocks = await viewModel.collectBlocks( + chapter: chapter, + blockId: sequential.id, + state: state, + videoOnly: isVideo + ) + if !blocks.isEmpty { + allBlocks.append(contentsOf: blocks) + sequentialsToDownload.append(sequential) + } + } + await viewModel.download( + state: state, + blocks: allBlocks, + sequentials: sequentialsToDownload + ) } } private func downloadAllButtonState(for chapter: CourseChapter, videoOnly: Bool) -> DownloadViewState? { if canDownloadAllSections(in: chapter, videoOnly: videoOnly) { - let downloads = chapter.childs.filter({ viewModel.sequentialsDownloadState[$0.id] != nil }) - - if downloads.contains(where: { viewModel.sequentialsDownloadState[$0.id] == .downloading }) { + var downloads: [DownloadViewState] = [] + for sequential in chapter.childs { + if let state = sequentialDownloadState(sequential, videoOnly: videoOnly) { + downloads.append(state) + } + } + if downloads.contains(.downloading) { return .downloading - } else if downloads.allSatisfy({ viewModel.sequentialsDownloadState[$0.id] == .finished }) { + } else if downloads.allSatisfy({ $0 == .finished }) { return .finished } else { return .available @@ -244,6 +266,35 @@ struct CustomDisclosureGroup: View { return nil } + private func sequentialDownloadState(_ sequential: CourseSequential, videoOnly: Bool) -> DownloadViewState? { + let blocks: [CourseBlock] + if videoOnly { + blocks = sequential.childs.flatMap { $0.childs }.filter { $0.isDownloadable && $0.type == .video } + } else { + blocks = sequential.childs.flatMap { $0.childs }.filter { $0.isDownloadable } + } + guard !blocks.isEmpty else { return nil } + var blockStates: [DownloadViewState] = [] + for block in blocks { + if let task = viewModel.courseDownloadTasks.first(where: { $0.blockId == block.id }) { + switch task.state { + case .waiting, .inProgress: + blockStates.append(.downloading) + case .finished: + blockStates.append(.finished) + } + } else { + blockStates.append(.available) + } + } + if blockStates.contains(.downloading) { + return .downloading + } else if blockStates.allSatisfy({ $0 == .finished }) { + return .finished + } else { + return .available + } + } } #if DEBUG