diff --git a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index ed12a0c8a..3dc8a8e03 100644 --- a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,10 +1,9 @@ - + - diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift index e7256850c..f250fc49c 100644 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift @@ -15,7 +15,7 @@ public protocol CorePersistenceProtocol { func getNextBlockForDownloading() -> DownloadData? func getDownloadsForCourse(_ courseId: String) -> [DownloadData] func downloadData(by blockId: String) -> DownloadData? - func updateDownloadState(id: String, state: DownloadState, path: String?, resumeData: Data?) + func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) func deleteDownloadData(id: String) throws func saveDownloadData(data: DownloadData) } diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 4b8ed6df8..774c767f0 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -24,7 +24,6 @@ public struct DownloadData { public let id: String public let courseId: String public let url: String - public let path: String? public let fileName: String public let progress: Double public let resumeData: Data? @@ -35,7 +34,6 @@ public struct DownloadData { id: String, courseId: String, url: String, - path: String?, fileName: String, progress: Double, resumeData: Data?, @@ -45,7 +43,6 @@ public struct DownloadData { self.id = id self.courseId = courseId self.url = url - self.path = path self.fileName = fileName self.progress = progress self.resumeData = resumeData @@ -148,28 +145,27 @@ public class DownloadManager: DownloadManagerProtocol { persistence.updateDownloadState( id: download.id, state: .inProgress, - path: nil, resumeData: download.resumeData ) self.isDownloadingInProgress = true - let fileName = url.lastPathComponent if let resumeData = download.resumeData { downloadRequest = AF.download(resumingWith: resumeData) } else { downloadRequest = AF.download(url) } -// downloadRequest?.downloadProgress { prog in -// let completed = Double(prog.fractionCompleted * 100) -// print(">>>>> Downloading", download.url, completed, "%") -// } + #if DEBUG + downloadRequest?.downloadProgress { prog in + let completed = Double(prog.fractionCompleted * 100) + print(">>>>> Downloading", download.url, completed, "%") + } + #endif downloadRequest?.responseData(completionHandler: { [weak self] data in guard let self else { return } if let data = data.value, let url = self.videosFolderUrl() { - let fileUrl = self.saveFile(fileName: fileName, data: data, folderURL: url) + self.saveFile(fileName: download.fileName, data: data, folderURL: url) self.persistence.updateDownloadState( id: download.id, state: .finished, - path: fileUrl?.absoluteString, resumeData: nil ) try? self.newDownload() @@ -188,7 +184,6 @@ public class DownloadManager: DownloadManagerProtocol { self.persistence.updateDownloadState( id: currentDownload.id, state: .paused, - path: nil, resumeData: resumeData ) }) @@ -196,16 +191,13 @@ public class DownloadManager: DownloadManagerProtocol { public func deleteFile(blocks: [CourseBlock]) { for block in blocks { - let downloadData = persistence.downloadData(by: block.id) - guard let path = persistence.downloadData(by: block.id)?.path, - let fileUrl = URL(string: path) else { return } - do { try persistence.deleteDownloadData(id: block.id) - try FileManager.default.removeItem(at: fileUrl) - print("File deleted successfully") + if let fileUrl = fileUrl(for: block.id) { + try FileManager.default.removeItem(at: fileUrl) + } } catch { - print("Error deleting file: \(error.localizedDescription)") + NSLog("Error deleting file: \(error.localizedDescription)") } } } @@ -213,7 +205,7 @@ public class DownloadManager: DownloadManagerProtocol { public func deleteAllFiles() { let downloadData = persistence.getAllDownloadData() downloadData.forEach { - if let path = $0.path, let fileURL = URL(string: path) { + if let fileURL = fileUrl(for: $0.id) { do { try FileManager.default.removeItem(at: fileURL) } catch { @@ -227,8 +219,9 @@ public class DownloadManager: DownloadManagerProtocol { guard let data = persistence.downloadData(by: blockId), data.url.count > 0, data.state == .finished else { return nil } - - return URL(string: data.path ?? "") + let path = videosFolderUrl() + let fileName = data.fileName + return path?.appendingPathComponent(fileName) } private func videosFolderUrl() -> URL? { @@ -252,15 +245,13 @@ public class DownloadManager: DownloadManagerProtocol { } } - private func saveFile(fileName: String, data: Data, folderURL: URL) -> URL? { + private func saveFile(fileName: String, data: Data, folderURL: URL) { let fileURL = folderURL.appendingPathComponent(fileName) do { try data.write(to: fileURL) - return fileURL } catch { - print("SaveFile Error", error.localizedDescription) + NSLog("SaveFile Error", error.localizedDescription) } - return nil } } diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index 3c0ae6ec4..ffea4335f 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -22,28 +22,28 @@ public struct WebBrowser: View { public var body: some View { ZStack(alignment: .top) { - + CoreAssets.background.swiftUIColor.ignoresSafeArea() // MARK: - Page name VStack(alignment: .center) { + NavigationBar(title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() }) + // MARK: - Page Body VStack { ZStack(alignment: .top) { - NavigationView { +// NavigationView { WebView( viewModel: .init(url: url, baseURL: ""), isLoading: $isShowProgress, refreshCookies: {} ) - .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 - .navigationBarHidden(true) - .ignoresSafeArea() - } - } + +// } + }.navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 + .navigationBarHidden(true) + .ignoresSafeArea() } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(pageTitle) } } } diff --git a/Course/Course/Data/Model/Data_UpdatesResponse.swift b/Course/Course/Data/Model/Data_UpdatesResponse.swift index 88cdc883f..bb9824e7b 100644 --- a/Course/Course/Data/Model/Data_UpdatesResponse.swift +++ b/Course/Course/Data/Model/Data_UpdatesResponse.swift @@ -13,7 +13,7 @@ public extension DataLayer { public let id: Int public let date: String public let content: String - public let status: String + public let status: String? } typealias CourseUpdates = [CourseUpdate] } diff --git a/Course/Course/Domain/Model/CourseUpdate.swift b/Course/Course/Domain/Model/CourseUpdate.swift index 345d1e2a3..2ef09f210 100644 --- a/Course/Course/Domain/Model/CourseUpdate.swift +++ b/Course/Course/Domain/Model/CourseUpdate.swift @@ -11,9 +11,9 @@ public struct CourseUpdate { public let id: Int public let date: String public var content: String - public let status: String + public let status: String? - public init(id: Int, date: String, content: String, status: String) { + public init(id: Int, date: String, content: String, status: String?) { self.id = id self.date = date self.content = content diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index eaf467484..57b12b542 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -18,7 +18,6 @@ public struct HandoutsUpdatesDetailView: View { private var handouts: String? private var announcements: [CourseUpdate]? private let title: String - @State private var height: [Int: CGFloat] = [:] public init( handouts: String?, @@ -68,6 +67,8 @@ public struct HandoutsUpdatesDetailView: View { public var body: some View { ZStack(alignment: .top) { + Theme.Colors.background + .ignoresSafeArea() GeometryReader { reader in // MARK: - Page Body @@ -98,12 +99,12 @@ public struct HandoutsUpdatesDetailView: View { type: .discovery, screenWidth: reader.size.width ) - HTMLFormattedText( - fixBrokenLinks(in: formattedAnnouncements), - isScrollEnabled: true, - textViewHeight: $height[index] - ) - .frame(height: height[index]) + HStack { + HTMLFormattedText(formattedAnnouncements) + Spacer() + } + + .id(UUID()) if index != announcements.count - 1 { Divider() @@ -120,13 +121,7 @@ public struct HandoutsUpdatesDetailView: View { router.back() } Spacer(minLength: 84) - - .background( - Theme.Colors.background - .ignoresSafeArea() - ) } - } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index 81cd06f6a..d6563b46f 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -12,7 +12,7 @@ struct HandoutsView: View { private let courseID: String - @ObservedObject + @StateObject private var viewModel: HandoutsViewModel public init( @@ -20,7 +20,8 @@ struct HandoutsView: View { viewModel: HandoutsViewModel ) { self.courseID = courseID - self.viewModel = viewModel +// self.viewModel = viewModel + self._viewModel = StateObject(wrappedValue: { viewModel }()) } public var body: some View { diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index f58b2438e..19a6fa413 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -71,6 +71,7 @@ public struct CourseOutlineView: View { ) .frame(width: 141) .padding(.top, 8) + .fullScreenCover( isPresented: $openCertificateView, content: { diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 1d9637440..e9afacbc1 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -45,7 +45,6 @@ public struct CourseUnitView: View { GeometryReader { reader in VStack(spacing: 0) { VStack {}.frame(height: 100) - if viewModel.connectivity.isInternetAvaliable { LazyVStack(spacing: 0) { let data = Array(viewModel.verticals[viewModel.verticalIndex].childs.enumerated()) ForEach(data, id: \.offset) { index, block in @@ -54,40 +53,58 @@ public struct CourseUnitView: View { 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) + if viewModel.connectivity.isInternetAvaliable { + YouTubeView( + name: block.displayName, + url: url, + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ).frameLimit() + Spacer(minLength: 100) + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } // 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) + let url = viewModel.urlForVideoFileOrFallback( + blockId: blockID, + url: encodedUrl + ) + if viewModel.connectivity.isInternetAvaliable || url?.isFileURL == true { + EncodedVideoView( + name: block.displayName, + url: url, + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ).frameLimit() + Spacer(minLength: 100) + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } // MARK: Web case .web(let url): - WebView(url: url, viewModel: viewModel) + if viewModel.connectivity.isInternetAvaliable { + WebView(url: url, viewModel: viewModel) + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } // MARK: Unknown case .unknown(let url): + if viewModel.connectivity.isInternetAvaliable { UnknownView(url: url, viewModel: viewModel) Spacer() + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } // MARK: Discussion case let .discussion(blockID, blockKey, title): + if viewModel.connectivity.isInternetAvaliable { VStack { if showDiscussion { DiscussionView( @@ -104,6 +121,9 @@ public struct CourseUnitView: View { } } }.frameLimit() + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } } } else { EmptyView() @@ -126,21 +146,7 @@ public struct CourseUnitView: View { } }) - } 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) .clipped() @@ -334,3 +340,22 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif + +struct NoInternetView: View { + + let playerStateSubject: CurrentValueSubject + + var body: some 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) + } +} diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 63632ac3b..86c12aa7a 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -631,7 +631,6 @@ final class CourseContainerViewModelTests: XCTestCase { id: "1", courseId: "course123", url: "https://example.com/file.mp4", - path: nil, fileName: "file.mp4", progress: 0, resumeData: nil, @@ -740,7 +739,6 @@ final class CourseContainerViewModelTests: XCTestCase { id: "1", courseId: "course123", url: "https://example.com/file.mp4", - path: "file://../file.mp4", fileName: "file.mp4", progress: 0, resumeData: nil, @@ -862,7 +860,6 @@ final class CourseContainerViewModelTests: XCTestCase { id: "1", courseId: "course123", url: "https://example.com/file.mp4", - path: nil, fileName: "file.mp4", progress: 0, resumeData: nil, diff --git a/OpenEdX/Data/CorePersistence.swift b/OpenEdX/Data/CorePersistence.swift index 38605c97b..75b136eb5 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/OpenEdX/Data/CorePersistence.swift @@ -49,7 +49,6 @@ public class CorePersistence: CorePersistenceProtocol { id: $0.id ?? "", courseId: $0.courseId ?? "", url: $0.url ?? "", - path: $0.path, fileName: $0.fileName ?? "", progress: $0.progress, resumeData: $0.resumeData, @@ -65,7 +64,9 @@ public class CorePersistence: CorePersistenceProtocol { request.predicate = NSPredicate(format: "id = %@", block.id) guard (try? context.fetch(request).first) == nil else { continue } guard let url = block.videoUrl, - let fileName = URL(string: url)?.lastPathComponent else { continue } + let fileExtension = URL(string: url)?.pathExtension + else { continue } + let fileName = "\(block.id).\(fileExtension)" context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump @@ -90,7 +91,6 @@ public class CorePersistence: CorePersistenceProtocol { id: data.id ?? "", courseId: data.courseId ?? "", url: data.url ?? "", - path: data.path, fileName: data.fileName ?? "", progress: data.progress, resumeData: data.resumeData, @@ -108,7 +108,6 @@ public class CorePersistence: CorePersistenceProtocol { id: $0.id ?? "", courseId: $0.courseId ?? "", url: $0.url ?? "", - path: $0.path, fileName: $0.fileName ?? "", progress: $0.progress, resumeData: $0.resumeData, @@ -126,7 +125,6 @@ public class CorePersistence: CorePersistenceProtocol { id: downloadData.id ?? "", courseId: downloadData.courseId ?? "", url: downloadData.url ?? "", - path: downloadData.path, fileName: downloadData.fileName ?? "", progress: downloadData.progress, resumeData: downloadData.resumeData, @@ -135,13 +133,12 @@ public class CorePersistence: CorePersistenceProtocol { ) } - public func updateDownloadState(id: String, state: DownloadState, path: String?, resumeData: Data?) { + public func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { context.performAndWait { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "id = %@", id) guard let downloadData = try? context.fetch(request).first else { return } downloadData.state = state.rawValue - downloadData.path = path downloadData.resumeData = resumeData do { try context.save() diff --git a/OpenEdX/uk.lproj/languages.json b/OpenEdX/uk.lproj/languages.json index 5c07f7b8c..971a7c53b 100644 --- a/OpenEdX/uk.lproj/languages.json +++ b/OpenEdX/uk.lproj/languages.json @@ -1,6 +1,6 @@ [ { - "аа": "Афарська" + "aa": "Афарська" }, { "ab": "Абхазька" diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index 3a1b79761..f0a439d48 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -199,9 +199,18 @@ public struct EditProfileView: View { } } .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + .navigationBarBackButtonHidden(true) .navigationTitle(ProfileLocalization.editProfile) .toolbar { + ToolbarItem(placement: .navigationBarLeading, content: { + Button(action: { + viewModel.backButtonTapped() + }, label: { + CoreAssets.arrowLeft.swiftUIImage + .renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + }).opacity(viewModel.isChanged ? 1 : 0.3) + }) ToolbarItem(placement: .navigationBarTrailing, content: { Button(action: { if viewModel.isChanged { @@ -212,7 +221,8 @@ public struct EditProfileView: View { } }, label: { HStack(spacing: 2) { - CoreAssets.done.swiftUIImage + CoreAssets.done.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) Text(CoreLocalization.done) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.accentColor)