From 88724fe18f08a22a09d0393741d88b0960f1a9a2 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:43:14 +0530 Subject: [PATCH] NMC 2248 - Image video upload customisation --- .../NextcloudUnitTests/AssetUploadTest.swift | 36 ++ .../Main/Create cloud/NCUploadAssets.swift | 540 ++++++++++++++++++ 2 files changed, 576 insertions(+) create mode 100644 Tests/NextcloudUnitTests/AssetUploadTest.swift create mode 100644 iOSClient/Main/Create cloud/NCUploadAssets.swift diff --git a/Tests/NextcloudUnitTests/AssetUploadTest.swift b/Tests/NextcloudUnitTests/AssetUploadTest.swift new file mode 100644 index 0000000000..72f541042b --- /dev/null +++ b/Tests/NextcloudUnitTests/AssetUploadTest.swift @@ -0,0 +1,36 @@ +// +// AssetUploadTest.swift +// NextcloudTests +// +// Created by A200020526 on 12/06/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import XCTest + +final class AssetUploadTest: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/iOSClient/Main/Create cloud/NCUploadAssets.swift b/iOSClient/Main/Create cloud/NCUploadAssets.swift new file mode 100644 index 0000000000..2ad771c0c3 --- /dev/null +++ b/iOSClient/Main/Create cloud/NCUploadAssets.swift @@ -0,0 +1,540 @@ +// +// NCUploadAssets.swift +// Nextcloud +// +// Created by Marino Faggiana on 04/01/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import SwiftUI +import NextcloudKit +import TLPhotoPicker +import Mantis +import Photos +import QuickLook + +class NCHostingUploadAssetsView: NSObject { + + func makeShipDetailsUI(assets: [TLPHAsset], serverUrl: String, userBaseUrl: NCUserBaseUrl) -> UIViewController { + + let uploadAssets = NCUploadAssets(assets: assets, serverUrl: serverUrl, userBaseUrl: userBaseUrl ) + let details = UploadAssetsView(uploadAssets: uploadAssets) + return UIHostingController(rootView: details) + } +} + +// MARK: - Class + +struct PreviewStore { + var id: String + var asset: TLPHAsset + var assetType: TLPHAsset.AssetType + var data: Data? + var fileName: String + var image: UIImage +} + +class NCUploadAssets: NSObject, ObservableObject, NCCreateFormUploadConflictDelegate { + @Published var serverUrl: String + @Published var assets: [TLPHAsset] + @Published var userBaseUrl: NCUserBaseUrl + @Published var dismiss = false + @Published var isHiddenSave = true + @Published var isUseAutoUploadFolder: Bool = false + @Published var isUseAutoUploadSubFolder: Bool = false + @Published var previewStore: [PreviewStore] = [] + @Published var showHUD: Bool = false + @Published var uploadInProgress: Bool = false + + var metadatasNOConflict: [tableMetadata] = [] + var metadatasUploadInConflict: [tableMetadata] = [] + var timer: Timer? + + init(assets: [TLPHAsset], serverUrl: String, userBaseUrl: NCUserBaseUrl) { + + self.assets = assets + self.serverUrl = serverUrl + self.userBaseUrl = userBaseUrl + } + + func loadImages() { + var previewStore: [PreviewStore] = [] + self.showHUD = true + DispatchQueue.global().async { + for asset in self.assets { + guard let image = asset.fullResolutionImage?.resizeImage(size: CGSize(width: 300, height: 300), isAspectRation: true), let localIdentifier = asset.phAsset?.localIdentifier else { continue } + previewStore.append(PreviewStore(id: localIdentifier, asset: asset, assetType: asset.type, fileName: "", image: image)) + } + DispatchQueue.main.async { + self.showHUD = false + self.previewStore = previewStore + self.isHiddenSave = false + } + } + } + + func startTimer(navigationItem: UINavigationItem) { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in + guard let buttonDone = navigationItem.leftBarButtonItems?.first, let buttonCrop = navigationItem.leftBarButtonItems?.last else { return } + buttonCrop.isEnabled = true + buttonDone.isEnabled = true + if let markup = navigationItem.rightBarButtonItems?.first(where: { $0.accessibilityIdentifier == "QLOverlayMarkupButtonAccessibilityIdentifier" }) { + if let originalButton = markup.value(forKey: "originalButton") as AnyObject? { + if let symbolImageName = originalButton.value(forKey: "symbolImageName") as? String { + if symbolImageName == "pencil.tip.crop.circle.on" { + buttonCrop.isEnabled = false + buttonDone.isEnabled = false + } + } + } + } + }) + } + + func stopTimer() { + self.timer?.invalidate() + } + + func dismissCreateFormUploadConflict(metadatas: [tableMetadata]?) { + guard let metadatas = metadatas else { + self.showHUD = false + self.uploadInProgress.toggle() + return + } + + func createProcessUploads() { + if !self.dismiss { + NCNetworkingProcess.shared.createProcessUploads(metadatas: metadatas, completion: { _ in + self.dismiss = true + }) + } + } + + if isUseAutoUploadFolder { + DispatchQueue.global().async { + let assets = self.assets.compactMap { $0.phAsset } + let result = NCNetworking.shared.createFolder(assets: assets, selector: NCGlobal.shared.selectorUploadFile, useSubFolder: self.isUseAutoUploadSubFolder, account: self.userBaseUrl.account, urlBase: self.userBaseUrl.urlBase, userId: self.userBaseUrl.userId, withPush: false) + DispatchQueue.main.async { + self.showHUD = false + self.uploadInProgress.toggle() + if result { + createProcessUploads() + } else { + let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_error_createsubfolders_upload_") + NCContentPresenter().showError(error: error) + } + } + } + } else { + createProcessUploads() + } + } +} + +// MARK: - View + +struct UploadAssetsView: View { + @State private var fileName: String = NCKeychain().getFileNameMask(key: NCGlobal.shared.keyFileNameMask) + @State private var isMaintainOriginalFilename: Bool = NCKeychain().getOriginalFileName(key: NCGlobal.shared.keyFileNameOriginal) + @State private var isAddFilenametype: Bool = NCKeychain().getFileNameType(key: NCGlobal.shared.keyFileNameType) + @State private var isPresentedSelect = false + @State private var isPresentedUploadConflict = false + @State private var isPresentedQuickLook = false + @State private var isPresentedAlert = false + @State private var fileNamePath = NSTemporaryDirectory() + "Photo.jpg" + @State private var renameFileName: String = "" + @State private var renameIndex: Int = 0 + @State private var metadata: tableMetadata? + @State private var index: Int = 0 + + var gridItems: [GridItem] = [GridItem()] + + @ObservedObject var uploadAssets: NCUploadAssets + @Environment(\.presentationMode) var presentationMode + + init(uploadAssets: NCUploadAssets) { + self.uploadAssets = uploadAssets + uploadAssets.loadImages() + } + + func getTextServerUrl(_ serverUrl: String) -> String { + if let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", uploadAssets.userBaseUrl.account, serverUrl)), let metadata = NCManageDatabase.shared.getMetadataFromOcId(directory.ocId) { + return (metadata.fileNameView) + } else { + return (serverUrl as NSString).lastPathComponent + } + } + + private func setFileNameMaskForPreview(fileName: String?) -> String { + let utilityFileSystem = NCUtilityFileSystem() + guard let asset = uploadAssets.assets.first?.phAsset else { return "" } + var preview: String = "" + let creationDate = asset.creationDate ?? Date() + + NCKeychain().setOriginalFileName(key: NCGlobal.shared.keyFileNameOriginal, value: isMaintainOriginalFilename) + NCKeychain().setFileNameType(key: NCGlobal.shared.keyFileNameType, prefix: isAddFilenametype) + NCKeychain().setFileNameMask(key: NCGlobal.shared.keyFileNameMask, mask: fileName) + + preview = CCUtility.createFileName( + getOriginalFilenameForPreview() as String, + fileDate: creationDate, + fileType: asset.mediaType, + keyFileName: fileName.isEmptyOrNil ? nil : NCGlobal.shared.keyFileNameMask, + keyFileNameType: NCGlobal.shared.keyFileNameType, + keyFileNameOriginal: NCGlobal.shared.keyFileNameOriginal, + forcedNewFileName: false + ) + + let trimmedPreview = preview.trimmingCharacters(in: .whitespacesAndNewlines) + + if !(fileName?.isEmpty ?? false) { + + NCKeychain().setFileNameMask(key: fileName ?? "", mask: NCGlobal.shared.keyFileNameMask) + preview = CCUtility.createFileName(asset.value(forKey: "filename") as? String, + fileDate: creationDate, fileType: asset.mediaType, + keyFileName: NCGlobal.shared.keyFileNameMask, + keyFileNameType: NCGlobal.shared.keyFileNameType, + keyFileNameOriginal: NCGlobal.shared.keyFileNameOriginal, + forcedNewFileName: false) + + } else { + + NCKeychain().setFileNameMask(key: "", mask: NCGlobal.shared.keyFileNameMask) + preview = CCUtility.createFileName(asset.value(forKey: "filename") as? String, + fileDate: creationDate, + fileType: asset.mediaType, + keyFileName: nil, + keyFileNameType: NCGlobal.shared.keyFileNameType, + keyFileNameOriginal: NCGlobal.shared.keyFileNameOriginal, + forcedNewFileName: false) + } + + return (!isMaintainOriginalFilename ? (String(format: NSLocalizedString("_preview_filename_", comment: ""), "MM, MMM, DD, YY, YYYY, HH, hh, mm, ss, ampm") + ":" + "\n\n") : #"\#(NSLocalizedString("_filename_", comment: "") ): "#) + preview + } + + private func save(completion: @escaping (_ metadatasNOConflict: [tableMetadata], _ metadatasUploadInConflict: [tableMetadata]) -> Void) { + + let utilityFileSystem = NCUtilityFileSystem() + var metadatasNOConflict: [tableMetadata] = [] + var metadatasUploadInConflict: [tableMetadata] = [] + let autoUploadPath = NCManageDatabase.shared.getAccountAutoUploadPath(urlBase: uploadAssets.userBaseUrl.urlBase, userId: uploadAssets.userBaseUrl.userId, account: uploadAssets.userBaseUrl.account) + var serverUrl = uploadAssets.isUseAutoUploadFolder ? autoUploadPath : uploadAssets.serverUrl + let autoUploadSubfolderGranularity = NCManageDatabase.shared.getAccountAutoUploadSubfolderGranularity() + + for tlAsset in uploadAssets.assets { + guard let asset = tlAsset.phAsset, let previewStore = uploadAssets.previewStore.first(where: { $0.id == asset.localIdentifier }) else { continue } + + let assetFileName = asset.originalFilename + var livePhoto: Bool = false + let creationDate = asset.creationDate ?? Date() + let ext = assetFileName.pathExtension.lowercased() + + let fileName = previewStore.fileName.isEmpty + ? CCUtility.createFileName(assetFileName as String, + fileDate: creationDate, + fileType: asset.mediaType, + keyFileName: NCGlobal.shared.keyFileNameMask, + keyFileNameType: NCGlobal.shared.keyFileNameType, + keyFileNameOriginal: NCGlobal.shared.keyFileNameOriginal, + forcedNewFileName: false)! + : (previewStore.fileName + "." + ext) + + if previewStore.assetType == .livePhoto && NCKeychain().livePhoto && previewStore.data == nil { + livePhoto = true + } + + // Auto upload with subfolder + if uploadAssets.isUseAutoUploadSubFolder { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy" + let yearString = dateFormatter.string(from: creationDate) + dateFormatter.dateFormat = "MM" + let monthString = dateFormatter.string(from: creationDate) + dateFormatter.dateFormat = "dd" + let dayString = dateFormatter.string(from: creationDate) + if autoUploadSubfolderGranularity == NCGlobal.shared.subfolderGranularityYearly { + serverUrl = autoUploadPath + "/" + yearString + } else if autoUploadSubfolderGranularity == NCGlobal.shared.subfolderGranularityDaily { + serverUrl = autoUploadPath + "/" + yearString + "/" + monthString + "/" + dayString + } else { // Month Granularity is default + serverUrl = autoUploadPath + "/" + yearString + "/" + monthString + } + } + + // Check if is in upload + let isRecordInSessions = NCManageDatabase.shared.getAdvancedMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@ AND session != ''", uploadAssets.userBaseUrl.account, serverUrl, fileName), sorted: "fileName", ascending: false) + if !isRecordInSessions.isEmpty { continue } + + let metadata = NCManageDatabase.shared.createMetadata(account: uploadAssets.userBaseUrl.account, user: uploadAssets.userBaseUrl.user, userId: uploadAssets.userBaseUrl.userId, fileName: fileName, fileNameView: fileName, ocId: NSUUID().uuidString, serverUrl: serverUrl, urlBase: uploadAssets.userBaseUrl.urlBase, url: "", contentType: "") + + if livePhoto { + metadata.livePhotoFile = (metadata.fileName as NSString).deletingPathExtension + ".mov" + } + metadata.assetLocalIdentifier = asset.localIdentifier + metadata.session = NCNetworking.shared.sessionUploadBackground + metadata.sessionSelector = NCGlobal.shared.selectorUploadFile + metadata.status = NCGlobal.shared.metadataStatusWaitUpload + metadata.sessionDate = Date() + + // Modified + if let previewStore = uploadAssets.previewStore.first(where: { $0.id == asset.localIdentifier }), let data = previewStore.data { + if metadata.contentType == "image/heic" { + let fileNameNoExtension = (fileName as NSString).deletingPathExtension + metadata.contentType = "image/jpeg" + metadata.fileName = fileNameNoExtension + ".jpg" + metadata.fileNameView = fileNameNoExtension + ".jpg" + } + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) + do { + try data.write(to: URL(fileURLWithPath: fileNamePath)) + metadata.isExtractFile = true + metadata.size = utilityFileSystem.getFileSize(filePath: fileNamePath) + metadata.creationDate = asset.creationDate as? NSDate ?? (Date() as NSDate) + metadata.date = asset.modificationDate as? NSDate ?? (Date() as NSDate) + } catch { } + } + + if let result = NCManageDatabase.shared.getMetadataConflict(account: uploadAssets.userBaseUrl.account, serverUrl: serverUrl, fileNameView: fileName) { + metadata.fileName = result.fileName + metadatasUploadInConflict.append(metadata) + } else { + metadatasNOConflict.append(metadata) + } + } + + completion(metadatasNOConflict, metadatasUploadInConflict) + } + + private func presentedQuickLook(index: Int) { + var image: UIImage? + if let imageData = uploadAssets.previewStore[index].data { + image = UIImage(data: imageData) + } else if let imageFullResolution = uploadAssets.previewStore[index].asset.fullResolutionImage?.fixedOrientation() { + image = imageFullResolution + } + if let image = image { + if let data = image.jpegData(compressionQuality: 1) { + do { + try data.write(to: URL(fileURLWithPath: fileNamePath)) + self.index = index + isPresentedQuickLook = true + } catch { + } + } + } + } + + private func deleteAsset(index: Int) { + uploadAssets.assets.remove(at: index) + uploadAssets.previewStore.remove(at: index) + if uploadAssets.previewStore.isEmpty { + uploadAssets.dismiss = true + } + } + + private func getOriginalFilenameForPreview() -> NSString { + NCKeychain().setOriginalFileName(key: NCGlobal.shared.keyFileNameOriginal, value: isMaintainOriginalFilename) + if let asset = uploadAssets.assets.first?.phAsset { + return asset.originalFilename + } else { + return "" + } + } + + var body: some View { + let utilityFileSystem = NCUtilityFileSystem() + + NavigationView { + ZStack(alignment: .top) { + List { + //Save Path + Section(header: Text(NSLocalizedString("_save_path_", comment: "").uppercased()), footer: Text(NSLocalizedString("_auto_upload_help_text_", comment: ""))) { + HStack { + if utilityFileSystem.getHomeServer(urlBase: uploadAssets.userBaseUrl.urlBase, userId: uploadAssets.userBaseUrl.userId) == uploadAssets.serverUrl { + Text(NSLocalizedString("_prefix_upload_path_", comment: "")) + .font(.system(size: 15)) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text(self.getTextServerUrl(uploadAssets.serverUrl)) + .font(.system(size: 15)) + .frame(maxWidth: .infinity, alignment: .leading) + } + + Image("folder") + .renderingMode(.template) + .resizable() + .scaledToFit() + .foregroundColor(Color(NCBrandColor.shared.brand)) + .frame(width: 25, height: 25, alignment: .trailing) + } + .onTapGesture { + isPresentedSelect = true + } + + + Toggle(isOn: $uploadAssets.isUseAutoUploadFolder, label: { + Text(NSLocalizedString("_use_folder_auto_upload_", comment: "")) + .font(.system(size: 15)) + }) + .toggleStyle(SwitchToggleStyle(tint: Color(NCBrandColor.shared.brand))) + + Toggle(isOn: $uploadAssets.isUseAutoUploadSubFolder, label: { + Text(NSLocalizedString("_autoupload_create_subfolder_", comment: "")) + .font(.system(size: 15)) + .disabled(!uploadAssets.isUseAutoUploadFolder) + }) + .toggleStyle(SwitchToggleStyle(tint: Color(NCBrandColor.shared.brand))) + .disabled(!uploadAssets.isUseAutoUploadFolder) + } + + //File Name + Section(header: Text(NSLocalizedString("_filename_", comment: "").uppercased()), footer: Text(setFileNameMaskForPreview(fileName: fileName))) { + Toggle(isOn: $isMaintainOriginalFilename, label: { + Text(NSLocalizedString("_maintain_original_filename_", comment: "")) + .font(.system(size: 15)) + }) + .toggleStyle(SwitchToggleStyle(tint: Color(NCBrandColor.shared.brand))) + + if !isMaintainOriginalFilename { + Toggle(isOn: $isAddFilenametype, label: { + Text(NSLocalizedString("_add_filenametype_", comment: "")) + .font(.system(size: 15)) + }) + .toggleStyle(SwitchToggleStyle(tint: Color(NCBrandColor.shared.brand))) + HStack { + if isMaintainOriginalFilename { + Text(getOriginalFilenameForPreview() as? String ?? "") + .font(.system(size: 15)) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(Color.gray) + } else { + TextField(NSLocalizedString("_enter_filename_", comment: ""), text: $fileName) + .font(.system(size: 15)) + .modifier(TextFieldClearButton(text: $fileName)) + .multilineTextAlignment(.leading) + } + } + } + + }.complexModifier { view in + view.listRowSeparator(.hidden) + } + } + .listStyle(GroupedListStyle()) + .navigationTitle(NSLocalizedString("_upload_photos_videos_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + .toolbar{ + ToolbarItem(placement: .navigationBarLeading) { + Button(NSLocalizedString("_cancel_", comment: "")) { + self.uploadAssets.dismiss = true + }.foregroundColor(Color(NCBrandColor.shared.brand)) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button(NSLocalizedString("_save_", comment: "")) { + if uploadAssets.isUseAutoUploadFolder, uploadAssets.isUseAutoUploadSubFolder { + uploadAssets.showHUD = true + } + uploadAssets.uploadInProgress.toggle() + save { metadatasNOConflict, metadatasUploadInConflict in + if metadatasUploadInConflict.isEmpty { + uploadAssets.dismissCreateFormUploadConflict(metadatas: metadatasNOConflict) + } else { + uploadAssets.metadatasNOConflict = metadatasNOConflict + uploadAssets.metadatasUploadInConflict = metadatasUploadInConflict + isPresentedUploadConflict = true + } + } + }.foregroundColor(Color(NCBrandColor.shared.brand)) + } + } + + HUDView(showHUD: $uploadAssets.showHUD, textLabel: NSLocalizedString("_wait_", comment: ""), image: "doc.badge.arrow.up") + .offset(y: uploadAssets.showHUD ? 5 : -200) + .animation(.easeOut) + } + } + .navigationViewStyle(StackNavigationViewStyle()) + .sheet(isPresented: $isPresentedSelect) { + SelectView(serverUrl: $uploadAssets.serverUrl) + } + .sheet(isPresented: $isPresentedUploadConflict) { + UploadConflictView(delegate: uploadAssets, serverUrl: uploadAssets.serverUrl, metadatasUploadInConflict: uploadAssets.metadatasUploadInConflict, metadatasNOConflict: uploadAssets.metadatasNOConflict) + } + .fullScreenCover(isPresented: $isPresentedQuickLook) { + ViewerQuickLook(url: URL(fileURLWithPath: fileNamePath), index: $index, isPresentedQuickLook: $isPresentedQuickLook, uploadAssets: uploadAssets) + .ignoresSafeArea() + } + .onReceive(uploadAssets.$dismiss) { newValue in + if newValue { + presentationMode.wrappedValue.dismiss() + } + } + .onTapGesture { + UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.endEditing(true) + } + .onDisappear { + uploadAssets.dismiss = true + } + } + + struct ImageAsset: View { + @ObservedObject var uploadAssets: NCUploadAssets + @State var index: Int + + var body: some View { + ZStack(alignment: .bottomTrailing) { + if index < uploadAssets.previewStore.count { + let item = uploadAssets.previewStore[index] + Image(uiImage: item.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 80, height: 80, alignment: .center) + .cornerRadius(10) + if item.assetType == .livePhoto && item.data == nil { + Image(systemName: "livephoto") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + .foregroundColor(.white) + .padding(.horizontal, 5) + .padding(.vertical, 5) + } else if item.assetType == .video { + Image(systemName: "video.fill") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + .foregroundColor(.white) + .padding(.horizontal, 5) + .padding(.vertical, 5) + } + } + } + } + } +} + +// MARK: - Preview + +struct UploadAssetsView_Previews: PreviewProvider { + static var previews: some View { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + let uploadAssets = NCUploadAssets(assets: [], serverUrl: "/", userBaseUrl: appDelegate) + UploadAssetsView(uploadAssets: uploadAssets) + } + } +}