diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift index 320fb2fa2..a8eecb8bd 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift @@ -28,6 +28,12 @@ import UIKit typealias UploadFileDisplayed = CornerCellContainer final class UploadQueueViewController: UIViewController { + private let errorFile = UploadFileDisplayed(isFirstInList: true, isLastInList: true, content: UploadFile()) + + enum SectionModel: Differentiable { + case error, files + } + @IBOutlet var tableView: UITableView! @IBOutlet var retryButton: UIBarButtonItem! @IBOutlet var cancelButton: UIBarButtonItem! @@ -37,6 +43,7 @@ final class UploadQueueViewController: UIViewController { var currentDirectory: File! private var frozenUploadingFiles = [UploadFileDisplayed]() + private lazy var sections = buildSections(files: [UploadFileDisplayed]()) private var notificationToken: NotificationToken? override func viewDidLoad() { @@ -45,6 +52,7 @@ final class UploadQueueViewController: UIViewController { navigationItem.hideBackButtonText() tableView.register(cellView: UploadTableViewCell.self) + tableView.register(cellView: ErrorUploadTableViewCell.self) retryButton.accessibilityLabel = KDriveResourcesStrings.Localizable.buttonRetry cancelButton.accessibilityLabel = KDriveResourcesStrings.Localizable.buttonCancel @@ -53,7 +61,7 @@ final class UploadQueueViewController: UIViewController { ReachabilityListener.instance.observeNetworkChange(self) { [weak self] _ in Task { @MainActor in - self?.tableView.reloadData() + self?.reloadCollectionView() } } } @@ -94,7 +102,7 @@ final class UploadQueueViewController: UIViewController { } guard let newResults else { - reloadCollectionViewWith([]) + reloadCollectionView(with: []) return } @@ -105,22 +113,52 @@ final class UploadQueueViewController: UIViewController { content: frozenFile) } - reloadCollectionViewWith(wrappedFrozenFiles) + reloadCollectionView(with: wrappedFrozenFiles) } } - func reloadCollectionViewWith(_ frozenFiles: [UploadFileDisplayed]) { - let changeSet = StagedChangeset(source: frozenUploadingFiles, target: frozenFiles) + @MainActor func reloadCollectionView(with frozenFiles: [UploadFileDisplayed]? = nil) { + let newSections: [ArraySection] + if let frozenFiles { + newSections = buildSections(files: frozenFiles) + } else { + newSections = buildSections(files: frozenUploadingFiles) + } + + let changeSet = StagedChangeset(source: sections, target: newSections) + tableView.reload(using: changeSet, with: UITableView.RowAnimation.automatic, interrupt: { $0.changeCount > Endpoint.itemsPerPage }, - setData: { self.frozenUploadingFiles = $0 }) - - if frozenFiles.isEmpty { + setData: { newValues in + if let frozenFiles { + frozenUploadingFiles = frozenFiles + } + sections = newValues + }) + + if let frozenFiles, frozenFiles.isEmpty { navigationController?.popViewController(animated: true) } } + private func buildSections(files: [UploadFileDisplayed]) -> [ArraySection] { + guard !isUploadLimited else { + return [ + ArraySection(model: SectionModel.error, elements: [errorFile]), + ArraySection(model: SectionModel.files, elements: files) + ] + } + + return [ + ArraySection(model: SectionModel.files, elements: files) + ] + } + + private var isUploadLimited: Bool { + UserDefaults.shared.isWifiOnly && ReachabilityListener.instance.currentStatus == .cellular + } + @IBAction func cancelButtonPressed(_ sender: UIBarButtonItem) { uploadQueue.cancelAllOperations(withParent: currentDirectory.id, userId: accountManager.currentUserId, @@ -143,23 +181,46 @@ final class UploadQueueViewController: UIViewController { extension UploadQueueViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return frozenUploadingFiles.count + guard let rows = sections[safe: section] else { + return 0 + } + return rows.elements.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(type: UploadTableViewCell.self, for: indexPath) - let fileWrapper = frozenUploadingFiles[indexPath.row] - let frozenFile = fileWrapper.content + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } - cell.initWithPositionAndShadow(isFirst: fileWrapper.isFirstInList, - isLast: fileWrapper.isLastInList) + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 && isUploadLimited { + let cell = tableView.dequeueReusableCell(type: ErrorUploadTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, + isLast: true) + cell.delegate = self + return cell + } else { + let cell = tableView.dequeueReusableCell(type: UploadTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, + isLast: indexPath.row == self.tableView( + tableView, + numberOfRowsInSection: indexPath.section + ) - 1) + + guard let frozenUploadingFiles = sections[safe: indexPath.section]?.elements, + let file = frozenUploadingFiles[safe: indexPath.row]?.content, !file.isInvalidated else { + return cell + } - if !frozenFile.isInvalidated { - let progress: CGFloat? = (frozenFile.progress != nil) ? CGFloat(frozenFile.progress!) : nil - cell.configureWith(frozenUploadFile: frozenFile, progress: progress) + let progress: CGFloat? = (file.progress != nil) ? CGFloat(file.progress!) : nil + cell.configureWith(frozenUploadFile: file, progress: progress) + cell.selectionStyle = .none + return cell } + } +} - cell.selectionStyle = .none - return cell +extension UploadQueueViewController: AccessParametersDelegate { + func parameterButtonTapped() { + navigationController?.pushViewController(PhotoSyncSettingsViewController(), animated: true) } } diff --git a/kDrive/UI/Controller/Home/RootMenuHeaderView.swift b/kDrive/UI/Controller/Home/RootMenuHeaderView.swift index 89b305334..c6b687ec3 100644 --- a/kDrive/UI/Controller/Home/RootMenuHeaderView.swift +++ b/kDrive/UI/Controller/Home/RootMenuHeaderView.swift @@ -35,6 +35,10 @@ class RootMenuHeaderView: UICollectionReusableView { var onUploadCardViewTapped: (() -> Void)? + deinit { + NotificationCenter.default.removeObserver(self) + } + override func awakeFromNib() { super.awakeFromNib() @@ -45,9 +49,7 @@ class RootMenuHeaderView: UICollectionReusableView { radius: 10 ) - uploadCardView.titleLabel.text = KDriveResourcesStrings.Localizable.uploadInProgressTitle - uploadCardView.progressView.setInfomaniakStyle() - uploadCardView.progressView.enableIndeterminate() + updateWifiView() uploadCardView.isHidden = true offlineView.isHidden = true @@ -55,6 +57,13 @@ class RootMenuHeaderView: UICollectionReusableView { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapOnUploadCardView)) uploadCardView.addGestureRecognizer(tapGestureRecognizer) + + NotificationCenter.default.addObserver( + self, + selector: #selector(reloadWifiView), + name: .reloadWifiView, + object: nil + ) } func configureInCollectionView( @@ -82,6 +91,10 @@ class RootMenuHeaderView: UICollectionReusableView { } } + @objc func reloadWifiView(_ notification: Notification) { + updateWifiView() + } + @objc func didTapOnUploadCardView() { onUploadCardViewTapped?() } @@ -113,11 +126,34 @@ class RootMenuHeaderView: UICollectionReusableView { guard let self else { return } offlineView.isHidden = status != .offline - reloadHeader() + + updateWifiView() } } } + private func updateWifiView() { + if UserDefaults.shared.isWifiOnly && ReachabilityListener.instance.currentStatus == .cellular { + uploadCardView.titleLabel.text = KDriveResourcesStrings.Localizable.uploadPausedTitle + uploadCardView.progressView.isHidden = true + uploadCardView.iconView.image = UIImage(systemName: "exclamationmark.arrow.triangle.2.circlepath") + uploadCardView.iconView.isHidden = false + uploadCardView.iconView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + uploadCardView.iconView.widthAnchor.constraint(equalToConstant: 24), + uploadCardView.iconView.heightAnchor.constraint(equalToConstant: 24) + ]) + uploadCardView.iconView.tintColor = .gray + } else { + uploadCardView.titleLabel.text = KDriveResourcesStrings.Localizable.uploadInProgressTitle + uploadCardView.progressView.isHidden = false + uploadCardView.iconView.isHidden = true + uploadCardView.progressView.setInfomaniakStyle() + uploadCardView.progressView.enableIndeterminate() + } + reloadHeader() + } + private func reloadHeader() { hideIfNeeded() diff --git a/kDrive/UI/Controller/Home/RootMenuHeaderView.xib b/kDrive/UI/Controller/Home/RootMenuHeaderView.xib index a4f4a3155..5b7319805 100644 --- a/kDrive/UI/Controller/Home/RootMenuHeaderView.xib +++ b/kDrive/UI/Controller/Home/RootMenuHeaderView.xib @@ -172,7 +172,7 @@ - + diff --git a/kDrive/UI/Controller/Menu/MenuViewController.swift b/kDrive/UI/Controller/Menu/MenuViewController.swift index 2a3765f07..760db98f9 100644 --- a/kDrive/UI/Controller/Menu/MenuViewController.swift +++ b/kDrive/UI/Controller/Menu/MenuViewController.swift @@ -84,6 +84,10 @@ final class MenuViewController: UITableViewController, SelectSwitchDriveDelegate fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + override func viewDidLoad() { super.viewDidLoad() @@ -92,12 +96,27 @@ final class MenuViewController: UITableViewController, SelectSwitchDriveDelegate tableView.register(cellView: MenuTableViewCell.self) tableView.register(cellView: MenuTopTableViewCell.self) tableView.register(cellView: UploadsInProgressTableViewCell.self) + tableView.register(cellView: UploadsPausedTableViewCell.self) tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) updateTableContent() navigationItem.title = KDriveResourcesStrings.Localizable.menuTitle navigationItem.hideBackButtonText() + + NotificationCenter.default.addObserver( + self, + selector: #selector(reloadWifiView), + name: .reloadWifiView, + object: nil + ) + + ReachabilityListener.instance.observeNetworkChange(self) { [weak self] _ in + Task { @MainActor in + let indexPath = IndexPath(row: 0, section: 1) + self?.tableView.reloadRows(at: [indexPath], with: .automatic) + } + } } override func viewWillAppear(_ animated: Bool) { @@ -158,6 +177,10 @@ final class MenuViewController: UITableViewController, SelectSwitchDriveDelegate sections.insert(.uploads, at: 1) } } + + @objc func reloadWifiView(_ notification: Notification) { + reloadData() + } } // MARK: - Table view delegate @@ -189,11 +212,18 @@ extension MenuViewController { cell.switchDriveButton.addTarget(self, action: #selector(switchDriveButtonPressed(_:)), for: .touchUpInside) return cell } else if section == .uploads { - let cell = tableView.dequeueReusableCell(type: UploadsInProgressTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow(isFirst: true, isLast: true) - cell.progressView.enableIndeterminate() - cell.setUploadCount(uploadCountManager?.uploadCount ?? 0) - return cell + if UserDefaults.shared.isWifiOnly && ReachabilityListener.instance.currentStatus == .cellular { + let cell = tableView.dequeueReusableCell(type: UploadsPausedTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + cell.setUploadCount(uploadCountManager?.uploadCount ?? 0) + return cell + } else { + let cell = tableView.dequeueReusableCell(type: UploadsInProgressTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + cell.progressView.enableIndeterminate() + cell.setUploadCount(uploadCountManager?.uploadCount ?? 0) + return cell + } } else { let action = section.actions[indexPath.row] let cell = tableView.dequeueReusableCell(type: MenuTableViewCell.self, for: indexPath) diff --git a/kDrive/UI/Controller/Menu/ParameterTableViewController.swift b/kDrive/UI/Controller/Menu/ParameterTableViewController.swift index 5fac606e7..8bfac8749 100644 --- a/kDrive/UI/Controller/Menu/ParameterTableViewController.swift +++ b/kDrive/UI/Controller/Menu/ParameterTableViewController.swift @@ -77,7 +77,7 @@ class ParameterTableViewController: BaseGroupedTableViewController { case theme case notifications case security - case wifi + case offlineSync case storage case about case deleteAccount @@ -92,8 +92,8 @@ class ParameterTableViewController: BaseGroupedTableViewController { return KDriveResourcesStrings.Localizable.notificationTitle case .security: return KDriveResourcesStrings.Localizable.securityTitle - case .wifi: - return KDriveResourcesStrings.Localizable.settingsOnlyWifiSyncTitle + case .offlineSync: + return KDriveResourcesStrings.Localizable.syncWifiSettingsTitle case .storage: return KDriveResourcesStrings.Localizable.manageStorageTitle case .about: @@ -116,6 +116,7 @@ class ParameterTableViewController: BaseGroupedTableViewController { tableView.register(cellView: ParameterTableViewCell.self) tableView.register(cellView: ParameterAboutTableViewCell.self) tableView.register(cellView: ParameterWifiTableViewCell.self) + tableView.register(cellView: AboutDetailTableViewCell.self) navigationItem.hideBackButtonText() checkMykSuiteEnabledAndRefresh() @@ -234,15 +235,7 @@ class ParameterTableViewController: BaseGroupedTableViewController { cell.valueLabel.text = getNotificationText() } return cell - case .wifi: - let cell = tableView.dequeueReusableCell(type: ParameterWifiTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow() - cell.valueSwitch.isOn = UserDefaults.shared.isWifiOnly - cell.switchHandler = { sender in - MatomoUtils.track(eventWithCategory: .settings, name: "onlyWifiTransfer", value: sender.isOn) - UserDefaults.shared.isWifiOnly = sender.isOn - } - return cell + case .security, .storage, .about, .deleteAccount: let cell = tableView.dequeueReusableCell(type: ParameterAboutTableViewCell.self, for: indexPath) cell.initWithPositionAndShadow( @@ -251,6 +244,16 @@ class ParameterTableViewController: BaseGroupedTableViewController { ) cell.titleLabel.text = row.title return cell + + case .offlineSync: + let cell = tableView.dequeueReusableCell(type: AboutDetailTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow( + isFirst: indexPath.row == 0, + isLast: indexPath.row == GeneralParameterRow.allCases.count - 1 + ) + cell.titleLabel.text = KDriveResourcesStrings.Localizable.syncWifiSettingsTitle + cell.detailLabel.text = UserDefaults.shared.syncOfflineMode.title + return cell } } @@ -315,8 +318,11 @@ class ParameterTableViewController: BaseGroupedTableViewController { navigationController?.pushViewController(NotificationsSettingsTableViewController(), animated: true) case .security: navigationController?.pushViewController(SecurityTableViewController(), animated: true) - case .wifi: - break + case .offlineSync: + navigationController?.pushViewController( + WifiSyncSettingsViewController(selectedMode: UserDefaults.shared.syncOfflineMode, offlineSync: true), + animated: true + ) case .about: navigationController?.pushViewController(AboutTableViewController(), animated: true) case .deleteAccount: diff --git a/kDrive/UI/Controller/Menu/PhotoSyncSettingsViewController.swift b/kDrive/UI/Controller/Menu/PhotoSyncSettingsViewController.swift index 5f8bd75cf..c84315bc2 100644 --- a/kDrive/UI/Controller/Menu/PhotoSyncSettingsViewController.swift +++ b/kDrive/UI/Controller/Menu/PhotoSyncSettingsViewController.swift @@ -30,24 +30,25 @@ final class PhotoSyncSettingsViewController: BaseGroupedTableViewController { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var photoLibraryUploader: PhotoLibraryUploader @LazyInjectService var freeSpaceService: FreeSpaceService + @LazyInjectService var uploadQueue: UploadQueue - private enum PhotoSyncSection { + private enum PhotoSyncSection: Int { case syncSwitch case syncLocation case syncSettings case syncDenied } - private enum PhotoSyncSwitchRows: CaseIterable { + private enum PhotoSyncSwitchRows: Int, CaseIterable { case syncSwitch } - private enum PhotoSyncLocationRows: CaseIterable { + private enum PhotoSyncLocationRows: Int, CaseIterable { case driveSelection case folderSelection } - private enum PhotoSyncSettingsRows: CaseIterable { + private enum PhotoSyncSettingsRows: Int, CaseIterable { case syncMode case importPicturesSwitch case importVideosSwitch @@ -55,6 +56,7 @@ final class PhotoSyncSettingsViewController: BaseGroupedTableViewController { case createDatedSubFolders case deleteAssetsAfterImport case photoFormat + case wifiSync } private enum PhotoSyncDeniedRows: CaseIterable { @@ -110,6 +112,7 @@ final class PhotoSyncSettingsViewController: BaseGroupedTableViewController { tableView.register(cellView: PhotoAccessDeniedTableViewCell.self) tableView.register(cellView: PhotoSyncSettingsTableViewCell.self) tableView.register(cellView: PhotoFormatTableViewCell.self) + tableView.register(cellView: AboutDetailTableViewCell.self) let view = FooterButtonView.instantiate(title: KDriveResourcesStrings.Localizable.buttonSave) view.delegate = self @@ -224,6 +227,12 @@ final class PhotoSyncSettingsViewController: BaseGroupedTableViewController { let newSettings = PhotoSyncSettings(value: liveNewSyncSettings) photoLibraryUploader.enableSync(newSettings) + uploadQueue.retryAllOperations( + withParent: newSettings.parentDirectoryId, + userId: newSettings.userId, + driveId: newSettings.driveId + ) + uploadQueue.updateQueueSuspension() } private func requestAuthorization() async -> PHAuthorizationStatus { @@ -389,6 +398,12 @@ extension PhotoSyncSettingsViewController { cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == settingsRows.count - 1) cell.configure(with: liveNewSyncSettings.photoFormat) return cell + case .wifiSync: + let cell = tableView.dequeueReusableCell(type: AboutDetailTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == settingsRows.count - 1) + cell.titleLabel.text = KDriveResourcesStrings.Localizable.syncWifiPicturesTitle + cell.detailLabel.text = liveNewSyncSettings.wifiSync.title + return cell } case .syncDenied: switch deniedRows[indexPath.row] { @@ -446,6 +461,10 @@ extension PhotoSyncSettingsViewController { .instantiate(selectedFormat: liveNewSyncSettings.photoFormat) selectPhotoFormatViewController.delegate = self navigationController?.pushViewController(selectPhotoFormatViewController, animated: true) + case .wifiSync: + let wifiSyncSettingsViewController = WifiSyncSettingsViewController(selectedMode: liveNewSyncSettings.wifiSync) + wifiSyncSettingsViewController.delegate = self + navigationController?.pushViewController(wifiSyncSettingsViewController, animated: true) default: break } @@ -460,7 +479,14 @@ extension PhotoSyncSettingsViewController: SelectDriveDelegate { driveFileManager = accountManager.getDriveFileManager(for: drive.id, userId: drive.userId) selectedDirectory = nil updateSaveButtonState() - tableView.reloadRows(at: [IndexPath(row: 0, section: 1), IndexPath(row: 1, section: 1)], with: .fade) + tableView.reloadRows( + at: [IndexPath(row: PhotoSyncSettingsRows.syncMode.rawValue, section: PhotoSyncSection.syncLocation.rawValue), + IndexPath( + row: PhotoSyncSettingsRows.importPicturesSwitch.rawValue, + section: PhotoSyncSection.syncLocation.rawValue + )], + with: .fade + ) } } @@ -470,7 +496,13 @@ extension PhotoSyncSettingsViewController: SelectFolderDelegate { func didSelectFolder(_ folder: File) { selectedDirectory = folder updateSaveButtonState() - tableView.reloadRows(at: [IndexPath(row: 1, section: 1)], with: .fade) + tableView.reloadRows( + at: [IndexPath( + row: PhotoSyncSettingsRows.importPicturesSwitch.rawValue, + section: PhotoSyncSection.syncLocation.rawValue + )], + with: .fade + ) } } @@ -480,7 +512,10 @@ extension PhotoSyncSettingsViewController: SelectPhotoFormatDelegate { func didSelectPhotoFormat(_ format: PhotoFileFormat) { liveNewSyncSettings.photoFormat = format updateSaveButtonState() - tableView.reloadData() + tableView.reloadRows( + at: [IndexPath(row: PhotoSyncSettingsRows.photoFormat.rawValue, section: PhotoSyncSection.syncSettings.rawValue)], + with: .fade + ) } } @@ -517,3 +552,16 @@ extension PhotoSyncSettingsViewController: PhotoSyncSettingsTableViewCellDelegat updateSaveButtonState() } } + +extension PhotoSyncSettingsViewController: WifiSyncSettingsDelegate { + func didSelectSyncMode(_ mode: SyncMode) { + liveNewSyncSettings.wifiSync = mode + UserDefaults.shared.isWifiOnly = (mode == .onlyWifi) + updateSaveButtonState() + tableView.reloadRows( + at: [IndexPath(row: PhotoSyncSettingsRows.wifiSync.rawValue, section: PhotoSyncSection.syncSettings.rawValue)], + with: .fade + ) + NotificationCenter.default.post(name: .reloadWifiView, object: nil) + } +} diff --git a/kDrive/UI/Controller/Menu/WifiSyncSettingsViewController.swift b/kDrive/UI/Controller/Menu/WifiSyncSettingsViewController.swift new file mode 100644 index 000000000..d83246017 --- /dev/null +++ b/kDrive/UI/Controller/Menu/WifiSyncSettingsViewController.swift @@ -0,0 +1,93 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + 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 InfomaniakCoreUIKit +import InfomaniakDI +import kDriveCore +import kDriveResources +import UIKit + +protocol WifiSyncSettingsDelegate: AnyObject { + func didSelectSyncMode(_ mode: SyncMode) +} + +class WifiSyncSettingsViewController: BaseGroupedTableViewController { + @LazyInjectService private var appNavigable: AppNavigable + + private var tableContent: [SyncMode] = SyncMode.allCases + private var selectedMode: SyncMode + private var offlineSync: Bool + weak var delegate: WifiSyncSettingsDelegate? + + init(selectedMode: SyncMode, offlineSync: Bool = false) { + self.selectedMode = selectedMode + self.offlineSync = offlineSync + super.init() + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = KDriveResourcesStrings.Localizable.syncWifiSettingsTitle + + tableView.register(cellView: ParameterSyncTableViewCell.self) + tableView.allowsMultipleSelection = false + } + + static func instantiate(selectedMode: SyncMode) -> WifiSyncSettingsViewController { + let viewController = WifiSyncSettingsViewController(selectedMode: selectedMode) + return viewController + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + MatomoUtils.track(view: [MatomoUtils.Views.menu.displayName, MatomoUtils.Views.settings.displayName, "SelectSyncMode"]) + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableContent.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(type: ParameterSyncTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + let currentMode = tableContent[indexPath.row] + cell.syncTitleLabel.text = currentMode.title + cell.syncDetailLabel.text = currentMode.selectionTitle + if currentMode == selectedMode { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let mode = tableContent[indexPath.row] + MatomoUtils.track(eventWithCategory: .settings, name: "mod\(mode.rawValue.capitalized)") + if !offlineSync { + delegate?.didSelectSyncMode(mode) + } else { + UserDefaults.shared.syncOfflineMode = mode + } + + navigationController?.popViewController(animated: true) + } +} diff --git a/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.swift b/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.swift new file mode 100644 index 000000000..ea76aa6a3 --- /dev/null +++ b/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.swift @@ -0,0 +1,40 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + 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 InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +protocol AccessParametersDelegate: AnyObject { + func parameterButtonTapped() +} + +class ErrorUploadTableViewCell: InsetTableViewCell { + @IBOutlet var bannerView: UIView! + @IBOutlet var errorIconImageView: UIImageView! + @IBOutlet var errorTitleLabel: UILabel! + @IBOutlet var errorDetailLabel: UILabel! + @IBOutlet var settingButton: UIButton! + + weak var delegate: AccessParametersDelegate? + + @IBAction func updateButtonPressed(_ sender: UIButton) { + delegate?.parameterButtonTapped() + } +} diff --git a/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.xib b/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.xib new file mode 100644 index 000000000..3bf5e9028 --- /dev/null +++ b/kDrive/UI/View/Files/Upload/ErrorUploadTableViewCell.xib @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift b/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift index 68ef383b4..0498ed354 100644 --- a/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift +++ b/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift @@ -74,8 +74,13 @@ final class UploadTableViewCell: InsetTableViewCell { if let error = uploadFile.error, error != .taskRescheduled { cardContentView.retryButton?.isHidden = false - cardContentView.detailsLabel.text = KDriveResourcesStrings.Localizable - .errorUpload + " (\(error.localizedDescription))" + if error.localizedDescription == KDriveResourcesStrings.Localizable.uploadOverDataRestrictedError { + cardContentView.detailsLabel.text = error.localizedDescription + } else { + cardContentView.detailsLabel.text = KDriveResourcesStrings.Localizable + .errorUpload + " (\(error.localizedDescription))" + } + } else { cardContentView.retryButton? .isHidden = (uploadFile.maxRetryCount > 0) // Display retry for uploads that reached automatic retry limit diff --git a/kDrive/UI/View/Home/UploadsPausedTableViewCell.swift b/kDrive/UI/View/Home/UploadsPausedTableViewCell.swift new file mode 100644 index 000000000..82c72c3ff --- /dev/null +++ b/kDrive/UI/View/Home/UploadsPausedTableViewCell.swift @@ -0,0 +1,30 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + 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 InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +class UploadsPausedTableViewCell: InsetTableViewCell { + @IBOutlet var subtitleLabel: IKLabel! + + func setUploadCount(_ count: Int) { + subtitleLabel.text = KDriveResourcesStrings.Localizable.uploadInProgressNumberFile(count) + } +} diff --git a/kDrive/UI/View/Home/UploadsPausedTableViewCell.xib b/kDrive/UI/View/Home/UploadsPausedTableViewCell.xib new file mode 100644 index 000000000..f01b4c9ff --- /dev/null +++ b/kDrive/UI/View/Home/UploadsPausedTableViewCell.xib @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.swift b/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.swift new file mode 100644 index 000000000..1544217af --- /dev/null +++ b/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.swift @@ -0,0 +1,38 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + 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 InfomaniakCoreUIKit +import kDriveResources +import UIKit + +class ParameterSyncTableViewCell: InsetTableViewCell { + @IBOutlet var syncTitleLabel: UILabel! + @IBOutlet var syncDetailLabel: UILabel! + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + contentInsetView.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + if selected { + contentInsetView.borderColor = KDriveResourcesAsset.infomaniakColor.color + contentInsetView.borderWidth = 2 + } else { + contentInsetView.borderWidth = 0 + } + } +} diff --git a/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.xib b/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.xib new file mode 100644 index 000000000..acc02f70e --- /dev/null +++ b/kDrive/UI/View/Menu/Parameters/ParameterSyncTableViewCell.xib @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift index bfe181e39..850fdfd09 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift @@ -25,7 +25,7 @@ import RealmSwift /// Something to centralize schema versioning public enum RealmSchemaVersion { /// Current version of the Upload Realm - static let upload: UInt64 = 21 + static let upload: UInt64 = 22 /// Current version of the Drive Realm static let drive: UInt64 = 13 @@ -204,6 +204,20 @@ public class DriveFileManagerConstants { newObject["uploadingSession"] = nil } } + + // Migration to add syncWifi + if oldSchemaVersion < 22 { + migration.enumerateObjects(ofType: PhotoSyncSettings.className()) { _, newObject in + guard let newObject else { + return + } + if UserDefaults.shared.isWifiOnly { + newObject["wifiSync"] = SyncMode.onlyWifi + } else { + newObject["wifiSync"] = SyncMode.wifiAndMobileData + } + } + } } /// Path of the upload DB diff --git a/kDriveCore/Data/Cache/PhotoSyncSettings.swift b/kDriveCore/Data/Cache/PhotoSyncSettings.swift index 316f41721..06b90f19a 100644 --- a/kDriveCore/Data/Cache/PhotoSyncSettings.swift +++ b/kDriveCore/Data/Cache/PhotoSyncSettings.swift @@ -50,6 +50,7 @@ public final class PhotoSyncSettings: Object { @Persisted public var createDatedSubFolders = false @Persisted public var deleteAssetsAfterImport = false @Persisted public var photoFormat: PhotoFileFormat = .jpg + @Persisted public var wifiSync: SyncMode = .wifiAndMobileData public init(userId: Int, driveId: Int, @@ -62,7 +63,8 @@ public final class PhotoSyncSettings: Object { syncScreenshots: Bool, createDatedSubFolders: Bool, deleteAssetsAfterImport: Bool, - photoFormat: PhotoFileFormat) { + photoFormat: PhotoFileFormat, + wifiSync: SyncMode) { super.init() self.userId = userId self.driveId = driveId @@ -76,6 +78,7 @@ public final class PhotoSyncSettings: Object { self.createDatedSubFolders = createDatedSubFolders self.deleteAssetsAfterImport = deleteAssetsAfterImport self.photoFormat = photoFormat + self.wifiSync = wifiSync } override public init() { @@ -95,6 +98,7 @@ public final class PhotoSyncSettings: Object { deleteAssetsAfterImport == settings.deleteAssetsAfterImport && syncMode == settings.syncMode && fromDate == settings.fromDate && - photoFormat == settings.photoFormat + photoFormat == settings.photoFormat && + wifiSync == settings.wifiSync } } diff --git a/kDriveCore/Data/Models/DriveError.swift b/kDriveCore/Data/Models/DriveError.swift index f85b04a66..38073de6e 100644 --- a/kDriveCore/Data/Models/DriveError.swift +++ b/kDriveCore/Data/Models/DriveError.swift @@ -141,6 +141,11 @@ public struct DriveError: Error, Equatable { localizedString: KDriveResourcesStrings.Localizable.errorCache) public static let unknownError = DriveError(type: .localError, code: "unknownError") + public static let uploadOverDataRestrictedError = DriveError(type: .localError, + code: "uploadOverDataRestrictedError", + localizedString: KDriveResourcesStrings.Localizable + .uploadOverDataRestrictedError) + // MARK: - Server public static let refreshToken = DriveError(type: .serverError, code: "refreshToken") @@ -261,7 +266,8 @@ public struct DriveError: Error, Equatable { uploadTokenIsNotValid, fileAlreadyExistsError, errorDeviceStorage, - limitExceededError] + limitExceededError, + uploadOverDataRestrictedError] private static let encoder = JSONEncoder() private static let decoder = JSONDecoder() diff --git a/kDriveCore/Data/UploadQueue/Operation/UploadOperation+Error.swift b/kDriveCore/Data/UploadQueue/Operation/UploadOperation+Error.swift index ef974c081..5e0fe99a3 100644 --- a/kDriveCore/Data/UploadQueue/Operation/UploadOperation+Error.swift +++ b/kDriveCore/Data/UploadQueue/Operation/UploadOperation+Error.swift @@ -127,6 +127,10 @@ extension UploadOperation { // Silently stop if an UploadFile is no longer in base // _not_ overriding file.error self.cancel() + + case .uploadOverDataRestrictedError: + file.error = DriveError.uploadOverDataRestrictedError + self.uploadQueue.suspendAllOperations() } errorHandled = true diff --git a/kDriveCore/Data/UploadQueue/Operation/UploadOperation+PHAsset.swift b/kDriveCore/Data/UploadQueue/Operation/UploadOperation+PHAsset.swift index 5ae8f002c..168adc1c6 100644 --- a/kDriveCore/Data/UploadQueue/Operation/UploadOperation+PHAsset.swift +++ b/kDriveCore/Data/UploadQueue/Operation/UploadOperation+PHAsset.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakCore extension UploadOperation { func getPhAssetIfNeeded() async throws { @@ -56,4 +57,21 @@ extension UploadOperation { file.pathURL = url } } + + func checkForRestrictedUploadOverDataMode() throws { + let file = try readOnlyFile() + + guard file.type == .phAsset else { + // This UploadFile is not a PHAsset, return silently + return + } + + let status = ReachabilityListener.instance.currentStatus + let canUpload = !(status == .cellular && UserDefaults.shared.isWifiOnly) + + guard !canUpload else { + return + } + throw ErrorDomain.uploadOverDataRestrictedError + } } diff --git a/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift b/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift index 3b7d3c60a..c058344fa 100644 --- a/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift +++ b/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift @@ -58,6 +58,8 @@ public final class UploadOperation: AsynchronousOperation, UploadOperationable { case operationFinished /// Cannot decrease further retry count, already zero case retryCountIsZero + /// Cannot upload image because we are not in wifi + case uploadOverDataRestrictedError } // MARK: - Attributes @@ -127,6 +129,9 @@ public final class UploadOperation: AsynchronousOperation, UploadOperationable { // Clean existing error if any try self.cleanUploadFileError() + // Pause the upload depending on the status + try self.checkForRestrictedUploadOverDataMode() + // Fetch content from local library if needed try await self.getPhAssetIfNeeded() diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift index 75b1f899f..9f487fcee 100644 --- a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift +++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift @@ -561,4 +561,10 @@ extension UploadQueue: UploadQueueable { return operation } + + public func updateQueueSuspension() { + let isSuspended = (shouldSuspendQueue || forceSuspendQueue) + operationQueue.isSuspended = isSuspended + Log.uploadQueue("update isSuspended to :\(isSuspended)") + } } diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift index 56fa6f94a..03b1644e5 100644 --- a/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift +++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift @@ -102,7 +102,8 @@ public final class UploadQueue: ParallelismHeuristicDelegate { } let status = ReachabilityListener.instance.currentStatus - return status == .offline || (status != .wifi && UserDefaults.shared.isWifiOnly) + let shouldBeSuspended = status == .offline || (status != .wifi && UserDefaults.shared.isWifiOnly) + return shouldBeSuspended } /// Should suspend operation queue based on explicit `suspendAllOperations()` call @@ -132,13 +133,7 @@ public final class UploadQueue: ParallelismHeuristicDelegate { // Observe network state change ReachabilityListener.instance.observeNetworkChange(self) { [weak self] _ in - guard let self else { - return - } - - let isSuspended = (shouldSuspendQueue || forceSuspendQueue) - operationQueue.isSuspended = isSuspended - Log.uploadQueue("observeNetworkChange :\(isSuspended)") + self?.updateQueueSuspension() } observeMemoryWarnings() diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueueable.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueueable.swift index f341dfcd9..6c246291a 100644 --- a/kDriveCore/Data/UploadQueue/Queue/UploadQueueable.swift +++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueueable.swift @@ -84,4 +84,7 @@ public protocol UploadQueueable { /// /// Also make sure that UploadFiles initiated in FileManager will restart at next retry. func cleanNetworkAndLocalErrorsForAllOperations() + + /// Update queue suspension state + func updateQueueSuspension() } diff --git a/kDriveCore/Utils/DriveUserDefaults+Extension.swift b/kDriveCore/Utils/DriveUserDefaults+Extension.swift index 1fefb6042..71a0aa5f3 100644 --- a/kDriveCore/Utils/DriveUserDefaults+Extension.swift +++ b/kDriveCore/Utils/DriveUserDefaults+Extension.swift @@ -49,6 +49,7 @@ extension UserDefaults.Keys { static let selectedHomeIndex = UserDefaults.Keys(rawValue: "selectedHomeIndex") static let fpStorageVersion = UserDefaults.Keys(rawValue: "fpStorageVersion") static let importPhotoFormat = UserDefaults.Keys(rawValue: "importPhotoFormat") + static let syncOfflineMode = UserDefaults.Keys(rawValue: "syncOfflineMode") } public extension UserDefaults { @@ -336,4 +337,17 @@ public extension UserDefaults { set(newValue.rawValue, forKey: key(.importPhotoFormat)) } } + + var syncOfflineMode: SyncMode { + get { + if let rawValue = object(forKey: key(.syncOfflineMode)) as? String, + let mode = SyncMode(rawValue: rawValue) { + return mode + } + return .onlyWifi + } + set { + set(newValue.rawValue, forKey: key(.syncOfflineMode)) + } + } } diff --git a/kDriveCore/Utils/NotificationName+Extension.swift b/kDriveCore/Utils/NotificationName+Extension.swift index a4454d7e0..1711bbc9c 100644 --- a/kDriveCore/Utils/NotificationName+Extension.swift +++ b/kDriveCore/Utils/NotificationName+Extension.swift @@ -21,4 +21,5 @@ import Foundation public extension Notification.Name { static let locateUploadActionTapped = Notification.Name(rawValue: "kDriveLocateUploadActionTapped") static let reloadDrive = Notification.Name(rawValue: "kDriveReloadDrive") + static let reloadWifiView = Notification.Name(rawValue: "kDriveReloadWifiView") } diff --git a/kDriveCore/Utils/SyncMode.swift b/kDriveCore/Utils/SyncMode.swift new file mode 100644 index 000000000..6b3cc858b --- /dev/null +++ b/kDriveCore/Utils/SyncMode.swift @@ -0,0 +1,44 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 Infomaniak Network SA + + 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 kDriveResources +import RealmSwift +import UIKit + +public enum SyncMode: String, CaseIterable, PersistableEnum { + case onlyWifi + case wifiAndMobileData + + public var title: String { + switch self { + case .onlyWifi: + return KDriveResourcesStrings.Localizable.syncOnlyWifiTitle + case .wifiAndMobileData: + return KDriveResourcesStrings.Localizable.syncWifiAndMobileDataTitle + } + } + + public var selectionTitle: String { + switch self { + case .onlyWifi: + return KDriveResourcesStrings.Localizable.syncOnlyWifiDescription + case .wifiAndMobileData: + return KDriveResourcesStrings.Localizable.syncWifiAndMobileDataDescription + } + } +}