From 571e18dbf8c6ebb99013f1d6cd10e1343a7c2699 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 19 Sep 2024 14:39:00 +0200 Subject: [PATCH 01/31] Varie (#3063) * x 4 Signed-off-by: Marino Faggiana --- .../Data/NCManageDatabase+Metadata.swift | 6 +++--- .../NCCollectionViewCommon.swift | 2 +- iOSClient/Main/NCActionCenter.swift | 4 ++-- .../NCMedia+CollectionViewDelegate.swift | 4 ++-- iOSClient/Media/NCMedia.swift | 4 ++-- iOSClient/Media/NCMediaDataSource.swift | 19 ++++++++++++++++++- iOSClient/NCGlobal.swift | 2 +- iOSClient/Networking/NCNetworking+Task.swift | 12 ++++++------ 8 files changed, 35 insertions(+), 18 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index ee9a197c7a..ff53d9b8e1 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -1070,16 +1070,16 @@ extension NCManageDatabase { return nil } - func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String? = nil, ascending: Bool = false) -> [tableMetadata]? { + func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String? = nil, ascending: Bool = false) -> Results? { do { let realm = try Realm() realm.refresh() if let sortedByKeyPath { let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending) - return Array(results) + return results } else { let results = realm.objects(tableMetadata.self).filter(predicate) - return Array(results) + return results } } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index fca2a7719a..9ef6bca10e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -1117,7 +1117,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS NCNetworking.shared.isOnline, let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), !results.isEmpty { - return results + return Array(results) } return nil } diff --git a/iOSClient/Main/NCActionCenter.swift b/iOSClient/Main/NCActionCenter.swift index 30bc190734..c036a1783c 100644 --- a/iOSClient/Main/NCActionCenter.swift +++ b/iOSClient/Main/NCActionCenter.swift @@ -166,12 +166,12 @@ class NCActionCenter: NSObject, UIDocumentInteractionControllerDelegate, NCSelec if isOffline { if metadata.directory { self.database.setDirectory(serverUrl: serverUrl, offline: false, metadata: metadata) - if let metadatas = database.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND sessionSelector == %@ AND status == %d", + if let results = database.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND sessionSelector == %@ AND status == %d", metadata.account, serverUrl, NCGlobal.shared.selectorSynchronizationOffline, NCGlobal.shared.metadataStatusWaitDownload)) { - database.clearMetadataSession(metadatas: metadatas) + database.clearMetadataSession(metadatas: Array(results)) } } else { database.setOffLocalFile(ocId: metadata.ocId) diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index 5073ddfeb1..0b72bb2fbd 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -50,8 +50,8 @@ extension NCMedia: UICollectionViewDelegate { } else { // ACTIVE SERVERURL serverUrl = metadata.serverUrl - if let metadatas = database.getResultsMetadatas(predicate: getPredicate(filterLivePhotoFile: true), sortedByKeyPath: "date") { - NCViewer().view(viewController: self, metadata: metadata, metadatas: metadatas, indexMetadatas: indexPath.row, image: getImage(metadata: metadataDatasource, width: 1024, cost: indexPath.row)) + if let results = dataSource.getTableMetadatas() { + NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: getImage(metadata: metadataDatasource, width: 1024, cost: indexPath.row)) } } } diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index b5e6332cf0..82b888c6a7 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -141,8 +141,8 @@ class NCMedia: UIViewController { self.loadDataSource() } - let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) - collectionView.addGestureRecognizer(pinchGesture) + // let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) + // collectionView.addGestureRecognizer(pinchGesture) NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in self.layoutType = self.database.getLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "")?.layout ?? NCGlobal.shared.mediaLayoutRatio diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index 2176995433..7b0042e397 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -23,6 +23,7 @@ import UIKit import NextcloudKit +import RealmSwift extension NCMedia { func loadDataSource() { @@ -201,16 +202,28 @@ public class NCMediaDataSource: NSObject { private let utilityFileSystem = NCUtilityFileSystem() private let global = NCGlobal.shared private var metadatas: [Metadata] = [] + private var tableMetadatas: Results? override init() { super.init() } - init(metadatas: [tableMetadata]) { + init(metadatas: Results) { super.init() + self.metadatas.removeAll() for metadata in metadatas { let metadata = getMetadataFromTableMetadata(metadata) self.metadatas.append(metadata) } + + let reference = ThreadSafeReference(to: metadatas) + DispatchQueue.main.async { + do { + let realm = try Realm() + self.tableMetadatas = realm.resolve(reference) + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } } private func insertInMetadatas(metadata: Metadata) { @@ -262,6 +275,10 @@ public class NCMediaDataSource: NSObject { return self.metadatas } + func getTableMetadatas() -> Results? { + return self.tableMetadatas + } + func getMetadata(indexPath: IndexPath) -> Metadata? { if indexPath.row < self.metadatas.count { return self.metadatas[indexPath.row] diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index e10e06c606..dc08b76f0e 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -101,7 +101,7 @@ class NCGlobal: NSObject { func getSizeExtension(width: CGFloat?) -> String { guard let width else { return previewExt512 } - switch (width * 3) { + switch (width * 4) { case 0...119: return previewExt64 case 120...192: diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 0c099e1eb5..04db5ca762 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -143,7 +143,7 @@ extension NCNetworking { self.global.metadataStatusDownloading, self.global.metadataStatusDownloadError, sessionDownload)) { - self.database.clearMetadataSession(metadatas: results) + self.database.clearMetadataSession(metadatas: Array(results)) } } @@ -165,7 +165,7 @@ extension NCNetworking { self.global.metadataStatusDownloading, self.global.metadataStatusDownloadError, sessionDownloadBackground)) { - self.database.clearMetadataSession(metadatas: results) + self.database.clearMetadataSession(metadatas: Array(results)) } } } @@ -186,12 +186,12 @@ extension NCNetworking { if let metadata { self.database.deleteMetadataOcId(metadata.ocId) - } else if let metadatas = self.database.getResultsMetadatas(predicate: NSPredicate(format: "(status == %d || status == %d || status == %d) AND session == %@", + } else if let results = self.database.getResultsMetadatas(predicate: NSPredicate(format: "(status == %d || status == %d || status == %d) AND session == %@", self.global.metadataStatusWaitUpload, self.global.metadataStatusUploading, self.global.metadataStatusUploadError, sessionUpload)) { - self.database.deleteMetadatas(metadatas) + self.database.deleteMetadatas(Array(results)) } } @@ -227,7 +227,7 @@ extension NCNetworking { if let metadata { self.database.deleteMetadataOcId(metadata.ocId) - } else if let metadatas = self.database.getResultsMetadatas(predicate: NSPredicate(format: "(status == %d || status == %d || status == %d) AND (session == %@ || session == %@ || session == %@)", + } else if let results = self.database.getResultsMetadatas(predicate: NSPredicate(format: "(status == %d || status == %d || status == %d) AND (session == %@ || session == %@ || session == %@)", self.global.metadataStatusWaitUpload, self.global.metadataStatusUploading, self.global.metadataStatusUploadError, @@ -235,7 +235,7 @@ extension NCNetworking { sessionUploadBackgroundWWan, sessionUploadBackgroundExt )) { - self.database.deleteMetadatas(metadatas) + self.database.deleteMetadatas(Array(results)) } } } From 9ed11cd6ab573877fdea24cc0bc306e4d4954b03 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 19 Sep 2024 15:29:01 +0200 Subject: [PATCH 02/31] added hasSynchronizationTask Signed-off-by: Marino Faggiana --- .../NCNetworking+Synchronization.swift | 16 +++++++++++++++- iOSClient/Networking/NCNetworkingProcess.swift | 14 +++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/iOSClient/Networking/NCNetworking+Synchronization.swift b/iOSClient/Networking/NCNetworking+Synchronization.swift index 22dfb651e9..136a7c3174 100644 --- a/iOSClient/Networking/NCNetworking+Synchronization.swift +++ b/iOSClient/Networking/NCNetworking+Synchronization.swift @@ -31,7 +31,7 @@ extension NCNetworking { add: Bool, completion: @escaping (_ errorCode: Int, _ num: Int) -> Void = { _, _ in }) { let startDate = Date() - let options = NKRequestOptions(timeout: 120, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) + let options = NKRequestOptions(timeout: 120, taskDescription: "synchronization", queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrl, depth: "infinity", @@ -94,4 +94,18 @@ extension NCNetworking { return false } } + + func hasSynchronizationTask() async -> Bool { + guard let nkSessions = NextcloudKit.shared.nkCommonInstance.nksessions.getArray() else { return false } + + for nkSession in nkSessions { + let tasks = await nkSession.sessionData.session.tasks + for task in tasks.0 { + if task.taskDescription == "synchronization" { + return true + } + } + } + return false + } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index e4174b4aa6..5da9e65d86 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -82,11 +82,15 @@ class NCNetworkingProcess { guard !self.hasRun, NCNetworking.shared.isOnline else { return } self.hasRun = true - let resultsTransfer = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", self.global.metadataStatusInTransfer)) - if resultsTransfer == nil { - // No tranfer, disable - } else { - // transfer enable + Task { + let hasSynchronizationTask = await NCNetworking.shared.hasSynchronizationTask() + print("[DEBUG] \(hasSynchronizationTask)") + let resultsTransfer = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", self.global.metadataStatusInTransfer)) + if resultsTransfer == nil && !hasSynchronizationTask { + // No tranfer, disable + } else { + // transfer enable + } } guard let results = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status != %d", self.global.metadataStatusNormal)) else { return } From b490edba76ff9fb76711dd250c776e14fc5a3d33 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 19 Sep 2024 16:29:23 +0200 Subject: [PATCH 03/31] added test for async image cell Signed-off-by: Marino Faggiana --- .../NCMedia+CollectionViewDataSource.swift | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 1f4e2d3f38..1a9629ae10 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -86,38 +86,18 @@ extension NCMedia: UICollectionViewDataSource { } } - func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let metadata = dataSource.getMetadata(indexPath: indexPath), - let cell = (cell as? NCGridMediaCell) else { return } + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridMediaCell) else { + fatalError("Unable to dequeue NCGridMediaCell with identifier gridCell") + } + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return cell } + let width = self.collectionView.frame.size.width / CGFloat(self.numberOfColumns) let ext = NCGlobal.shared.getSizeExtension(width: width) let imageCache = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) let cost = indexPath.row cell.imageItem.image = imageCache - - if imageCache == nil { - if self.transitionColumns { - cell.imageItem.image = getImage(metadata: metadata, width: width, cost: cost) - } else { - DispatchQueue.global(qos: .userInteractive).async { - let image = self.getImage(metadata: metadata, width: width, cost: cost) - DispatchQueue.main.async { - cell.imageItem.image = image - } - } - } - } else { - print("[DEBUG] in cache, cost \(indexPath.row)") - } - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridMediaCell)! - guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { - return cell - } - cell.date = metadata.date as Date cell.ocId = metadata.ocId @@ -135,6 +115,24 @@ extension NCMedia: UICollectionViewDataSource { cell.selected(false) } + if imageCache == nil { + if self.transitionColumns { + cell.imageItem.image = getImage(metadata: metadata, width: width, cost: cost) + } else { + DispatchQueue.global(qos: .userInteractive).async { + let image = self.getImage(metadata: metadata, width: width, cost: cost) + DispatchQueue.main.async { + if let currentCell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, + currentCell.ocId == metadata.ocId { + currentCell.imageItem.image = image + } + } + } + } + } else { + print("[DEBUG] in cache, cost \(indexPath.row)") + } + return cell } } From efb93510899ccafd8b64be19dc8158be98eb6143 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 07:19:03 +0200 Subject: [PATCH 04/31] color Signed-off-by: Marino Faggiana --- iOSClient/Media/Cell/NCGridMediaCell.xib | 13 ++++--------- .../Media/NCMedia+CollectionViewDataSource.swift | 6 ++++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/iOSClient/Media/Cell/NCGridMediaCell.xib b/iOSClient/Media/Cell/NCGridMediaCell.xib index 0bac8e8a20..6c6588149c 100644 --- a/iOSClient/Media/Cell/NCGridMediaCell.xib +++ b/iOSClient/Media/Cell/NCGridMediaCell.xib @@ -1,25 +1,23 @@ - + - - + - + - + - @@ -72,8 +70,5 @@ - - - diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 1a9629ae10..8207e24453 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -97,6 +97,7 @@ extension NCMedia: UICollectionViewDataSource { let imageCache = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) let cost = indexPath.row + cell.backgroundColor = .secondarySystemBackground cell.imageItem.image = imageCache cell.date = metadata.date as Date cell.ocId = metadata.ocId @@ -115,7 +116,7 @@ extension NCMedia: UICollectionViewDataSource { cell.selected(false) } - if imageCache == nil { + if cell.imageItem.image == nil { if self.transitionColumns { cell.imageItem.image = getImage(metadata: metadata, width: width, cost: cost) } else { @@ -123,8 +124,9 @@ extension NCMedia: UICollectionViewDataSource { let image = self.getImage(metadata: metadata, width: width, cost: cost) DispatchQueue.main.async { if let currentCell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, - currentCell.ocId == metadata.ocId { + currentCell.ocId == metadata.ocId, let image { currentCell.imageItem.image = image + currentCell.backgroundColor = .black } } } From 2ed2fb933719d88e368c8b6d31a5a63ea5333800 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 07:27:38 +0200 Subject: [PATCH 05/31] rename to fileSelect Signed-off-by: Marino Faggiana --- iOSClient/Files/NCFiles.swift | 2 +- ...lectionViewCommon+CollectionViewDataSource.swift | 2 +- ...ollectionViewCommon+CollectionViewDelegate.swift | 8 ++++---- .../NCCollectionViewCommon+DragDrop.swift | 2 +- .../NCCollectionViewCommon+SelectTabBar.swift | 12 ++++++------ .../Collection Common/NCCollectionViewCommon.swift | 4 ++-- .../NCCollectionViewCommonSelectTabBar.swift | 4 ++-- iOSClient/Main/NCDragDrop.swift | 6 +++--- .../Media/NCMedia+CollectionViewDataSource.swift | 2 +- .../Media/NCMedia+CollectionViewDelegate.swift | 8 ++++---- iOSClient/Media/NCMedia+Command.swift | 13 ++++++++----- iOSClient/Media/NCMedia+DragDrop.swift | 2 +- iOSClient/Media/NCMedia.swift | 5 ++++- 13 files changed, 38 insertions(+), 32 deletions(-) diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index 3458d0802d..3db4c55c8f 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -62,7 +62,7 @@ class NCFiles: NCCollectionViewCommon { self.serverUrl = self.utilityFileSystem.getHomeServer(session: self.session) self.isSearchingMode = false self.isEditMode = false - self.selectOcId.removeAll() + self.fileSelect.removeAll() self.layoutForView = self.database.getLayoutForView(account: self.session.account, key: self.layoutKey, serverUrl: self.serverUrl) if self.isLayoutList { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 1a1a2ab83c..774c95e439 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -346,7 +346,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } // Edit mode - if selectOcId.contains(metadata.ocId) { + if fileSelect.contains(metadata.ocId) { cell.selected(true, isEditMode: isEditMode, account: metadata.account) a11yValues.append(NSLocalizedString("_selected_", comment: "")) } else { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 18102e8001..43520f1019 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -33,13 +33,13 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { else { return } if isEditMode { - if let index = selectOcId.firstIndex(of: metadata.ocId) { - selectOcId.remove(at: index) + if let index = fileSelect.firstIndex(of: metadata.ocId) { + fileSelect.remove(at: index) } else { - selectOcId.append(metadata.ocId) + fileSelect.append(metadata.ocId) } collectionView.reloadItems(at: [indexPath]) - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: metadata.userId) + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: metadata.userId) return } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift index 8198d7e781..b0e22f12c5 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift @@ -30,7 +30,7 @@ import NextcloudKit extension NCCollectionViewCommon: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { if isEditMode { - return NCDragDrop().performDrag(selectOcId: selectOcId) + return NCDragDrop().performDrag(fileSelect: fileSelect) } else if let metadata = self.dataSource.getMetadata(indexPath: indexPath) { return NCDragDrop().performDrag(metadata: metadata) } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift index e6490f1955..7335084051 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift @@ -27,12 +27,12 @@ import NextcloudKit extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func selectAll() { - if !selectOcId.isEmpty, self.dataSource.getResultMetadatas().count == selectOcId.count { - selectOcId = [] + if !fileSelect.isEmpty, self.dataSource.getResultMetadatas().count == fileSelect.count { + fileSelect = [] } else { - selectOcId = self.dataSource.getResultMetadatas().compactMap({ $0.ocId }) + fileSelect = self.dataSource.getResultMetadatas().compactMap({ $0.ocId }) } - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: session.userId) + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: session.userId) collectionView.reloadData() } @@ -124,7 +124,7 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func getSelectedMetadatas() -> [tableMetadata] { var selectedMetadatas: [tableMetadata] = [] - for ocId in selectOcId { + for ocId in fileSelect { guard let metadata = database.getMetadataFromOcId(ocId) else { continue } selectedMetadatas.append(metadata) } @@ -133,7 +133,7 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func setEditMode(_ editMode: Bool) { isEditMode = editMode - selectOcId.removeAll() + fileSelect.removeAll() if editMode { navigationItem.leftBarButtonItems = nil diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 9ef6bca10e..b1f9be0e1d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -45,7 +45,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var backgroundImageView = UIImageView() var serverUrl: String = "" var isEditMode = false - var selectOcId: [String] = [] + var fileSelect: [String] = [] var metadataFolder: tableMetadata? var dataSource = NCCollectionViewDataSource() let imageCache = NCImageCache.shared @@ -817,7 +817,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } if isEditMode { - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: session.userId) + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: session.userId) tabBarSelect.show() let select = UIBarButtonItem(title: NSLocalizedString("_cancel_", comment: ""), style: .done) { self.setEditMode(false) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift index 526f4188cb..1f6522b4b3 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift @@ -90,7 +90,7 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { return hostingController.view.isHidden } - func update(selectOcId: [String], metadatas: [tableMetadata]? = nil, userId: String? = nil) { + func update(fileSelect: [String], metadatas: [tableMetadata]? = nil, userId: String? = nil) { if let metadatas { isAnyOffline = false canSetAsOffline = true @@ -130,7 +130,7 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { } enableLock = !isAnyDirectory && canUnlock && !NCCapabilities.shared.getCapabilities(account: controller?.account).capabilityFilesLockVersion.isEmpty } - isSelectedEmpty = selectOcId.isEmpty + isSelectedEmpty = fileSelect.isEmpty } } diff --git a/iOSClient/Main/NCDragDrop.swift b/iOSClient/Main/NCDragDrop.swift index c920b808ad..f7a58ede0b 100644 --- a/iOSClient/Main/NCDragDrop.swift +++ b/iOSClient/Main/NCDragDrop.swift @@ -29,13 +29,13 @@ class NCDragDrop: NSObject { let utilityFileSystem = NCUtilityFileSystem() let database = NCManageDatabase.shared - func performDrag(metadata: tableMetadata? = nil, selectOcId: [String]? = nil) -> [UIDragItem] { + func performDrag(metadata: tableMetadata? = nil, fileSelect: [String]? = nil) -> [UIDragItem] { var metadatas: [tableMetadata] = [] if let metadata, metadata.status == 0, !metadata.isDirectoryE2EE, !metadata.e2eEncrypted { metadatas.append(metadata) - } else if let selectOcId { - for ocId in selectOcId { + } else if let fileSelect { + for ocId in fileSelect { if let metadata = database.getMetadataFromOcId(ocId), metadata.status == 0, !metadata.isDirectoryE2EE, !metadata.e2eEncrypted { metadatas.append(metadata) } diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 8207e24453..81a11fb021 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -110,7 +110,7 @@ extension NCMedia: UICollectionViewDataSource { cell.imageStatus.image = nil } - if isEditMode, selectOcId.contains(metadata.ocId) { + if isEditMode, fileSelect.contains(metadata.ocId) { cell.selected(true) } else { cell.selected(false) diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index 0b72bb2fbd..c71e86c733 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -38,15 +38,15 @@ extension NCMedia: UICollectionViewDelegate { } } if isEditMode { - if let index = selectOcId.firstIndex(of: metadata.ocId) { - selectOcId.remove(at: index) + if let index = fileSelect.firstIndex(of: metadata.ocId) { + fileSelect.remove(at: index) mediaCell?.selected(false) } else { - selectOcId.append(metadata.ocId) + fileSelect.append(metadata.ocId) mediaCell?.selected(true) } - tabBarSelect.selectCount = selectOcId.count + tabBarSelect.selectCount = fileSelect.count } else { // ACTIVE SERVERURL serverUrl = metadata.serverUrl diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 2f80d79d79..4f85af563f 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -37,13 +37,16 @@ extension NCMedia { } func setSelectcancelButton() { - selectOcId.removeAll() - tabBarSelect.selectCount = selectOcId.count + fileSelect.removeAll() + fileDeleted.removeAll() + tabBarSelect.selectCount = fileSelect.count + if let visibleCells = self.collectionView?.indexPathsForVisibleItems.compactMap({ self.collectionView?.cellForItem(at: $0) }) { for case let cell as NCGridMediaCell in visibleCells { cell.selected(false) } } + if isEditMode { activityIndicatorTrailing.constant = 150 selectOrCancelButton.setTitle( NSLocalizedString("_cancel_", comment: ""), for: .normal) @@ -322,16 +325,16 @@ extension NCMedia { extension NCMedia: NCMediaSelectTabBarDelegate { func delete() { - let selectOcId = self.selectOcId.map { $0 } + let fileSelect = self.fileSelect.map { $0 } var alertStyle = UIAlertController.Style.actionSheet if UIDevice.current.userInterfaceIdiom == .pad { alertStyle = .alert } - if !selectOcId.isEmpty { + if !fileSelect.isEmpty { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: alertStyle) alertController.addAction(UIAlertAction(title: NSLocalizedString("_delete_selected_photos_", comment: ""), style: .destructive) { (_: UIAlertAction) in Task { var error = NKError() var ocIds: [String] = [] - for ocId in selectOcId where error == .success { + for ocId in fileSelect where error == .success { if let metadata = self.database.getMetadataFromOcId(ocId) { error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: false, sceneIdentifier: self.controller?.sceneIdentifier) if error == .success { diff --git a/iOSClient/Media/NCMedia+DragDrop.swift b/iOSClient/Media/NCMedia+DragDrop.swift index 521ff14b29..94a70d9799 100644 --- a/iOSClient/Media/NCMedia+DragDrop.swift +++ b/iOSClient/Media/NCMedia+DragDrop.swift @@ -30,7 +30,7 @@ import NextcloudKit extension NCMedia: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { if isEditMode { - return NCDragDrop().performDrag(selectOcId: selectOcId) + return NCDragDrop().performDrag(fileSelect: fileSelect) } else if let ocId = dataSource.getMetadata(indexPath: indexPath)?.ocId, let metadata = database.getMetadataFromOcId(ocId) { return NCDragDrop().performDrag(metadata: metadata) diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 82b888c6a7..2c5a77ae33 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -53,8 +53,9 @@ class NCMedia: UIViewController { let taskDescriptionRetrievesProperties = "retrievesProperties" var isTop: Bool = true var isEditMode = false - var selectOcId: [String] = [] + var fileSelect: [String] = [] var filesExists: [String] = [] + var fileDeleted: [String] = [] var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] let gradient: CAGradientLayer = CAGradientLayer() @@ -254,6 +255,8 @@ class NCMedia: UIViewController { let ocId = userInfo["ocId"] as? [String], let error = userInfo["error"] as? NKError else { return } + fileDeleted = fileDeleted + ocId + dataSource.removeMetadata(ocId) collectionViewReloadData() From 47df2b63dd4e55654a04da77b77b2c3da6f5c5df Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 14:44:44 +0200 Subject: [PATCH 06/31] Improvements (#3066) * createMediaCache Signed-off-by: Marino Faggiana * XCode 16 Signed-off-by: Marino Faggiana * fix XCode 16 Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 -- .../Activity/NCActivityTableViewCell.swift | 2 +- iOSClient/BrowserWeb/NCBrowserWeb.swift | 2 +- iOSClient/Login/NCLoginProvider.swift | 2 +- ...nViewCommon+CollectionViewDataSource.swift | 4 +- ...+CollectionViewDataSourcePrefetching.swift | 2 +- .../NCCollectionViewCommon.swift | 10 +++++ .../NCCollectionViewDownloadThumbnail.swift | 2 +- .../NCCollectionViewUnifiedSearch.swift | 2 +- .../NCMedia+CollectionViewDataSource.swift | 7 ++-- ...+CollectionViewDataSourcePrefetching.swift | 3 +- .../NCMedia+CollectionViewDelegate.swift | 2 +- iOSClient/Media/NCMedia+Command.swift | 35 ++++------------ iOSClient/Media/NCMedia.swift | 34 ++++++---------- iOSClient/Media/NCMediaDataSource.swift | 35 +++++----------- .../Media/NCMediaDownloadThumbnail.swift | 6 +-- iOSClient/Menu/NCOperationSaveLivePhoto.swift | 2 +- iOSClient/Menu/UIViewController+Menu.swift | 2 +- iOSClient/NCGlobal.swift | 11 ++++- iOSClient/NCImageCache.swift | 40 +++++++++++++++++++ .../Networking/NCNetworking+Download.swift | 2 +- .../NCNetworking+Synchronization.swift | 16 +------- iOSClient/Networking/NCNetworking+Task.swift | 17 +++++++- .../Networking/NCNetworking+WebDAV.swift | 6 +-- .../Networking/NCNetworkingProcess.swift | 5 ++- .../NCViewerRichWorkspaceWebView.swift | 2 +- iOSClient/SceneDelegate.swift | 7 ++++ .../Settings/Helpers/NCWebBrowserView.swift | 2 +- iOSClient/Trash/NCTrash+Networking.swift | 2 +- .../NCViewerNextcloudText.swift | 2 +- .../NCViewerRichDocument.swift | 2 +- 31 files changed, 144 insertions(+), 126 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 0c7e243e5d..f15fba53d4 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -4961,7 +4961,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(TEST_HOST)"; TEST_TARGET_NAME = Nextcloud; @@ -5006,7 +5005,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(TEST_HOST)"; TEST_TARGET_NAME = Nextcloud; @@ -5057,7 +5055,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nextcloud.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Nextcloud"; }; @@ -5101,7 +5098,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nextcloud.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Nextcloud"; VALIDATE_PRODUCT = YES; diff --git a/iOSClient/Activity/NCActivityTableViewCell.swift b/iOSClient/Activity/NCActivityTableViewCell.swift index bead1189f7..a6af82556a 100644 --- a/iOSClient/Activity/NCActivityTableViewCell.swift +++ b/iOSClient/Activity/NCActivityTableViewCell.swift @@ -202,7 +202,7 @@ extension NCActivityTableViewCell: UICollectionViewDelegateFlowLayout { } } -class NCOperationDownloadThumbnailActivity: ConcurrentOperation { +class NCOperationDownloadThumbnailActivity: ConcurrentOperation, @unchecked Sendable { var collectionView: UICollectionView? var fileNamePreviewLocalPath: String var fileId: String diff --git a/iOSClient/BrowserWeb/NCBrowserWeb.swift b/iOSClient/BrowserWeb/NCBrowserWeb.swift index fc45fdee5b..1cdabbcc65 100644 --- a/iOSClient/BrowserWeb/NCBrowserWeb.swift +++ b/iOSClient/BrowserWeb/NCBrowserWeb.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit @objc protocol NCBrowserWebDelegate: AnyObject { @objc optional func browserWebDismiss() diff --git a/iOSClient/Login/NCLoginProvider.swift b/iOSClient/Login/NCLoginProvider.swift index 3e2342277a..add68eb699 100644 --- a/iOSClient/Login/NCLoginProvider.swift +++ b/iOSClient/Login/NCLoginProvider.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit import NextcloudKit import FloatingPanel diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 774c95e439..7341493534 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -38,7 +38,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { guard let metadata = self.dataSource.getMetadata(indexPath: indexPath), let cell = (cell as? NCCellProtocol) else { return } let existsPreview = utility.existsImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - let ext = global.getSizeExtension(width: self.sizeImage.width) + let ext = global.getSizeExtension(column: self.column) func downloadAvatar(fileName: String, user: String, dispalyName: String?) { if let image = database.getImageAvatarLoaded(fileName: fileName) { @@ -87,7 +87,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } if metadata.hasPreview && metadata.status == global.metadataStatusNormal && !existsPreview { for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { return } - NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: NCGlobal.shared.getSizeExtension(width: self.sizeImage.width))) + NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: NCGlobal.shared.getSizeExtension(column: self.column))) } } } else { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift index 7bbf6719ac..1c173ea538 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift @@ -27,7 +27,7 @@ import UIKit extension NCCollectionViewCommon: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { guard !isSearchingMode else { return } - let ext = global.getSizeExtension(width: self.sizeImage.width) + let ext = global.getSizeExtension(column: self.column) let metadatas = self.dataSource.getMetadatas(indexPaths: indexPaths) let cost = indexPaths.first?.row ?? 0 diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index b1f9be0e1d..8e5d641a4d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -119,6 +119,16 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } + var column: Int { + if isLayoutPhoto { + return layoutForView?.columnPhoto ?? 3 + } else if isLayoutGrid { + return layoutForView?.columnGrid ?? 3 + } else { + return 0 + } + } + var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift index 71c9d63806..4ea70ad0b6 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift @@ -27,7 +27,7 @@ import Queuer import NextcloudKit import RealmSwift -class NCCollectionViewDownloadThumbnail: ConcurrentOperation { +class NCCollectionViewDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { var metadata: tableMetadata var collectionView: UICollectionView? var ext = "" diff --git a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift index 20e6e3d12f..5fd8de1e52 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift @@ -27,7 +27,7 @@ import Queuer import NextcloudKit import RealmSwift -class NCCollectionViewUnifiedSearch: ConcurrentOperation { +class NCCollectionViewUnifiedSearch: ConcurrentOperation, @unchecked Sendable { var collectionViewCommon: NCCollectionViewCommon var metadatas: [tableMetadata] var searchResult: NKSearchResult diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 81a11fb021..0f2f62ef9f 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -92,8 +92,7 @@ extension NCMedia: UICollectionViewDataSource { } guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return cell } - let width = self.collectionView.frame.size.width / CGFloat(self.numberOfColumns) - let ext = NCGlobal.shared.getSizeExtension(width: width) + let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) let imageCache = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) let cost = indexPath.row @@ -118,10 +117,10 @@ extension NCMedia: UICollectionViewDataSource { if cell.imageItem.image == nil { if self.transitionColumns { - cell.imageItem.image = getImage(metadata: metadata, width: width, cost: cost) + cell.imageItem.image = getImage(metadata: metadata, cost: cost) } else { DispatchQueue.global(qos: .userInteractive).async { - let image = self.getImage(metadata: metadata, width: width, cost: cost) + let image = self.getImage(metadata: metadata, cost: cost) DispatchQueue.main.async { if let currentCell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, currentCell.ocId == metadata.ocId, let image { diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift b/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift index 3d9abaea8d..8be355a6c6 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift @@ -28,8 +28,7 @@ extension NCMedia: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { let cost = indexPaths.first?.row ?? 0 let metadatas = self.dataSource.getMetadatas(indexPaths: indexPaths) - let width = self.collectionView.frame.size.width / CGFloat(self.numberOfColumns) - let ext = NCGlobal.shared.getSizeExtension(width: width) + let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) let percentageCache = (Double(self.imageCache.cache.count) / Double(self.imageCache.countLimit - 1)) * 100 if cost > self.imageCache.countLimit, percentageCache > 75 { diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index c71e86c733..97e0f15d5a 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -51,7 +51,7 @@ extension NCMedia: UICollectionViewDelegate { // ACTIVE SERVERURL serverUrl = metadata.serverUrl if let results = dataSource.getTableMetadatas() { - NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: getImage(metadata: metadataDatasource, width: 1024, cost: indexPath.row)) + NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: getImage(metadata: metadataDatasource, cost: indexPath.row, forceExt: NCGlobal.shared.previewExt1024)) } } } diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 4f85af563f..4997a18c65 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -64,26 +64,10 @@ extension NCMedia { } } - func setTitleDate(_ offset: CGFloat = 10) { - titleDate?.text = "" - if let metadata = dataSource.getMetadatas().first { - let contentOffsetY = collectionView.contentOffset.y - let top = insetsTop + view.safeAreaInsets.top + offset - if insetsTop + view.safeAreaInsets.top + contentOffsetY < 10 { - titleDate?.text = utility.getTitleFromDate(metadata.date as Date) - return - } - let point = CGPoint(x: offset, y: top + contentOffsetY) - if let indexPath = collectionView.indexPathForItem(at: point) { - let cell = self.collectionView(collectionView, cellForItemAt: indexPath) as? NCGridMediaCell - if let date = cell?.date { - self.titleDate?.text = utility.getTitleFromDate(date) - } - } else { - if offset < 20 { - self.setTitleDate(20) - } - } + func setTitleDate() { + if let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min(by: { $0.row < $1.row && $0.section <= $1.section }), + let metadata = dataSource.getMetadata(indexPath: firstVisibleIndexPath) { + titleDate?.text = utility.getTitleFromDate(metadata.date as Date) } } @@ -154,7 +138,6 @@ extension NCMedia { } self.createMenu() self.collectionView.reloadData() - self.setTitleDate() } let viewOptionsMedia = UIMenu(title: "", options: .displayInline, children: [ @@ -170,11 +153,11 @@ extension NCMedia { ]) let zoomOut = UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: utility.loadImage(named: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in - let lastExt = NCGlobal.shared.getSizeExtension(width: self.collectionView.frame.size.width / CGFloat(self.numberOfColumns)) + let lastExt = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) UIView.animate(withDuration: 0.0, animations: { self.numberOfColumns += 1 - let ext = NCGlobal.shared.getSizeExtension(width: self.collectionView.frame.size.width / CGFloat(self.numberOfColumns)) + let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", columnPhoto: self.numberOfColumns) @@ -184,16 +167,15 @@ extension NCMedia { self.createMenu() self.collectionView.reloadData() - self.setTitleDate() }) } let zoomIn = UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: utility.loadImage(named: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in - let lastExt = NCGlobal.shared.getSizeExtension(width: self.collectionView.frame.size.width / CGFloat(self.numberOfColumns)) + let lastExt = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) UIView.animate(withDuration: 0.0, animations: { self.numberOfColumns -= 1 - let ext = NCGlobal.shared.getSizeExtension(width: self.collectionView.frame.size.width / CGFloat(self.numberOfColumns)) + let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", columnPhoto: self.numberOfColumns) @@ -203,7 +185,6 @@ extension NCMedia { self.createMenu() self.collectionView.reloadData() - self.setTitleDate() }) } diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 2c5a77ae33..e4721fe940 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -50,7 +50,6 @@ class NCMedia: UIViewController { var dataSource = NCMediaDataSource() var serverUrl = "" let refreshControl = UIRefreshControl() - let taskDescriptionRetrievesProperties = "retrievesProperties" var isTop: Bool = true var isEditMode = false var fileSelect: [String] = [] @@ -61,7 +60,6 @@ class NCMedia: UIViewController { let gradient: CAGradientLayer = CAGradientLayer() var showOnlyImages = false var showOnlyVideos = false - var lastContentOffsetY: CGFloat = 0 var timeIntervalSearchNewMedia: TimeInterval = 2.0 var timerSearchNewMedia: Timer? let insetsTop: CGFloat = 75 @@ -201,9 +199,7 @@ class NCMedia: UIViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: nil) { _ in - self.setTitleDate() - } + coordinator.animate(alongsideTransition: nil) { _ in } } override var preferredStatusBarStyle: UIStatusBarStyle { @@ -236,17 +232,17 @@ class NCMedia: UIViewController { timerSearchNewMedia?.invalidate() timerSearchNewMedia = nil filesExists.removeAll() + fileDeleted.removeAll() + NCNetworking.shared.fileExistsQueue.cancelAll() NCNetworking.shared.downloadThumbnailQueue.cancelAll() - if let nkSession = NextcloudKit.shared.nkCommonInstance.getSession(account: session.account) { - nkSession.sessionData.session.getTasksWithCompletionHandler { dataTasks, _, _ in - dataTasks.forEach { - if $0.taskDescription == self.taskDescriptionRetrievesProperties { - $0.cancel() - } - } + Task { + let tasks = await NCNetworking.shared.getAllDataTask() + for task in tasks.filter({ $0.description == NCGlobal.shared.taskDescriptionRetrievesProperties }) { + task.cancel() } + } } @@ -319,13 +315,10 @@ class NCMedia: UIViewController { // MARK: - Image - func getImage(metadata: NCMediaDataSource.Metadata, width: CGFloat? = nil, cost: Int) -> UIImage? { + func getImage(metadata: NCMediaDataSource.Metadata, cost: Int, forceExt: String? = nil) -> UIImage? { var returnImage: UIImage? - var width = width - if width == nil { - width = self.collectionView.frame.size.width / CGFloat(self.numberOfColumns) - } - let ext = NCGlobal.shared.getSizeExtension(width: width) + var ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) + if let forceExt { ext = forceExt } if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { returnImage = image @@ -366,11 +359,8 @@ extension NCMedia: UIScrollViewDelegate { if !dataSource.isEmpty() { isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25) setColor() + setTitleDate() setNeedsStatusBarAppearanceUpdate() - if lastContentOffsetY == 0 || lastContentOffsetY / 2 <= scrollView.contentOffset.y || lastContentOffsetY / 2 >= scrollView.contentOffset.y { - setTitleDate() - lastContentOffsetY = scrollView.contentOffset.y - } } else { setColor() } diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index 7b0042e397..35816c429e 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -28,7 +28,7 @@ import RealmSwift extension NCMedia { func loadDataSource() { DispatchQueue.global().async { - if let metadatas = self.database.getResultsMetadatas(predicate: self.getPredicate(filterLivePhotoFile: true), sortedByKeyPath: "date") { + if let metadatas = self.database.getResultsMetadatas(predicate: self.imageCache.getMediaPredicate(filterLivePhotoFile: true, session: self.session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos), sortedByKeyPath: "date") { self.dataSource = NCMediaDataSource(metadatas: metadatas) } self.collectionViewReloadData() @@ -60,7 +60,7 @@ extension NCMedia { var lessDate = Date.distantFuture var greaterDate = Date.distantPast let countMetadatas = self.dataSource.getMetadatas().count - let options = NKRequestOptions(timeout: 120, taskDescription: self.taskDescriptionRetrievesProperties, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) + let options = NKRequestOptions(timeout: 120, taskDescription: NCGlobal.shared.taskDescriptionRetrievesProperties, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) var firstCellDate: Date? var lastCellDate: Date? @@ -108,6 +108,14 @@ extension NCMedia { if error == .success, let files, self.session.account == account { + /// Removes all files in `files` that have an `ocId` present in `fileDeleted` + var files = files + files.removeAll { file in + self.fileDeleted.contains(file.ocId) + } + self.fileDeleted.removeAll() + + /// No files, remove all if lessDate == Date.distantFuture, greaterDate == Date.distantPast, files.isEmpty { self.dataSource.removeAll() self.collectionViewReloadData() @@ -118,7 +126,7 @@ extension NCMedia { self.database.addMetadatas(metadatas) if let firstCellDate, let lastCellDate, self.isViewActived { - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.getPredicate(filterLivePhotoFile: false)]) + let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.imageCache.getMediaPredicate(filterLivePhotoFile: false, session: self.session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos)]) if let resultsMetadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: predicate) { for metadata in resultsMetadatas where !self.filesExists.contains(metadata.ocId) { @@ -147,27 +155,6 @@ extension NCMedia { } } } - - func getPredicate(filterLivePhotoFile: Bool) -> NSPredicate { - guard let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { return NSPredicate() } - let startServerUrl = NCUtilityFileSystem().getHomeServer(session: session) + tableAccount.mediaPath - - var showBothPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND hasPreview == true AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload')" - var showOnlyPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND hasPreview == true AND classFile == %@ AND NOT (session CONTAINS[c] 'upload')" - - if filterLivePhotoFile { - showBothPredicateMediaString = showBothPredicateMediaString + " AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" - showOnlyPredicateMediaString = showOnlyPredicateMediaString + " AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" - } - - if showOnlyImages { - return NSPredicate(format: showOnlyPredicateMediaString, session.account, startServerUrl, NKCommon.TypeClassFile.image.rawValue) - } else if showOnlyVideos { - return NSPredicate(format: showOnlyPredicateMediaString, session.account, startServerUrl, NKCommon.TypeClassFile.video.rawValue) - } else { - return NSPredicate(format: showBothPredicateMediaString, session.account, startServerUrl) - } - } } // MARK: - diff --git a/iOSClient/Media/NCMediaDownloadThumbnail.swift b/iOSClient/Media/NCMediaDownloadThumbnail.swift index 9e17bbcc10..09751cfac5 100644 --- a/iOSClient/Media/NCMediaDownloadThumbnail.swift +++ b/iOSClient/Media/NCMediaDownloadThumbnail.swift @@ -25,7 +25,7 @@ import UIKit import NextcloudKit import Queuer -class NCMediaDownloadThumbnail: ConcurrentOperation { +class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { var metadata: NCMediaDataSource.Metadata var collectionView: UICollectionView? let utilityFileSystem = NCUtilityFileSystem() @@ -64,10 +64,10 @@ class NCMediaDownloadThumbnail: ConcurrentOperation { if NCImageCache.shared.cache.count < NCImageCache.shared.countLimit, self.cost < NCImageCache.shared.countLimit { - NCImageCache.shared.addImageCache(ocId: self.metadata.ocId, etag: self.metadata.etag, data: data, ext: NCGlobal.shared.getSizeExtension(width: self.width), cost: self.cost) + NCImageCache.shared.addImageCache(ocId: self.metadata.ocId, etag: self.metadata.etag, data: data, ext: NCGlobal.shared.getSizeExtension(column: self.media?.numberOfColumns ?? 3), cost: self.cost) } - let image = self.media?.getImage(metadata: self.metadata, width: self.width, cost: self.cost) + let image = self.media?.getImage(metadata: self.metadata, cost: self.cost) DispatchQueue.main.async { for case let cell as NCGridMediaCell in collectionView.visibleCells { diff --git a/iOSClient/Menu/NCOperationSaveLivePhoto.swift b/iOSClient/Menu/NCOperationSaveLivePhoto.swift index e8ef4a134d..158d74099b 100644 --- a/iOSClient/Menu/NCOperationSaveLivePhoto.swift +++ b/iOSClient/Menu/NCOperationSaveLivePhoto.swift @@ -25,7 +25,7 @@ import UIKit import Queuer import NextcloudKit -class NCOperationSaveLivePhoto: ConcurrentOperation { +class NCOperationSaveLivePhoto: ConcurrentOperation, @unchecked Sendable { var metadata: tableMetadata var metadataMOV: tableMetadata let hud: NCHud? diff --git a/iOSClient/Menu/UIViewController+Menu.swift b/iOSClient/Menu/UIViewController+Menu.swift index d6e5436b0c..7e91db9aff 100644 --- a/iOSClient/Menu/UIViewController+Menu.swift +++ b/iOSClient/Menu/UIViewController+Menu.swift @@ -121,7 +121,7 @@ extension UIViewController { } } -extension UIViewController: MFMailComposeViewControllerDelegate { +extension UIViewController: @retroactive MFMailComposeViewControllerDelegate { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true) } diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index dc08b76f0e..3e26f35b8d 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -98,8 +98,8 @@ class NCGlobal: NSObject { let previewExt128 = ".128.preview.jpg" let previewExt64 = ".64.preview.jpg" - func getSizeExtension(width: CGFloat?) -> String { - guard let width else { return previewExt512 } + func getSizeExtension(column: Int) -> String { + let width = UIScreen.main.bounds.width / CGFloat(column) switch (width * 4) { case 0...119: @@ -415,4 +415,11 @@ class NCGlobal: NSObject { // GROUP AMIN // let groupAdmin = "admin" + + // DATA TASK DESCRIPTION + // + let taskDescriptionRetrievesProperties = "retrievesProperties" + let taskDescriptionSynchronization = "synchronization" + let taskDescriptionDeleteFileOrFolder = "deleteFileOrFolder" + } diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 8afc4a36bc..1c9f7936be 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -92,6 +92,46 @@ class NCImageCache: NSObject { cache.removeAllValues() } + // MARK: - MEDIA - + + func createMediaCache(session: NCSession.Session) { + var cost: Int = 0 + guard let layout = NCManageDatabase.shared.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "") else { return } + let ext = NCGlobal.shared.getSizeExtension(column: layout.columnPhoto) + + if let metadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: getMediaPredicate(filterLivePhotoFile: true, session: session, showOnlyImages: false, showOnlyVideos: false), sortedByKeyPath: "date") { + + for metadata in metadatas { + if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext, cost: cost) + cost += 1 + if cost == countLimit { break } + } + } + } + } + + func getMediaPredicate(filterLivePhotoFile: Bool, session: NCSession.Session, showOnlyImages: Bool, showOnlyVideos: Bool) -> NSPredicate { + guard let tableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { return NSPredicate() } + let startServerUrl = NCUtilityFileSystem().getHomeServer(session: session) + tableAccount.mediaPath + + var showBothPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND hasPreview == true AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload')" + var showOnlyPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND hasPreview == true AND classFile == %@ AND NOT (session CONTAINS[c] 'upload')" + + if filterLivePhotoFile { + showBothPredicateMediaString = showBothPredicateMediaString + " AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" + showOnlyPredicateMediaString = showOnlyPredicateMediaString + " AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" + } + + if showOnlyImages { + return NSPredicate(format: showOnlyPredicateMediaString, session.account, startServerUrl, NKCommon.TypeClassFile.image.rawValue) + } else if showOnlyVideos { + return NSPredicate(format: showOnlyPredicateMediaString, session.account, startServerUrl, NKCommon.TypeClassFile.video.rawValue) + } else { + return NSPredicate(format: showBothPredicateMediaString, session.account, startServerUrl) + } + } + // MARK: - func getImageFile() -> UIImage { diff --git a/iOSClient/Networking/NCNetworking+Download.swift b/iOSClient/Networking/NCNetworking+Download.swift index 1493ce1579..acfffa520d 100644 --- a/iOSClient/Networking/NCNetworking+Download.swift +++ b/iOSClient/Networking/NCNetworking+Download.swift @@ -299,7 +299,7 @@ extension NCNetworking { #endif } -class NCOperationDownload: ConcurrentOperation { +class NCOperationDownload: ConcurrentOperation, @unchecked Sendable { var metadata: tableMetadata var selector: String diff --git a/iOSClient/Networking/NCNetworking+Synchronization.swift b/iOSClient/Networking/NCNetworking+Synchronization.swift index 136a7c3174..c67c4e20a1 100644 --- a/iOSClient/Networking/NCNetworking+Synchronization.swift +++ b/iOSClient/Networking/NCNetworking+Synchronization.swift @@ -31,7 +31,7 @@ extension NCNetworking { add: Bool, completion: @escaping (_ errorCode: Int, _ num: Int) -> Void = { _, _ in }) { let startDate = Date() - let options = NKRequestOptions(timeout: 120, taskDescription: "synchronization", queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) + let options = NKRequestOptions(timeout: 120, taskDescription: NCGlobal.shared.taskDescriptionSynchronization, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrl, depth: "infinity", @@ -94,18 +94,4 @@ extension NCNetworking { return false } } - - func hasSynchronizationTask() async -> Bool { - guard let nkSessions = NextcloudKit.shared.nkCommonInstance.nksessions.getArray() else { return false } - - for nkSession in nkSessions { - let tasks = await nkSession.sessionData.session.tasks - for task in tasks.0 { - if task.taskDescription == "synchronization" { - return true - } - } - } - return false - } } diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 04db5ca762..efdd91c802 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -241,7 +241,22 @@ extension NCNetworking { } } - // MARK: - Zombie + // MARK: - + + func getAllDataTask() async -> [URLSessionDataTask] { + guard let nkSessions = NextcloudKit.shared.nkCommonInstance.nksessions.getArray() else { return [] } + var taskArray: [URLSessionDataTask] = [] + + for nkSession in nkSessions { + let tasks = await nkSession.sessionData.session.tasks + for task in tasks.0 { + taskArray.append(task) + } + } + return taskArray + } + + // MARK: - func verifyZombie() async { var metadatas: [tableMetadata] = [] diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 9635154ada..75ec80fc00 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -432,7 +432,7 @@ extension NCNetworking { return NKError(errorCode: self.global.errorInternalError, errorDescription: "_no_permission_delete_file_") } let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName - let options = NKRequestOptions(customHeader: customHeader) + let options = NKRequestOptions(customHeader: customHeader, taskDescription: NCGlobal.shared.taskDescriptionDeleteFileOrFolder) let result = await deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: metadata.account, options: options) if result.error == .success || result.error.errorCode == self.global.errorResourceNotFound { @@ -926,7 +926,7 @@ extension NCNetworking { } } -class NCOperationDownloadAvatar: ConcurrentOperation { +class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { var user: String var fileName: String var etag: String? @@ -981,7 +981,7 @@ class NCOperationDownloadAvatar: ConcurrentOperation { } } -class NCOperationFileExists: ConcurrentOperation { +class NCOperationFileExists: ConcurrentOperation, @unchecked Sendable { var serverUrlFileName: String var account: String var ocId: String diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 5da9e65d86..fdd3a7d095 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -83,9 +83,10 @@ class NCNetworkingProcess { self.hasRun = true Task { - let hasSynchronizationTask = await NCNetworking.shared.hasSynchronizationTask() - print("[DEBUG] \(hasSynchronizationTask)") + let tasks = await NCNetworking.shared.getAllDataTask() + let hasSynchronizationTask = tasks.contains { $0.description == NCGlobal.shared.taskDescriptionSynchronization } let resultsTransfer = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", self.global.metadataStatusInTransfer)) + if resultsTransfer == nil && !hasSynchronizationTask { // No tranfer, disable } else { diff --git a/iOSClient/RichWorkspace/NCViewerRichWorkspaceWebView.swift b/iOSClient/RichWorkspace/NCViewerRichWorkspaceWebView.swift index 6775ad61d6..844a4a8878 100644 --- a/iOSClient/RichWorkspace/NCViewerRichWorkspaceWebView.swift +++ b/iOSClient/RichWorkspace/NCViewerRichWorkspaceWebView.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit class NCViewerRichWorkspaceWebView: UIViewController, WKNavigationDelegate, WKScriptMessageHandler { diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index 780c29f2f5..676a81f755 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -65,6 +65,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.makeKeyAndVisible() /// Set the ACCOUNT controller.account = activeTableAccount.account + /// Create media cache + DispatchQueue.global(qos: .utility).async { + if NCImageCache.shared.cache.count == 0 { + let session = NCSession.shared.getSession(account: activeTableAccount.account) + NCImageCache.shared.createMediaCache(session: session) + } + } } } else { NCKeychain().removeAll() diff --git a/iOSClient/Settings/Helpers/NCWebBrowserView.swift b/iOSClient/Settings/Helpers/NCWebBrowserView.swift index 835241b76c..de751f6cf7 100644 --- a/iOSClient/Settings/Helpers/NCWebBrowserView.swift +++ b/iOSClient/Settings/Helpers/NCWebBrowserView.swift @@ -22,7 +22,7 @@ // import SwiftUI -import WebKit +@preconcurrency import WebKit /// Returns a WebView preferably for Sheets in SwiftUI, using a UIViewRepresentable struct with WebKit library /// diff --git a/iOSClient/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index 57587460d6..b3fd91305b 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -85,7 +85,7 @@ extension NCTrash { } } -class NCOperationDownloadThumbnailTrash: ConcurrentOperation { +class NCOperationDownloadThumbnailTrash: ConcurrentOperation, @unchecked Sendable { var trash: tableTrash var fileId: String var collectionView: UICollectionView? diff --git a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift b/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift index 6d3fe1b438..3f71f5d871 100644 --- a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift +++ b/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit class NCViewerNextcloudText: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate { var webView = WKWebView() diff --git a/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift b/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift index 12c2daf782..f709bf6627 100644 --- a/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift +++ b/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit import NextcloudKit class NCViewerRichDocument: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, NCSelectDelegate { From 09d2094787472676130d42a0336fe58eaa7b1535 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 15:50:23 +0200 Subject: [PATCH 07/31] Improvements (#3067) * fix Signed-off-by: Marino Faggiana * fix device error offline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- .../Collection Common/NCCollectionViewCommon.swift | 1 - iOSClient/Media/NCMedia+Command.swift | 14 +++++++++++--- iOSClient/Utility/NCContentPresenter.swift | 2 +- iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift | 1 + iOSClient/Viewer/NCViewerProviderContextMenu.swift | 3 +++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 8e5d641a4d..09ed060579 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -184,7 +184,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // Refresh Control collectionView.refreshControl = refreshControl refreshControl.action(for: .valueChanged) { _ in - self.dataSource.removeAll() self.database.cleanEtagDirectory(serverUrl: self.serverUrl, account: self.session.account) self.reloadDataSourceNetwork() } diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 4997a18c65..5d8c7084fa 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -65,10 +65,17 @@ extension NCMedia { } func setTitleDate() { - if let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min(by: { $0.row < $1.row && $0.section <= $1.section }), - let metadata = dataSource.getMetadata(indexPath: firstVisibleIndexPath) { - titleDate?.text = utility.getTitleFromDate(metadata.date as Date) + + if let layoutAttributes = collectionView.collectionViewLayout.layoutAttributesForElements(in: collectionView.bounds) { + let sortedAttributes = layoutAttributes.sorted { $0.frame.minY < $1.frame.minY || ($0.frame.minY == $1.frame.minY && $0.frame.minX < $1.frame.minX) } + + if let firstAttribute = sortedAttributes.first, let metadata = dataSource.getMetadata(indexPath: firstAttribute.indexPath) { + titleDate?.text = utility.getTitleFromDate(metadata.date as Date) + return + } } + + titleDate?.text = "" } func setColor() { @@ -148,6 +155,7 @@ extension NCMedia { viewController.delegate = self viewController.typeOfCommandView = .select viewController.type = "mediaFolder" + viewController.session = self.session self.present(navigationController, animated: true) }) ]) diff --git a/iOSClient/Utility/NCContentPresenter.swift b/iOSClient/Utility/NCContentPresenter.swift index 8d944efc57..cf2085e0a0 100644 --- a/iOSClient/Utility/NCContentPresenter.swift +++ b/iOSClient/Utility/NCContentPresenter.swift @@ -108,7 +108,7 @@ class NCContentPresenter: NSObject { switch error.errorCode { case Int(CFNetworkErrors.cfurlErrorNotConnectedToInternet.rawValue): let image = UIImage(named: "InfoNetwork")?.image(color: .white, size: 20) - self.noteTop(text: NSLocalizedString(title, comment: ""), image: image, color: .lightGray, delay: delay, priority: .max) + self.noteTop(text: NSLocalizedString("_network_not_available_", comment: ""), image: image, color: .lightGray, delay: delay, priority: .max) default: var responseMessage = "" if let data = error.responseData { diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift index 0af121f334..26c17eb797 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift @@ -255,6 +255,7 @@ class NCViewerMedia: UIViewController { let fileNameExtension = (metadata.fileNameView as NSString).pathExtension.uppercased() if metadata.isLivePhoto, + NCNetworking.shared.isOnline, let metadata = self.database.getMetadataLivePhoto(metadata: metadata), !utilityFileSystem.fileProviderStorageExists(metadata), let metadata = self.database.setMetadatasSessionInWaitDownload(metadatas: [metadata], session: NCNetworking.shared.sessionDownload, selector: "") { diff --git a/iOSClient/Viewer/NCViewerProviderContextMenu.swift b/iOSClient/Viewer/NCViewerProviderContextMenu.swift index 2f61b51f94..cbe7d2e4a0 100644 --- a/iOSClient/Viewer/NCViewerProviderContextMenu.swift +++ b/iOSClient/Viewer/NCViewerProviderContextMenu.swift @@ -97,18 +97,21 @@ class NCViewerProviderContextMenu: UIViewController { } // AUTO DOWNLOAD IMAGE GIF if !utilityFileSystem.fileProviderStorageExists(metadata), + NCNetworking.shared.isOnline, metadata.contentType == "image/gif", NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadata, selector: "")) } // AUTO DOWNLOAD IMAGE SVG if !utilityFileSystem.fileProviderStorageExists(metadata), + NCNetworking.shared.isOnline, metadata.contentType == "image/svg+xml", NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadata, selector: "")) } // AUTO DOWNLOAD LIVE PHOTO if let metadataLivePhoto = self.metadataLivePhoto, + NCNetworking.shared.isOnline, !utilityFileSystem.fileProviderStorageExists(metadataLivePhoto), NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadataLivePhoto, selector: "")) From f670e3cb827e31c6d4a82494fd062fb8cba08596 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 16:04:14 +0200 Subject: [PATCH 08/31] remove 64 Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMedia.swift | 10 ++++++++-- iOSClient/NCGlobal.swift | 8 ++------ iOSClient/NCImageCache.swift | 5 ++--- iOSClient/Utility/NCUtility+Image.swift | 6 +++--- iOSClient/Utility/NCUtilityFileSystem.swift | 3 +-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index e4721fe940..071ec5f9e8 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -70,7 +70,11 @@ class NCMedia: UIViewController { var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 +#if DEBUG + let maxColumns: Int = 10 +#else let maxColumns: Int = 7 +#endif var transitionColumns = false var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 @@ -140,8 +144,10 @@ class NCMedia: UIViewController { self.loadDataSource() } - // let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) - // collectionView.addGestureRecognizer(pinchGesture) +#if DEBUG + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) + collectionView.addGestureRecognizer(pinchGesture) +#endif NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in self.layoutType = self.database.getLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "")?.layout ?? NCGlobal.shared.mediaLayoutRatio diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 3e26f35b8d..e4e8a6ec93 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -90,25 +90,21 @@ class NCGlobal: NSObject { let size512: CGSize = CGSize(width: 512, height: 512) let size256: CGSize = CGSize(width: 256, height: 256) let size128: CGSize = CGSize(width: 128, height: 128) - let size64: CGSize = CGSize(width: 64, height: 64) // Image extension let previewExt1024 = ".1024.preview.jpg" let previewExt512 = ".512.preview.jpg" let previewExt256 = ".256.preview.jpg" let previewExt128 = ".128.preview.jpg" - let previewExt64 = ".64.preview.jpg" func getSizeExtension(column: Int) -> String { let width = UIScreen.main.bounds.width / CGFloat(column) switch (width * 4) { - case 0...119: - return previewExt64 - case 120...192: + case 0...192: return previewExt128 case 193...384: return previewExt256 - case 384...768: + case 385...768: return previewExt512 default: return previewExt1024 diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 1c9f7936be..06135518f8 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -33,7 +33,7 @@ class NCImageCache: NSObject { private let utility = NCUtility() private let global = NCGlobal.shared - private let allowExtensions = [NCGlobal.shared.previewExt256, NCGlobal.shared.previewExt128, NCGlobal.shared.previewExt64] + private let allowExtensions = [NCGlobal.shared.previewExt256, NCGlobal.shared.previewExt128] private var brandElementColor: UIColor? public var countLimit = 5_000 @@ -80,8 +80,7 @@ class NCImageCache: NSObject { let exts = [global.previewExt1024, global.previewExt512, global.previewExt256, - global.previewExt128, - global.previewExt64] + global.previewExt128] for i in 0.. Date: Fri, 20 Sep 2024 22:04:40 +0200 Subject: [PATCH 09/31] some improvement Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMedia+CollectionViewDataSource.swift | 2 +- iOSClient/Media/NCMedia.swift | 7 ++++++- iOSClient/Media/NCMediaDataSource.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 0f2f62ef9f..74a9cde1ae 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -116,7 +116,7 @@ extension NCMedia: UICollectionViewDataSource { } if cell.imageItem.image == nil { - if self.transitionColumns { + if isPinchGestureActive || ext == NCGlobal.shared.previewExt512 || ext == NCGlobal.shared.previewExt1024 { cell.imageItem.image = getImage(metadata: metadata, cost: cost) } else { DispatchQueue.global(qos: .userInteractive).async { diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 071ec5f9e8..8804d3659c 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -67,6 +67,7 @@ class NCMedia: UIViewController { let playImage = NCUtility().loadImage(named: "play.fill", colors: [.white]) var photoImage = UIImage() var videoImage = UIImage() + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 @@ -93,6 +94,10 @@ class NCMedia: UIViewController { return self.isViewLoaded && self.view.window != nil } + var isPinchGestureActive: Bool { + return pinchGesture.state == .began || pinchGesture.state == .changed + } + // MARK: - View Life Cycle override func viewDidLoad() { @@ -145,7 +150,7 @@ class NCMedia: UIViewController { } #if DEBUG - let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) + pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) collectionView.addGestureRecognizer(pinchGesture) #endif diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index 35816c429e..805688022c 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -49,7 +49,7 @@ extension NCMedia { self.lockQueue.sync { guard self.isViewActived, !self.hasRunSearchMedia, - !self.transitionColumns, + !self.isPinchGestureActive, !isEditMode, NCNetworking.shared.downloadThumbnailQueue.operationCount == 0, let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) From 5546804f366f0dece406fd1e89fc3dce61a429d4 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 20 Sep 2024 22:11:34 +0200 Subject: [PATCH 10/31] some improvement Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMediaPinchGesture.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/iOSClient/Media/NCMediaPinchGesture.swift b/iOSClient/Media/NCMediaPinchGesture.swift index b8363e4f8a..5f958bb14e 100644 --- a/iOSClient/Media/NCMediaPinchGesture.swift +++ b/iOSClient/Media/NCMediaPinchGesture.swift @@ -81,7 +81,6 @@ extension NCMedia { case .ended: currentScale = 1.0 collectionView.transform = .identity - self.collectionViewReloadData() default: break } From 12e021d79e1fac4ce9a72cc86bb33501c50695f9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 22 Sep 2024 12:22:34 +0200 Subject: [PATCH 11/31] Fix x code16 i os18 (#3068) Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 +- .../File Provider Extension.xcscheme | 10 ++- .../Data/NCManageDatabase+Metadata.swift | 28 ++++++-- iOSClient/Data/NCManageDatabase.swift | 3 +- .../Upload Assets/NCUploadAssetsModel.swift | 16 ++--- .../Upload Assets/NCUploadAssetsView.swift | 67 +++++++++---------- iOSClient/Main/NCPickerViewController.swift | 9 +-- .../NCMedia+CollectionViewDataSource.swift | 14 +++- ...+CollectionViewDataSourcePrefetching.swift | 2 + .../NCMedia+CollectionViewDelegate.swift | 3 +- iOSClient/Media/NCMedia.swift | 29 +------- iOSClient/Media/NCMediaDataSource.swift | 28 +++----- .../Media/NCMediaDownloadThumbnail.swift | 39 +++-------- iOSClient/Media/NCMediaPinchGesture.swift | 1 + iOSClient/NCGlobal.swift | 18 +++-- iOSClient/NCImageCache.swift | 36 +++++----- .../Networking/NCNetworkingProcess.swift | 2 +- iOSClient/Utility/NCUtility+Image.swift | 3 - 18 files changed, 143 insertions(+), 169 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f15fba53d4..dcecf7e4e1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5715,7 +5715,7 @@ repositoryURL = "https://github.com/realm/realm-swift"; requirement = { kind = exactVersion; - version = 10.53.1; + version = 10.54.0; }; }; F72CD01027A7E92400E59476 /* XCRemoteSwiftPackageReference "JGProgressHUD" */ = { @@ -5819,7 +5819,7 @@ repositoryURL = "https://github.com/krzyzanowskim/OpenSSL"; requirement = { kind = exactVersion; - version = 3.3.1000; + version = 3.3.2000; }; }; F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */ = { diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme index 63a60a11ef..37b52330d0 100755 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme @@ -120,8 +120,12 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" launchAutomaticallySubstyle = "2"> - + + + - + [tableMetadata] { + func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String, ascending: Bool, arraySlice: Int) -> [tableMetadata] { do { let realm = try Realm() let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending).prefix(arraySlice) @@ -1059,26 +1059,44 @@ extension NCManageDatabase { } func getResultMetadataFromFileId(_ fileId: String?) -> tableMetadata? { + guard let fileId else { return nil } + do { let realm = try Realm() - guard let fileId = fileId else { return nil } - guard let result = realm.objects(tableMetadata.self).filter("fileId == %@", fileId).first else { return nil } - return result + return realm.objects(tableMetadata.self).filter("fileId == %@", fileId).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String? = nil, ascending: Bool = false) -> Results? { + func getResultMetadataFromOcId(_ ocId: String?) -> tableMetadata? { + guard let ocId else { return nil } + + do { + let realm = try Realm() + return realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + } + return nil + } + + func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String? = nil, ascending: Bool = false, freeze: Bool = false) -> Results? { do { let realm = try Realm() realm.refresh() if let sortedByKeyPath { let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending) + if freeze { + return results.freeze() + } return results } else { let results = realm.objects(tableMetadata.self).filter(predicate) + if freeze { + return results.freeze() + } return results } } catch let error as NSError { diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index a273692e5f..6683318c5f 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -98,7 +98,8 @@ class NCManageDatabase: NSObject { if isAppex { if bundleFileName == "File Provider Extension.appex" { - objectTypesAppex = [tableMetadata.self, + objectTypesAppex = [NCKeyValue.self, + tableMetadata.self, tableLocalFile.self, tableDirectory.self, tableTag.self, diff --git a/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsModel.swift b/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsModel.swift index 9c4ea4515f..3a714bdb15 100644 --- a/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsModel.swift +++ b/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsModel.swift @@ -39,7 +39,7 @@ struct PreviewStore { var image: UIImage } -class NCUploadAssetsModel: NSObject, ObservableObject, NCCreateFormUploadConflictDelegate { +class NCUploadAssetsModel: ObservableObject, NCCreateFormUploadConflictDelegate { @Published var serverUrl: String @Published var assets: [TLPHAsset] @Published var previewStore: [PreviewStore] = [] @@ -65,13 +65,14 @@ class NCUploadAssetsModel: NSObject, ObservableObject, NCCreateFormUploadConflic self.serverUrl = serverUrl self.controller = controller self.showHUD = true - super.init() DispatchQueue.global(qos: .userInteractive).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 } - self.previewStore.append(PreviewStore(id: localIdentifier, asset: asset, assetType: asset.type, fileName: "", image: image)) + DispatchQueue.main.async { + self.previewStore.append(PreviewStore(id: localIdentifier, asset: asset, assetType: asset.type, fileName: "", image: image)) + } } DispatchQueue.main.async { self.showHUD = false @@ -208,12 +209,9 @@ class NCUploadAssetsModel: NSObject, ObservableObject, NCCreateFormUploadConflic // Check if is in upload if let results = database.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@ AND session != ''", - session.account, - serverUrl, - fileName), - sortedByKeyPath: "fileName", - ascending: false), - !results.isEmpty { + session.account, + serverUrl, + fileName), sortedByKeyPath: "fileName", ascending: false), !results.isEmpty { continue } diff --git a/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsView.swift b/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsView.swift index 9e11e4a754..d1bd806afa 100644 --- a/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsView.swift +++ b/iOSClient/Main/Create cloud/Upload Assets/NCUploadAssetsView.swift @@ -11,6 +11,7 @@ import NextcloudKit struct NCUploadAssetsView: View { @ObservedObject var model: NCUploadAssetsModel + @State private var showSelect = false @State private var showUploadConflict = false @State private var showQuickLook = false @@ -23,12 +24,11 @@ struct NCUploadAssetsView: View { var metadata: tableMetadata? let gridItems: [GridItem] = [GridItem()] let fileNamePath = NSTemporaryDirectory() + "Photo.jpg" + let utilityFileSystem = NCUtilityFileSystem() @Environment(\.presentationMode) var presentationMode var body: some View { - let utilityFileSystem = NCUtilityFileSystem() - NavigationView { ZStack(alignment: .top) { List { @@ -160,40 +160,42 @@ struct NCUploadAssetsView: View { } } - Button(NSLocalizedString("_save_", comment: "")) { - if model.useAutoUploadFolder, model.useAutoUploadSubFolder { - model.showHUD = true - } - model.uploadInProgress.toggle() - model.save { metadatasNOConflict, metadatasUploadInConflict in - if metadatasUploadInConflict.isEmpty { - model.dismissCreateFormUploadConflict(metadatas: metadatasNOConflict) - } else { - model.metadatasNOConflict = metadatasNOConflict - model.metadatasUploadInConflict = metadatasUploadInConflict - showUploadConflict = true + Section { + Button(NSLocalizedString("_save_", comment: "")) { + if model.useAutoUploadFolder, model.useAutoUploadSubFolder { + model.showHUD = true + } + model.uploadInProgress.toggle() + model.save { metadatasNOConflict, metadatasUploadInConflict in + if metadatasUploadInConflict.isEmpty { + model.dismissCreateFormUploadConflict(metadatas: metadatasNOConflict) + } else { + model.metadatasNOConflict = metadatasNOConflict + model.metadatasUploadInConflict = metadatasUploadInConflict + showUploadConflict = true + } } } + .frame(maxWidth: .infinity) + .buttonStyle(ButtonRounded(disabled: model.uploadInProgress, account: model.session.account)) + .listRowBackground(Color(UIColor.systemGroupedBackground)) + .disabled(model.uploadInProgress) + .hiddenConditionally(isHidden: model.hiddenSave) } - .frame(maxWidth: .infinity) - .buttonStyle(ButtonRounded(disabled: model.uploadInProgress, account: model.session.account)) - .listRowBackground(Color(UIColor.systemGroupedBackground)) - .disabled(model.uploadInProgress) - .hiddenConditionally(isHidden: model.hiddenSave) } - .navigationTitle(NSLocalizedString("_upload_photos_videos_", comment: "")) - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(systemName: "xmark") - .font(Font.system(.body).weight(.light)) - .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) - }) - NCHUDView(showHUD: $model.showHUD, textLabel: NSLocalizedString("_wait_", comment: ""), image: "doc.badge.arrow.up", color: NCBrandColor.shared.getElement(account: model.session.account)) - .offset(y: model.showHUD ? 5 : -200) - .animation(.easeOut, value: model.showHUD) } + .navigationTitle(NSLocalizedString("_upload_photos_videos_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: Button(action: { + model.dismissView = true + }) { + Image(systemName: "xmark") + .font(Font.system(.body).weight(.light)) + .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) + }) + NCHUDView(showHUD: $model.showHUD, textLabel: NSLocalizedString("_wait_", comment: ""), image: "doc.badge.arrow.up", color: NCBrandColor.shared.getElement(account: model.session.account)) + .offset(y: model.showHUD ? 5 : -200) + .animation(.easeOut, value: model.showHUD) } .navigationViewStyle(StackNavigationViewStyle()) .sheet(isPresented: $showSelect) { @@ -211,9 +213,6 @@ struct NCUploadAssetsView: View { presentationMode.wrappedValue.dismiss() } } - .onTapGesture { - SceneManager.shared.getWindow(controller: model.controller)?.endEditing(true) - } .onDisappear { model.dismissView = true } diff --git a/iOSClient/Main/NCPickerViewController.swift b/iOSClient/Main/NCPickerViewController.swift index 65b2b2ac26..4e9cb89c00 100644 --- a/iOSClient/Main/NCPickerViewController.swift +++ b/iOSClient/Main/NCPickerViewController.swift @@ -45,10 +45,11 @@ class NCPhotosPickerViewController: NSObject { self.openPhotosPickerViewController { assets in if !assets.isEmpty { - let serverUrl = controller.currentServerUrl() - let view = NCUploadAssetsView(model: NCUploadAssetsModel(assets: assets, serverUrl: serverUrl, controller: controller)) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { - controller.present(UIHostingController(rootView: view), animated: true, completion: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let model = NCUploadAssetsModel(assets: assets, serverUrl: controller.currentServerUrl(), controller: controller) + let view = NCUploadAssetsView(model: model) + let viewController = UIHostingController(rootView: view) + controller.present(viewController, animated: true, completion: nil) } } } diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 74a9cde1ae..4bfa056688 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -86,6 +86,15 @@ extension NCMedia: UICollectionViewDataSource { } } + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return } + + if !utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag), + NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnail)?.metadata.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.downloadThumbnailQueue.addOperation(NCMediaDownloadThumbnail(metadata: metadata, media: self)) + } + } + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridMediaCell) else { fatalError("Unable to dequeue NCGridMediaCell with identifier gridCell") @@ -94,7 +103,6 @@ extension NCMedia: UICollectionViewDataSource { let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) let imageCache = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) - let cost = indexPath.row cell.backgroundColor = .secondarySystemBackground cell.imageItem.image = imageCache @@ -117,10 +125,10 @@ extension NCMedia: UICollectionViewDataSource { if cell.imageItem.image == nil { if isPinchGestureActive || ext == NCGlobal.shared.previewExt512 || ext == NCGlobal.shared.previewExt1024 { - cell.imageItem.image = getImage(metadata: metadata, cost: cost) + cell.imageItem.image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) } else { DispatchQueue.global(qos: .userInteractive).async { - let image = self.getImage(metadata: metadata, cost: cost) + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) DispatchQueue.main.async { if let currentCell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, currentCell.ocId == metadata.ocId, let image { diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift b/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift index 8be355a6c6..994bc2496c 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift @@ -26,6 +26,8 @@ import UIKit extension NCMedia: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { + guard !imageCache.isLoadingCache else { return } + let cost = indexPaths.first?.row ?? 0 let metadatas = self.dataSource.getMetadatas(indexPaths: indexPaths) let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index 97e0f15d5a..312ed892a6 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -51,7 +51,8 @@ extension NCMedia: UICollectionViewDelegate { // ACTIVE SERVERURL serverUrl = metadata.serverUrl if let results = dataSource.getTableMetadatas() { - NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: getImage(metadata: metadataDatasource, cost: indexPath.row, forceExt: NCGlobal.shared.previewExt1024)) + let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) + NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: image) } } } diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 8804d3659c..d2b382bea5 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -71,11 +71,7 @@ class NCMedia: UIViewController { var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 -#if DEBUG let maxColumns: Int = 10 -#else - let maxColumns: Int = 7 -#endif var transitionColumns = false var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 @@ -149,10 +145,8 @@ class NCMedia: UIViewController { self.loadDataSource() } -#if DEBUG pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) collectionView.addGestureRecognizer(pinchGesture) -#endif NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in self.layoutType = self.database.getLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "")?.layout ?? NCGlobal.shared.mediaLayoutRatio @@ -250,10 +244,9 @@ class NCMedia: UIViewController { Task { let tasks = await NCNetworking.shared.getAllDataTask() - for task in tasks.filter({ $0.description == NCGlobal.shared.taskDescriptionRetrievesProperties }) { + for task in tasks.filter({ $0.taskDescription == NCGlobal.shared.taskDescriptionRetrievesProperties }) { task.cancel() } - } } @@ -324,26 +317,6 @@ class NCMedia: UIViewController { } } - // MARK: - Image - - func getImage(metadata: NCMediaDataSource.Metadata, cost: Int, forceExt: String? = nil) -> UIImage? { - var returnImage: UIImage? - var ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) - if let forceExt { ext = forceExt } - - if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - returnImage = image - } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - returnImage = image - } else if NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnail)?.metadata.ocId == metadata.ocId }).isEmpty { - DispatchQueue.main.async { - NCNetworking.shared.downloadThumbnailQueue.addOperation(NCMediaDownloadThumbnail(metadata: metadata, collectionView: self.collectionView, media: self, cost: cost)) - } - } - - return returnImage - } - func buildMediaPhotoVideo(columnCount: Int) { var pointSize: CGFloat = 0 diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index 805688022c..b5f04c8f02 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -125,6 +125,10 @@ extension NCMedia { self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in self.database.addMetadatas(metadatas) + if self.dataSource.addMetadatas(metadatas) { + self.collectionViewReloadData() + } + if let firstCellDate, let lastCellDate, self.isViewActived { let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.imageCache.getMediaPredicate(filterLivePhotoFile: false, session: self.session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos)]) @@ -139,10 +143,6 @@ extension NCMedia { } } - if self.dataSource.addFiles(files) { - self.collectionViewReloadData() - } - } else { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media error code \(error.errorCode) " + error.errorDescription) } @@ -197,7 +197,7 @@ public class NCMediaDataSource: NSObject { super.init() self.metadatas.removeAll() - for metadata in metadatas { + metadatas.forEach { metadata in let metadata = getMetadataFromTableMetadata(metadata) self.metadatas.append(metadata) } @@ -234,16 +234,6 @@ public class NCMediaDataSource: NSObject { ocId: metadata.ocId) } - private func getMetadataFromFile(_ file: NKFile) -> Metadata { - return Metadata(date: file.date as Date, - etag: file.etag, - imageSize: CGSize(width: file.width, height: file.height), - isImage: file.classFile == NKCommon.TypeClassFile.image.rawValue, - isLivePhoto: !file.livePhotoFile.isEmpty, - isVideo: file.classFile == NKCommon.TypeClassFile.video.rawValue, - ocId: file.ocId) - } - // MARK: - func removeAll() { @@ -291,15 +281,15 @@ public class NCMediaDataSource: NSObject { } } - func addFiles(_ files: [NKFile]) -> Bool { + func addMetadatas(_ metadatas: [tableMetadata]) -> Bool { var metadatasToInsert: [Metadata] = [] - for file in files { - let metadata = getMetadataFromFile(file) + for tableMetadata in metadatas { + let metadata = getMetadataFromTableMetadata(tableMetadata) if metadata.isLivePhoto, metadata.isVideo { continue } - if let index = self.metadatas.firstIndex(where: { $0.ocId == file.ocId }) { + if let index = self.metadatas.firstIndex(where: { $0.ocId == tableMetadata.ocId }) { self.metadatas[index] = metadata } else { metadatasToInsert.append(metadata) diff --git a/iOSClient/Media/NCMediaDownloadThumbnail.swift b/iOSClient/Media/NCMediaDownloadThumbnail.swift index 09751cfac5..89d45c72ee 100644 --- a/iOSClient/Media/NCMediaDownloadThumbnail.swift +++ b/iOSClient/Media/NCMediaDownloadThumbnail.swift @@ -27,25 +27,17 @@ import Queuer class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { var metadata: NCMediaDataSource.Metadata - var collectionView: UICollectionView? let utilityFileSystem = NCUtilityFileSystem() - let media: NCMedia? - var width: CGFloat? - let cost: Int + let media: NCMedia - init(metadata: NCMediaDataSource.Metadata, collectionView: UICollectionView?, media: NCMedia?, cost: Int) { + init(metadata: NCMediaDataSource.Metadata, media: NCMedia) { self.metadata = metadata - self.collectionView = collectionView self.media = media - self.cost = cost - - if let collectionView, let numberOfColumns = self.media?.numberOfColumns { - width = collectionView.frame.size.width / CGFloat(numberOfColumns) - } } override func start() { - guard !isCancelled, let tableMetadata = NCManageDatabase.shared.getMetadataFromOcId(self.metadata.ocId), let media = self.media else { return self.finish() } + guard !isCancelled, + let tableMetadata = NCManageDatabase.shared.getResultMetadataFromOcId(self.metadata.ocId)?.freeze() else { return self.finish() } var etagResource: String? if utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) { @@ -56,27 +48,18 @@ class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { etag: etagResource, account: media.session.account, options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, data, _, _, etag, error in - if error == .success, let data, let collectionView = self.collectionView { + if error == .success, let data { - self.media?.filesExists.append(self.metadata.ocId) + self.media.filesExists.append(self.metadata.ocId) NCManageDatabase.shared.setMetadataEtagResource(ocId: self.metadata.ocId, etagResource: etag) - NCUtility().createImage(metadata: tableMetadata, data: data, cost: self.cost) - - if NCImageCache.shared.cache.count < NCImageCache.shared.countLimit, - self.cost < NCImageCache.shared.countLimit { - NCImageCache.shared.addImageCache(ocId: self.metadata.ocId, etag: self.metadata.etag, data: data, ext: NCGlobal.shared.getSizeExtension(column: self.media?.numberOfColumns ?? 3), cost: self.cost) - } - - let image = self.media?.getImage(metadata: self.metadata, cost: self.cost) + NCUtility().createImage(metadata: tableMetadata, data: data) + let image = NCUtility().getImage(ocId: self.metadata.ocId, etag: self.metadata.etag, ext: NCGlobal.shared.getSizeExtension(column: self.media.numberOfColumns)) DispatchQueue.main.async { - for case let cell as NCGridMediaCell in collectionView.visibleCells { + for case let cell as NCGridMediaCell in self.media.collectionView.visibleCells { if cell.ocId == self.metadata.ocId { - UIView.transition(with: cell.imageItem, - duration: 0.75, - options: .transitionCrossDissolve, - animations: { cell.imageItem.image = image }, - completion: nil) + UIView.transition(with: cell.imageItem, duration: 0.75, options: .transitionCrossDissolve, animations: { cell.imageItem.image = image + }, completion: nil) break } } diff --git a/iOSClient/Media/NCMediaPinchGesture.swift b/iOSClient/Media/NCMediaPinchGesture.swift index 5f958bb14e..306d57d76e 100644 --- a/iOSClient/Media/NCMediaPinchGesture.swift +++ b/iOSClient/Media/NCMediaPinchGesture.swift @@ -61,6 +61,7 @@ extension NCMedia { case .began: networkRemoveAll() lastScale = gestureRecognizer.scale + lastNumberOfColumns = numberOfColumns case .changed: guard !transitionColumns else { return diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index e4e8a6ec93..c54b040327 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -99,16 +99,14 @@ class NCGlobal: NSObject { func getSizeExtension(column: Int) -> String { let width = UIScreen.main.bounds.width / CGFloat(column) - switch (width * 4) { - case 0...192: - return previewExt128 - case 193...384: - return previewExt256 - case 385...768: - return previewExt512 - default: - return previewExt1024 - } + switch (width * 4) { + case 0...384: + return previewExt256 + case 385...768: + return previewExt512 + default: + return previewExt1024 + } } // E2EE diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 06135518f8..67bc27ba90 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -36,11 +36,13 @@ class NCImageCache: NSObject { private let allowExtensions = [NCGlobal.shared.previewExt256, NCGlobal.shared.previewExt128] private var brandElementColor: UIColor? - public var countLimit = 5_000 + public var countLimit = 10_000 lazy var cache: LRUCache = { return LRUCache(countLimit: countLimit) }() + public var isLoadingCache: Bool = false + override init() { super.init() NotificationCenter.default.addObserver(self, selector: #selector(handleMemoryWarning), name: LRUCacheMemoryWarningNotification, object: nil) @@ -55,19 +57,20 @@ class NCImageCache: NSObject { countLimit = countLimit - 500 if countLimit <= 0 { countLimit = 100 } self.cache = LRUCache(countLimit: countLimit) +#if DEBUG + NCContentPresenter().messageNotification("Cache image memory warning \(countLimit)", error: .success, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, priority: .max) +#endif } func addImageCache(ocId: String, etag: String, data: Data, ext: String, cost: Int) { guard allowExtensions.contains(ext), - cache.count < countLimit, let image = UIImage(data: data) else { return } cache.setValue(image, forKey: ocId + etag + ext, cost: cost) } func addImageCache(ocId: String, etag: String, image: UIImage, ext: String, cost: Int) { - guard allowExtensions.contains(ext), - cache.count < countLimit else { return } + guard allowExtensions.contains(ext) else { return } cache.setValue(image, forKey: ocId + etag + ext, cost: cost) } @@ -77,13 +80,8 @@ class NCImageCache: NSObject { } func removeImageCache(ocIdPlusEtag: String) { - let exts = [global.previewExt1024, - global.previewExt512, - global.previewExt256, - global.previewExt128] - - for i in 0.. Date: Sun, 22 Sep 2024 12:27:46 +0200 Subject: [PATCH 12/31] remove old 128 Signed-off-by: Marino Faggiana --- iOSClient/NCGlobal.swift | 2 -- iOSClient/NCImageCache.swift | 2 +- iOSClient/Utility/NCUtility+Image.swift | 6 +++--- iOSClient/Utility/NCUtilityFileSystem.swift | 3 +-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index c54b040327..877a7812cb 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -89,12 +89,10 @@ class NCGlobal: NSObject { let size1024: CGSize = CGSize(width: 1024, height: 1024) let size512: CGSize = CGSize(width: 512, height: 512) let size256: CGSize = CGSize(width: 256, height: 256) - let size128: CGSize = CGSize(width: 128, height: 128) // Image extension let previewExt1024 = ".1024.preview.jpg" let previewExt512 = ".512.preview.jpg" let previewExt256 = ".256.preview.jpg" - let previewExt128 = ".128.preview.jpg" func getSizeExtension(column: Int) -> String { let width = UIScreen.main.bounds.width / CGFloat(column) diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 67bc27ba90..eea29260c5 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -33,7 +33,7 @@ class NCImageCache: NSObject { private let utility = NCUtility() private let global = NCGlobal.shared - private let allowExtensions = [NCGlobal.shared.previewExt256, NCGlobal.shared.previewExt128] + private let allowExtensions = [NCGlobal.shared.previewExt256] private var brandElementColor: UIColor? public var countLimit = 10_000 diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index ab523bb1af..5e86db5ff2 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -158,9 +158,9 @@ extension NCUtility { } private func createImageStandard(ocId: String, etag: String, classFile: String, image: UIImage, cost: Int) { - let ext = [global.previewExt512, global.previewExt256, global.previewExt128] - let size = [global.size512, global.size256, global.size128] - let compressionQuality = [0.6, 0.7, 0.8] + let ext = [global.previewExt512, global.previewExt256] + let size = [global.size512, global.size256] + let compressionQuality = [0.6, 0.7] for i in 0.. Bool { if fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt1024), fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt512), - fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt256), - fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt128) { + fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt256) { return true } return false From 72762ac933a72dd853991668bc430b0f9ce23f1e Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 22 Sep 2024 12:28:29 +0200 Subject: [PATCH 13/31] build 15 Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index dcecf7e4e1..e119911d42 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5470,7 +5470,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5536,7 +5536,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; From 853a2e932a9554ca830088ad642484525384c3ad Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 22 Sep 2024 12:32:01 +0200 Subject: [PATCH 14/31] remove old menu Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMedia+Command.swift | 129 -------------------------- 1 file changed, 129 deletions(-) diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 5d8c7084fa..95a1d090b6 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -107,135 +107,6 @@ extension NCMedia { let layoutTitle = (layout == NCGlobal.shared.mediaLayoutRatio) ? NSLocalizedString("_media_square_", comment: "") : NSLocalizedString("_media_ratio_", comment: "") let layoutImage = (layout == NCGlobal.shared.mediaLayoutRatio) ? utility.loadImage(named: "square.grid.3x3") : utility.loadImage(named: "rectangle.grid.3x2") - if numberOfColumns >= maxColumns { - self.attributesZoomIn = [] - self.attributesZoomOut = .disabled - } else if numberOfColumns <= 1 { - self.attributesZoomIn = .disabled - self.attributesZoomOut = [] - } else { - self.attributesZoomIn = [] - self.attributesZoomOut = [] - } - - let viewFilterMenu = UIMenu(title: "", options: .displayInline, children: [ - UIAction(title: NSLocalizedString("_media_viewimage_show_", comment: ""), image: utility.loadImage(named: "photo")) { _ in - self.showOnlyImages = true - self.showOnlyVideos = false - self.loadDataSource() - }, - UIAction(title: NSLocalizedString("_media_viewvideo_show_", comment: ""), image: utility.loadImage(named: "video")) { _ in - self.showOnlyImages = false - self.showOnlyVideos = true - self.loadDataSource() - }, - UIAction(title: NSLocalizedString("_media_show_all_", comment: ""), image: utility.loadImage(named: "photo.on.rectangle")) { _ in - self.showOnlyImages = false - self.showOnlyVideos = false - self.loadDataSource() - } - ]) - let viewLayoutMenu = UIAction(title: layoutTitle, image: layoutImage) { _ in - if layout == NCGlobal.shared.mediaLayoutRatio { - NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", layout: NCGlobal.shared.mediaLayoutSquare) - self.layoutType = NCGlobal.shared.mediaLayoutSquare - } else { - NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", layout: NCGlobal.shared.mediaLayoutRatio) - self.layoutType = NCGlobal.shared.mediaLayoutRatio - } - self.createMenu() - self.collectionView.reloadData() - } - - let viewOptionsMedia = UIMenu(title: "", options: .displayInline, children: [ - UIMenu(title: NSLocalizedString("_media_view_options_", comment: ""), children: [viewFilterMenu, viewLayoutMenu]), - UIAction(title: NSLocalizedString("_select_media_folder_", comment: ""), image: utility.loadImage(named: "folder"), handler: { _ in - guard let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController, - let viewController = navigationController.topViewController as? NCSelect else { return } - viewController.delegate = self - viewController.typeOfCommandView = .select - viewController.type = "mediaFolder" - viewController.session = self.session - self.present(navigationController, animated: true) - }) - ]) - - let zoomOut = UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: utility.loadImage(named: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in - let lastExt = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) - - UIView.animate(withDuration: 0.0, animations: { - self.numberOfColumns += 1 - let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) - - NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", columnPhoto: self.numberOfColumns) - - if ext != lastExt { - self.imageCache.removeAll() - } - - self.createMenu() - self.collectionView.reloadData() - }) - } - - let zoomIn = UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: utility.loadImage(named: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in - let lastExt = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) - - UIView.animate(withDuration: 0.0, animations: { - self.numberOfColumns -= 1 - let ext = NCGlobal.shared.getSizeExtension(column: self.numberOfColumns) - - NCManageDatabase.shared.setLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "", columnPhoto: self.numberOfColumns) - - if ext != lastExt { - self.imageCache.removeAll() - } - - self.createMenu() - self.collectionView.reloadData() - }) - } - - let playFile = UIAction(title: NSLocalizedString("_play_from_files_", comment: ""), image: utility.loadImage(named: "play.circle")) { _ in - guard let controller = self.tabBarController as? NCMainTabBarController else { return } - self.documentPickerViewController = NCDocumentPickerViewController(controller: controller, isViewerMedia: true, allowsMultipleSelection: false, viewController: self) - } - - let playURL = UIAction(title: NSLocalizedString("_play_from_url_", comment: ""), image: utility.loadImage(named: "link")) { _ in - let alert = UIAlertController(title: NSLocalizedString("_valid_video_url_", comment: ""), message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: nil)) - alert.addTextField(configurationHandler: { textField in - textField.placeholder = "http://myserver.com/movie.mkv" - }) - alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in - guard let stringUrl = alert.textFields?.first?.text, !stringUrl.isEmpty, let url = URL(string: stringUrl) else { return } - let fileName = url.lastPathComponent - let metadata = self.database.createMetadata(fileName: fileName, - fileNameView: fileName, - ocId: NSUUID().uuidString, - serverUrl: "", - url: stringUrl, - contentType: "", - session: self.session, - sceneIdentifier: self.controller?.sceneIdentifier) - self.database.addMetadata(metadata) - NCViewer().view(viewController: self, metadata: metadata, metadatas: [metadata]) - })) - self.present(alert, animated: true) - } - - menuButton.menu = UIMenu(title: "", children: [zoomOut, zoomIn, viewOptionsMedia, playFile, playURL]) - } - - func createMenuNEW() { - let layoutForView = database.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewMedia, serverUrl: "") - var layout = layoutForView?.layout ?? NCGlobal.shared.mediaLayoutRatio - /// Overwrite default value - if layout == NCGlobal.shared.layoutList { layout = NCGlobal.shared.mediaLayoutRatio } - /// - let layoutTitle = (layout == NCGlobal.shared.mediaLayoutRatio) ? NSLocalizedString("_media_square_", comment: "") : NSLocalizedString("_media_ratio_", comment: "") - let layoutImage = (layout == NCGlobal.shared.mediaLayoutRatio) ? utility.loadImage(named: "square.grid.3x3") : utility.loadImage(named: "rectangle.grid.3x2") - let viewFilterMenu = UIMenu(title: "", options: .displayInline, children: [ UIAction(title: NSLocalizedString("_media_viewimage_show_", comment: ""), image: utility.loadImage(named: "photo")) { _ in self.showOnlyImages = true From a448dfcc2fd9ac87175b9b3be0c15307d3b08222 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 23 Sep 2024 10:16:49 +0200 Subject: [PATCH 15/31] Performance (#3071) * iPad --------- Signed-off-by: Marino Faggiana --- Widget/Files/FilesData.swift | 2 +- iOSClient/Activity/NCActivity.swift | 2 +- ...ionViewCommon+CollectionViewDelegate.swift | 19 ++++-- .../NCCollectionViewDownloadThumbnail.swift | 4 +- .../Main/Create cloud/NCCreateDocument.swift | 4 +- iOSClient/Main/NCActionCenter.swift | 8 +-- iOSClient/Main/NCMainTabBarController.swift | 4 ++ iOSClient/Main/NCPickerViewController.swift | 2 +- .../NCMedia+CollectionViewDelegate.swift | 40 +++++-------- iOSClient/Media/NCMedia+Command.swift | 2 +- iOSClient/Media/NCMedia.swift | 2 +- iOSClient/Media/NCMediaDataSource.swift | 15 ----- iOSClient/NCGlobal.swift | 2 + iOSClient/Recent/NCRecent.swift | 2 +- iOSClient/Trash/NCTrash+CollectionView.swift | 2 +- iOSClient/Trash/NCTrash+Networking.swift | 29 +++++---- iOSClient/Utility/NCUtility+Image.swift | 10 ++-- iOSClient/Viewer/NCViewer.swift | 18 +++--- .../NCViewerMedia/NCViewerMediaPage.swift | 59 ++++++++++--------- 19 files changed, 110 insertions(+), 116 deletions(-) diff --git a/Widget/Files/FilesData.swift b/Widget/Files/FilesData.swift index ca43645376..741be5788d 100644 --- a/Widget/Files/FilesData.swift +++ b/Widget/Files/FilesData.swift @@ -224,7 +224,7 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi account: activeTableAccount.account, options: options) if result.error == .success, let data = result.data { - utility.createImage(ocId: file.ocId, etag: file.etag, classFile: file.classFile, data: data) + utility.createImage(ocId: file.ocId, etag: file.etag, data: data) } } if image == nil { diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 2e446ac16b..8b4eb2e5fe 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -352,7 +352,7 @@ extension NCActivity { var bottom: CGFloat = 0 if let mainTabBar = self.tabBarController?.tabBar as? NCMainTabBar { - bottom = -mainTabBar.getHeight() + bottom = -mainTabBar.getHeight() } NCActivityIndicator.shared.start(backgroundView: self.view, bottom: bottom - 35, style: .medium) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 43520f1019..7da1ccb459 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -63,19 +63,28 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) if !metadata.isDirectoryE2EE, (metadata.isImage || metadata.isAudioOrVideo) { let metadatas = self.dataSource.getResultMetadatas() - let metadatasMedia = metadatas.filter { $0.classFile == NKCommon.TypeClassFile.image.rawValue || $0.classFile == NKCommon.TypeClassFile.video.rawValue || $0.classFile == NKCommon.TypeClassFile.audio.rawValue } - let indexMetadatas = metadatasMedia.firstIndex(where: { $0.ocId == metadata.ocId }) ?? 0 - return NCViewer().view(viewController: self, metadata: metadata, metadatas: metadatasMedia, indexMetadatas: indexMetadatas, image: image) - } else if metadata.isAvailableEditorView || utilityFileSystem.fileProviderStorageExists(metadata) || metadata.name == NCGlobal.shared.talkName { - NCViewer().view(viewController: self, metadata: metadata, metadatas: [metadata], image: image) + let ocIds = metadatas.filter { $0.classFile == NKCommon.TypeClassFile.image.rawValue || + $0.classFile == NKCommon.TypeClassFile.video.rawValue || + $0.classFile == NKCommon.TypeClassFile.audio.rawValue }.map(\.ocId) + + return NCViewer().view(viewController: self, metadata: metadata, ocIds: ocIds, image: image) + + } else if metadata.isAvailableEditorView || + utilityFileSystem.fileProviderStorageExists(metadata) || + metadata.name == NCGlobal.shared.talkName { + + NCViewer().view(viewController: self, metadata: metadata, image: image) + } else if NextcloudKit.shared.isNetworkReachable(), let metadata = database.setMetadatasSessionInWaitDownload(metadatas: [metadata], session: NCNetworking.shared.sessionDownload, selector: global.selectorLoadFileView, sceneIdentifier: self.controller?.sceneIdentifier) { + NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) } else { let error = NKError(errorCode: global.errorOffline, errorDescription: "_go_online_") + NCContentPresenter().showInfo(error: error) } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift index 4ea70ad0b6..9ef11d6c6c 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift @@ -60,8 +60,8 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation, @unchecked Sendabl let image = self.utility.getImage(ocId: self.metadata.ocId, etag: self.metadata.etag, ext: self.ext) DispatchQueue.main.async { - for case let cell as NCCellProtocol in collectionView.visibleCells { - if cell.fileOcId == self.metadata.ocId, let filePreviewImageView = cell.filePreviewImageView { + for case let cell as NCCellProtocol in collectionView.visibleCells where cell.fileOcId == self.metadata.ocId { + if let filePreviewImageView = cell.filePreviewImageView { filePreviewImageView.contentMode = .scaleAspectFill if self.metadata.hasPreviewBorder { filePreviewImageView.layer.borderWidth = 0.2 diff --git a/iOSClient/Main/Create cloud/NCCreateDocument.swift b/iOSClient/Main/Create cloud/NCCreateDocument.swift index 75744f2882..543eb70b39 100644 --- a/iOSClient/Main/Create cloud/NCCreateDocument.swift +++ b/iOSClient/Main/Create cloud/NCCreateDocument.swift @@ -59,7 +59,7 @@ class NCCreateDocument: NSObject { session: session, sceneIdentifier: controller.sceneIdentifier) - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) } } @@ -80,7 +80,7 @@ class NCCreateDocument: NSObject { session: session, sceneIdentifier: controller.sceneIdentifier) - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) } } } diff --git a/iOSClient/Main/NCActionCenter.swift b/iOSClient/Main/NCActionCenter.swift index c036a1783c..6acf00d4be 100644 --- a/iOSClient/Main/NCActionCenter.swift +++ b/iOSClient/Main/NCActionCenter.swift @@ -134,7 +134,7 @@ class NCActionCenter: NSObject, UIDocumentInteractionControllerDelegate, NCSelec } else { if let viewController = controller.currentViewController() { let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata], image: image) + NCViewer().view(viewController: viewController, metadata: metadata, image: image) } } @@ -202,7 +202,7 @@ class NCActionCenter: NSObject, UIDocumentInteractionControllerDelegate, NCSelec let attr = try FileManager.default.attributesOfItem(atPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)) let fileSize = attr[FileAttributeKey.size] as? UInt64 ?? 0 if fileSize > 0 { - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) return } } catch { @@ -230,7 +230,7 @@ class NCActionCenter: NSObject, UIDocumentInteractionControllerDelegate, NCSelec let fileNameLocalPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) if metadata.isAudioOrVideo { - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) } else { hud.show() NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account, requestHandler: { request in @@ -242,7 +242,7 @@ class NCActionCenter: NSObject, UIDocumentInteractionControllerDelegate, NCSelec hud.dismiss() if account == accountDownload && error == .success { self.database.addLocalFile(metadata: metadata) - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) } } } diff --git a/iOSClient/Main/NCMainTabBarController.swift b/iOSClient/Main/NCMainTabBarController.swift index 4e70e35889..0c8a0aad70 100644 --- a/iOSClient/Main/NCMainTabBarController.swift +++ b/iOSClient/Main/NCMainTabBarController.swift @@ -46,6 +46,10 @@ class NCMainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() delegate = self + + if #available(iOS 17.0, *) { + traitOverrides.horizontalSizeClass = .compact + } } override func viewDidAppear(_ animated: Bool) { diff --git a/iOSClient/Main/NCPickerViewController.swift b/iOSClient/Main/NCPickerViewController.swift index 4e9cb89c00..8c413db174 100644 --- a/iOSClient/Main/NCPickerViewController.swift +++ b/iOSClient/Main/NCPickerViewController.swift @@ -157,7 +157,7 @@ class NCDocumentPickerViewController: NSObject, UIDocumentPickerDelegate { controller.present(UIAlertController.warning(message: "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"), animated: true) } else { database.addMetadata(metadata) - NCViewer().view(viewController: viewController, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: viewController, metadata: metadata) } } else { diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index 312ed892a6..fce198ca6b 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -27,34 +27,23 @@ import RealmSwift extension NCMedia: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - var mediaCell: NCGridMediaCell? - if let metadataDatasource = dataSource.getMetadata(indexPath: indexPath), - let metadata = database.getMetadataFromOcId(metadataDatasource.ocId) { - if let visibleCells = self.collectionView?.indexPathsForVisibleItems.compactMap({ self.collectionView?.cellForItem(at: $0) }) { - for case let cell as NCGridMediaCell in visibleCells { - if cell.ocId == metadata.ocId { - mediaCell = cell - } - } - } - if isEditMode { - if let index = fileSelect.firstIndex(of: metadata.ocId) { - fileSelect.remove(at: index) - mediaCell?.selected(false) - } else { - fileSelect.append(metadata.ocId) - mediaCell?.selected(true) + guard let metadata = dataSource.getMetadata(indexPath: indexPath), + let cell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell else { return } - } - tabBarSelect.selectCount = fileSelect.count + if isEditMode { + if let index = fileSelect.firstIndex(of: metadata.ocId) { + fileSelect.remove(at: index) + cell.selected(false) } else { - // ACTIVE SERVERURL - serverUrl = metadata.serverUrl - if let results = dataSource.getTableMetadatas() { - let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - NCViewer().view(viewController: self, metadata: metadata, metadatas: Array(results), indexMetadatas: indexPath.row, image: image) - } + fileSelect.append(metadata.ocId) + cell.selected(true) } + tabBarSelect.selectCount = fileSelect.count + } else if let metadata = database.getMetadataFromOcId(metadata.ocId) { + let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) + let ocIds = dataSource.getMetadatas().map { $0.ocId } + + NCViewer().view(viewController: self, metadata: metadata, ocIds: ocIds, image: image) } } @@ -64,7 +53,6 @@ extension NCMedia: UICollectionViewDelegate { else { return nil } let identifier = indexPath as NSCopying let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - self.serverUrl = metadata.serverUrl return UIContextMenuConfiguration(identifier: identifier, previewProvider: { return NCViewerProviderContextMenu(metadata: metadata, image: image) diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 95a1d090b6..ff3b1d5f2e 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -174,7 +174,7 @@ extension NCMedia { session: self.session, sceneIdentifier: self.controller?.sceneIdentifier) self.database.addMetadata(metadata) - NCViewer().view(viewController: self, metadata: metadata, metadatas: [metadata]) + NCViewer().view(viewController: self, metadata: metadata) })) self.present(alert, animated: true) } diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index d2b382bea5..4671c9f08c 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -48,7 +48,6 @@ class NCMedia: UIViewController { let database = NCManageDatabase.shared let imageCache = NCImageCache.shared var dataSource = NCMediaDataSource() - var serverUrl = "" let refreshControl = UIRefreshControl() var isTop: Bool = true var isEditMode = false @@ -143,6 +142,7 @@ class NCMedia: UIViewController { collectionView.refreshControl = refreshControl refreshControl.action(for: .valueChanged) { _ in self.loadDataSource() + self.searchMediaUI(true) } pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index b5f04c8f02..ecaa1969b7 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -189,7 +189,6 @@ public class NCMediaDataSource: NSObject { private let utilityFileSystem = NCUtilityFileSystem() private let global = NCGlobal.shared private var metadatas: [Metadata] = [] - private var tableMetadatas: Results? override init() { super.init() } @@ -201,16 +200,6 @@ public class NCMediaDataSource: NSObject { let metadata = getMetadataFromTableMetadata(metadata) self.metadatas.append(metadata) } - - let reference = ThreadSafeReference(to: metadatas) - DispatchQueue.main.async { - do { - let realm = try Realm() - self.tableMetadatas = realm.resolve(reference) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } } private func insertInMetadatas(metadata: Metadata) { @@ -252,10 +241,6 @@ public class NCMediaDataSource: NSObject { return self.metadatas } - func getTableMetadatas() -> Results? { - return self.tableMetadatas - } - func getMetadata(indexPath: IndexPath) -> Metadata? { if indexPath.row < self.metadatas.count { return self.metadatas[indexPath.row] diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 877a7812cb..eab80a900e 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -95,6 +95,7 @@ class NCGlobal: NSObject { let previewExt256 = ".256.preview.jpg" func getSizeExtension(column: Int) -> String { + if column == 0 { return previewExt256 } let width = UIScreen.main.bounds.width / CGFloat(column) switch (width * 4) { @@ -369,6 +370,7 @@ class NCGlobal: NSObject { let configuration_disable_openin_file = "disable_openin_file" // MORE NEXTCLOUD APPS + // let talkSchemeUrl = "nextcloudtalk://" let notesSchemeUrl = "nextcloudnotes://" let talkAppStoreUrl = "https://apps.apple.com/in/app/nextcloud-talk/id1296825574" diff --git a/iOSClient/Recent/NCRecent.swift b/iOSClient/Recent/NCRecent.swift index 50bf0752a7..03e461dd56 100644 --- a/iOSClient/Recent/NCRecent.swift +++ b/iOSClient/Recent/NCRecent.swift @@ -49,7 +49,7 @@ class NCRecent: NCCollectionViewCommon { override func queryDB() { super.queryDB() - let metadatas = self.database.getResultsMetadatas(predicate: NSPredicate(format: "account == %@", session.account), sortedByKeyPath: "date", ascending: false, arraySlice: 200) + let metadatas = self.database.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND fileName != '.'", session.account), sortedByKeyPath: "date", ascending: false, arraySlice: 200) layoutForView?.sort = "date" layoutForView?.ascending = false diff --git a/iOSClient/Trash/NCTrash+CollectionView.swift b/iOSClient/Trash/NCTrash+CollectionView.swift index 76c330643f..90f673dc23 100644 --- a/iOSClient/Trash/NCTrash+CollectionView.swift +++ b/iOSClient/Trash/NCTrash+CollectionView.swift @@ -85,7 +85,7 @@ extension NCTrash: UICollectionViewDataSource { } else { if resultTableTrash.hasPreview { if NCNetworking.shared.downloadThumbnailTrashQueue.operations.filter({ ($0 as? NCOperationDownloadThumbnailTrash)?.fileId == resultTableTrash.fileId }).isEmpty { - NCNetworking.shared.downloadThumbnailTrashQueue.addOperation(NCOperationDownloadThumbnailTrash(resultTableTrash: resultTableTrash, fileId: resultTableTrash.fileId, account: session.account, cell: cell, collectionView: collectionView)) + NCNetworking.shared.downloadThumbnailTrashQueue.addOperation(NCOperationDownloadThumbnailTrash(fileId: resultTableTrash.fileId, fileName: resultTableTrash.fileName, account: session.account, collectionView: collectionView)) } } } diff --git a/iOSClient/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index b3fd91305b..8d2e267e19 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -86,35 +86,34 @@ extension NCTrash { } class NCOperationDownloadThumbnailTrash: ConcurrentOperation, @unchecked Sendable { - var trash: tableTrash var fileId: String - var collectionView: UICollectionView? - var cell: NCTrashCellProtocol? + var fileName: String + var collectionView: UICollectionView var account: String - init(resultTableTrash: tableTrash, fileId: String, account: String, cell: NCTrashCellProtocol?, collectionView: UICollectionView?) { - self.trash = tableTrash(value: resultTableTrash) + init(fileId: String, fileName: String, account: String, collectionView: UICollectionView) { self.fileId = fileId + self.fileName = fileName self.account = account - self.cell = cell self.collectionView = collectionView } override func start() { guard !isCancelled else { return self.finish() } - NextcloudKit.shared.downloadTrashPreview(fileId: trash.fileId, - account: account) { _, data, _, _, error in - if error == .success, - let data, - self.fileId == self.cell?.objectId, - let imageView = self.cell?.imageItem { - self.cell?.imageItem?.contentMode = .scaleAspectFill - UIView.transition(with: imageView, + NextcloudKit.shared.downloadTrashPreview(fileId: fileId, account: account) { _, data, _, _, error in + if error == .success, let data { + + NCUtility().createImage(ocId: self.fileId, etag: self.fileName, data: data) + + for case let cell as NCTrashCellProtocol in self.collectionView.visibleCells where cell.objectId == self.fileId { + cell.imageItem?.contentMode = .scaleAspectFill + UIView.transition(with: cell.imageItem, duration: 0.75, options: .transitionCrossDissolve, - animations: { imageView.image = UIImage(data: data) }, + animations: { cell.imageItem.image = UIImage(data: data) }, completion: nil) + } } self.finish() } diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index 5e86db5ff2..a263cca5f8 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -139,14 +139,14 @@ extension NCUtility { guard let image else { return } - createImageStandard(ocId: metadata.ocId, etag: metadata.etag, classFile: metadata.classFile, image: image, cost: cost) + createImageStandard(ocId: metadata.ocId, etag: metadata.etag, image: image, cost: cost) } func createImage(metadata: tableMetadata, data: Data, cost: Int = 0) { - createImage(ocId: metadata.ocId, etag: metadata.etag, classFile: metadata.classFile, data: data, cost: cost) + createImage(ocId: metadata.ocId, etag: metadata.etag, data: data, cost: cost) } - func createImage(ocId: String, etag: String, classFile: String, data: Data, cost: Int = 0) { + func createImage(ocId: String, etag: String, data: Data, cost: Int = 0) { guard let image = UIImage(data: data) else { return } let fileNamePath1024 = self.utilityFileSystem.getDirectoryProviderStorageImageOcId(ocId, etag: etag, ext: global.previewExt1024) @@ -154,10 +154,10 @@ extension NCUtility { try data.write(to: URL(fileURLWithPath: fileNamePath1024), options: .atomic) } catch { } - createImageStandard(ocId: ocId, etag: etag, classFile: classFile, image: image, cost: cost) + createImageStandard(ocId: ocId, etag: etag, image: image, cost: cost) } - private func createImageStandard(ocId: String, etag: String, classFile: String, image: UIImage, cost: Int) { + private func createImageStandard(ocId: String, etag: String, image: UIImage, cost: Int) { let ext = [global.previewExt512, global.previewExt256] let size = [global.size512, global.size256] let compressionQuality = [0.6, 0.7] diff --git a/iOSClient/Viewer/NCViewer.swift b/iOSClient/Viewer/NCViewer.swift index 104aba9bb2..6fb405966a 100644 --- a/iOSClient/Viewer/NCViewer.swift +++ b/iOSClient/Viewer/NCViewer.swift @@ -30,12 +30,8 @@ class NCViewer: NSObject { let utility = NCUtility() let database = NCManageDatabase.shared private var viewerQuickLook: NCViewerQuickLook? - private var metadata = tableMetadata() - private var metadatas: [tableMetadata] = [] - func view(viewController: UIViewController, metadata: tableMetadata, metadatas: [tableMetadata], indexMetadatas: Int = 0, image: UIImage? = nil) { - self.metadata = metadata - self.metadatas = metadatas + func view(viewController: UIViewController, metadata: tableMetadata, ocIds: [String]? = nil, image: UIImage? = nil) { let session = NCSession.shared.getSession(account: metadata.account) // URL @@ -64,9 +60,17 @@ class NCViewer: NSObject { if metadata.isImage || metadata.isAudioOrVideo { if let navigationController = viewController.navigationController, let viewerMediaPageContainer: NCViewerMediaPage = UIStoryboard(name: "NCViewerMediaPage", bundle: nil).instantiateInitialViewController() as? NCViewerMediaPage { - viewerMediaPageContainer.currentIndex = indexMetadatas - viewerMediaPageContainer.metadatas = metadatas + viewerMediaPageContainer.delegateViewController = viewController + + if let ocIds { + viewerMediaPageContainer.currentIndex = ocIds.firstIndex(where: { $0 == metadata.ocId }) ?? 0 + viewerMediaPageContainer.ocIds = ocIds + } else { + viewerMediaPageContainer.currentIndex = 0 + viewerMediaPageContainer.ocIds = [metadata.ocId] + } + navigationController.pushViewController(viewerMediaPageContainer, animated: true) } return diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift index bea151e685..c7ad2c4ede 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift @@ -35,26 +35,13 @@ var viewerMediaScreenMode: ScreenMode = .normal class NCViewerMediaPage: UIViewController { @IBOutlet weak var progressView: UIProgressView! - // swiftlint:disable force_cast - var pageViewController: UIPageViewController { - return self.children[0] as! UIPageViewController - } - - var currentViewController: NCViewerMedia { - return self.pageViewController.viewControllers![0] as! NCViewerMedia - } - // swiftlint:enable force_cast - - private var hideStatusBar: Bool = false { - didSet { - setNeedsStatusBarAppearanceUpdate() - } - } - - var metadatas: [tableMetadata] = [] + /// Parameters + var ocIds: [String] = [] + var currentIndex: Int = 0 var delegateViewController: UIViewController? + + /// var modifiedOcId: [String] = [] - var currentIndex: Int = 0 var nextIndex: Int? var panGestureRecognizer: UIPanGestureRecognizer! var singleTapGestureRecognizer: UITapGestureRecognizer! @@ -67,6 +54,7 @@ class NCViewerMediaPage: UIViewController { var nextTrackCommand: Any? var previousTrackCommand: Any? let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared // This prevents the scroll views to scroll when you drag and drop files/images/subjects (from this or other apps) // https://forums.developer.apple.com/forums/thread/89396 and https://forums.developer.apple.com/forums/thread/115736 @@ -78,6 +66,22 @@ class NCViewerMediaPage: UIViewController { private lazy var moreNavigationItem = UIBarButtonItem(image: NCImageCache.shared.getImageButtonMore(), style: .plain, target: self, action: #selector(openMenuMore)) private lazy var imageDetailNavigationItem = UIBarButtonItem(image: NCUtility().loadImage(named: "info.circle", colors: [NCBrandColor.shared.iconImageColor]), style: .plain, target: self, action: #selector(toggleDetail)) + // swiftlint:disable force_cast + var pageViewController: UIPageViewController { + return self.children[0] as! UIPageViewController + } + + var currentViewController: NCViewerMedia { + return self.pageViewController.viewControllers![0] as! NCViewerMedia + } + // swiftlint:enable force_cast + + private var hideStatusBar: Bool = false { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + // MARK: - View Life Cycle override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { @@ -96,6 +100,7 @@ class NCViewerMediaPage: UIViewController { super.viewDidLoad() navigationController?.navigationBar.tintColor = NCBrandColor.shared.iconImageColor + let metadata = database.getMetadataFromOcId(ocIds[currentIndex])! singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanWith(gestureRecognizer:))) @@ -111,11 +116,11 @@ class NCViewerMediaPage: UIViewController { pageViewController.view.addGestureRecognizer(singleTapGestureRecognizer) pageViewController.view.addGestureRecognizer(longtapGestureRecognizer) - progressView.tintColor = NCBrandColor.shared.getElement(account: metadatas.first?.account) + progressView.tintColor = NCBrandColor.shared.getElement(account: metadata.account) progressView.trackTintColor = .clear progressView.progress = 0 - let viewerMedia = getViewerMedia(index: currentIndex, metadata: tableMetadata(value: metadatas[currentIndex])) + let viewerMedia = getViewerMedia(index: currentIndex, metadata: metadata) pageViewController.setViewControllers([viewerMedia], direction: .forward, animated: true, completion: nil) changeScreenMode(mode: viewerMediaScreenMode) @@ -411,8 +416,7 @@ class NCViewerMediaPage: UIViewController { @objc func renameFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let index = metadatas.firstIndex(where: {$0.ocId == ocId}) + let ocId = userInfo["ocId"] as? String else { return } // Stop media @@ -420,8 +424,7 @@ class NCViewerMediaPage: UIViewController { ncplayer.playerPause() } - let metadata = metadatas[index] - if index == currentIndex { + if currentIndex == ocIds.firstIndex(where: { $0 == ocId}), let metadata = database.getMetadataFromOcId(ocId) { navigationItem.title = metadata.fileNameView currentViewController.metadata = metadata self.currentViewController.metadata = metadata @@ -534,16 +537,16 @@ class NCViewerMediaPage: UIViewController { extension NCViewerMediaPage: UIPageViewControllerDelegate, UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - if currentIndex == 0 { return nil } - let metadata = tableMetadata(value: metadatas[currentIndex - 1]) + guard currentIndex > 0, + let metadata = database.getMetadataFromOcId(ocIds[currentIndex - 1]) else { return nil } let viewerMedia = getViewerMedia(index: currentIndex - 1, metadata: metadata) return viewerMedia } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - if currentIndex == metadatas.count - 1 { return nil } - let metadata = tableMetadata(value: metadatas[currentIndex + 1]) + guard currentIndex < ocIds.count - 1, + let metadata = database.getMetadataFromOcId(ocIds[currentIndex + 1]) else { return nil } let viewerMedia = getViewerMedia(index: currentIndex + 1, metadata: metadata) return viewerMedia From 5381a29490a290a29f4ed1b371bda4ea3e06621d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 23 Sep 2024 10:49:53 +0200 Subject: [PATCH 16/31] cod Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMedia.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 4671c9f08c..a46b6dd14b 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -257,8 +257,20 @@ class NCMedia: UIViewController { fileDeleted = fileDeleted + ocId + var indexPaths: [IndexPath] = [] + + for ocId in ocId { + if let row = dataSource.getMetadatas().firstIndex(where: {$0.ocId == ocId}) { + indexPaths.append(IndexPath(row: row, section: 0)) + } + } + dataSource.removeMetadata(ocId) - collectionViewReloadData() + if indexPaths.count == ocId.count { + collectionView.deleteItems(at: indexPaths) + } else { + collectionViewReloadData() + } if error != .success { NCContentPresenter().showError(error: error) From 956ccd88301d624e27aaeb4c1f4fc9bf81f52ae0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 23 Sep 2024 11:04:24 +0200 Subject: [PATCH 17/31] cleaning Signed-off-by: Marino Faggiana --- .../NCMedia+CollectionViewDataSource.swift | 6 ++--- .../NCMedia+CollectionViewDelegate.swift | 2 +- iOSClient/Media/NCMedia+MediaLayout.swift | 4 +-- iOSClient/Media/NCMedia.swift | 23 +++++++++------- iOSClient/Media/NCMediaDataSource.swift | 26 ++++--------------- 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index 4bfa056688..b505cd1c10 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -39,8 +39,8 @@ extension NCMedia: UICollectionViewDataSource { return header } else { guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFooter", for: indexPath) as? NCSectionFooter else { return NCSectionFooter() } - let images = dataSource.getMetadatas().filter({ $0.isImage }).count - let video = dataSource.getMetadatas().count - images + let images = dataSource.metadatas.filter({ $0.isImage }).count + let video = dataSource.metadatas.count - images footer.setTitleLabel("\(images) " + NSLocalizedString("_images_", comment: "") + " • " + "\(video) " + NSLocalizedString("_video_", comment: "")) footer.separatorIsHidden(true) @@ -49,7 +49,7 @@ extension NCMedia: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - let numberOfItemsInSection = dataSource.getMetadatas().count + let numberOfItemsInSection = dataSource.metadatas.count self.numberOfColumns = getColumnCount() if numberOfItemsInSection == 0 || NCNetworking.shared.isOffline { diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index fce198ca6b..a382a3c988 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -41,7 +41,7 @@ extension NCMedia: UICollectionViewDelegate { tabBarSelect.selectCount = fileSelect.count } else if let metadata = database.getMetadataFromOcId(metadata.ocId) { let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - let ocIds = dataSource.getMetadatas().map { $0.ocId } + let ocIds = dataSource.metadatas.map { $0.ocId } NCViewer().view(viewController: self, metadata: metadata, ocIds: ocIds, image: image) } diff --git a/iOSClient/Media/NCMedia+MediaLayout.swift b/iOSClient/Media/NCMedia+MediaLayout.swift index 7caba5e2a6..eec118200e 100644 --- a/iOSClient/Media/NCMedia+MediaLayout.swift +++ b/iOSClient/Media/NCMedia+MediaLayout.swift @@ -50,14 +50,14 @@ extension NCMedia: NCMediaLayoutDelegate { func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, heightForHeaderInSection section: Int) -> Float { var height: Double = 0 - if dataSource.getMetadatas().count == 0 { + if dataSource.metadatas.count == 0 { height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: -20) } return Float(height) } func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, heightForFooterInSection section: Int) -> Float { - if dataSource.getMetadatas().count == 0 { + if dataSource.metadatas.count == 0 { return .zero } else { return 70.0 diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index a46b6dd14b..8237d57498 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -156,7 +156,7 @@ class NCMedia: UIViewController { } NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterClearCache), object: nil, queue: nil) { _ in - self.dataSource.removeAll() + self.dataSource.metadatas.removeAll() self.imageCache.removeAll() self.searchMediaUI(true) } @@ -172,7 +172,7 @@ class NCMedia: UIViewController { super.viewWillAppear(animated) navigationController?.setMediaAppreance() - if dataSource.isEmpty() { + if dataSource.metadatas.isEmpty { loadDataSource() } } @@ -252,21 +252,24 @@ class NCMedia: UIViewController { @objc func deleteFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? [String], + let ocIds = userInfo["ocId"] as? [String], let error = userInfo["error"] as? NKError else { return } - fileDeleted = fileDeleted + ocId + fileDeleted = fileDeleted + ocIds var indexPaths: [IndexPath] = [] + let indices = dataSource.metadatas.enumerated().filter { ocIds.contains($0.element.ocId) }.map { $0.offset } - for ocId in ocId { - if let row = dataSource.getMetadatas().firstIndex(where: {$0.ocId == ocId}) { - indexPaths.append(IndexPath(row: row, section: 0)) + for index in indices { + let indexPath = IndexPath(row: index, section: 0) + if let cell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, + dataSource.metadatas[index].ocId == cell.ocId { + indexPaths.append(indexPath) } } - dataSource.removeMetadata(ocId) - if indexPaths.count == ocId.count { + dataSource.removeMetadata(ocIds) + if indexPaths.count == ocIds.count { collectionView.deleteItems(at: indexPaths) } else { collectionViewReloadData() @@ -352,7 +355,7 @@ class NCMedia: UIViewController { extension NCMedia: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !dataSource.isEmpty() { + if !dataSource.metadatas.isEmpty { isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25) setColor() setTitleDate() diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index ecaa1969b7..b1e7e12357 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -59,7 +59,7 @@ extension NCMedia { let limit = max(collectionView.visibleCells.count * 2, 300) var lessDate = Date.distantFuture var greaterDate = Date.distantPast - let countMetadatas = self.dataSource.getMetadatas().count + let countMetadatas = self.dataSource.metadatas.count let options = NKRequestOptions(timeout: 120, taskDescription: NCGlobal.shared.taskDescriptionRetrievesProperties, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) var firstCellDate: Date? var lastCellDate: Date? @@ -71,7 +71,7 @@ extension NCMedia { if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }), !distant { firstCellDate = (visibleCells.first as? NCGridMediaCell)?.date - if firstCellDate == self.dataSource.getMetadatas().first?.date { + if firstCellDate == self.dataSource.metadatas.first?.date { lessDate = Date.distantFuture } else { if let date = firstCellDate { @@ -82,7 +82,7 @@ extension NCMedia { } lastCellDate = (visibleCells.last as? NCGridMediaCell)?.date - if lastCellDate == self.dataSource.getMetadatas().last?.date { + if lastCellDate == self.dataSource.metadatas.last?.date { greaterDate = Date.distantPast } else { if let date = lastCellDate { @@ -117,7 +117,7 @@ extension NCMedia { /// No files, remove all if lessDate == Date.distantFuture, greaterDate == Date.distantPast, files.isEmpty { - self.dataSource.removeAll() + self.dataSource.metadatas.removeAll() self.collectionViewReloadData() } @@ -188,7 +188,7 @@ public class NCMediaDataSource: NSObject { private let utilityFileSystem = NCUtilityFileSystem() private let global = NCGlobal.shared - private var metadatas: [Metadata] = [] + var metadatas: [Metadata] = [] override init() { super.init() } @@ -225,22 +225,6 @@ public class NCMediaDataSource: NSObject { // MARK: - - func removeAll() { - self.metadatas.removeAll() - } - - func isEmpty() -> Bool { - return self.metadatas.isEmpty - } - - func count() -> Int { - return self.metadatas.count - } - - func getMetadatas() -> [Metadata] { - return self.metadatas - } - func getMetadata(indexPath: IndexPath) -> Metadata? { if indexPath.row < self.metadatas.count { return self.metadatas[indexPath.row] From 77afe887d0a57dfeb4b2b7131f1adb20a3218750 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 23 Sep 2024 16:27:59 +0200 Subject: [PATCH 18/31] Activity/Comment cell fixes (#3064) * Handle SVGs/Fix cell height Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev --- iOSClient/Activity/NCActivity.swift | 15 ++++---- iOSClient/Networking/NCService.swift | 1 - .../Advanced/NCShareNewUserAddComment.swift | 1 - iOSClient/Share/NCShareCommentsCell.xib | 35 ++++++++++--------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 8b4eb2e5fe..78fce24e12 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -25,6 +25,7 @@ import UIKit import SwiftRichString import NextcloudKit +import SVGKit class NCActivity: UIViewController, NCSharePagingContent { @IBOutlet weak var viewContainerConstraint: NSLayoutConstraint! @@ -150,10 +151,6 @@ extension NCActivity: UITableViewDelegate { return 50 } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 80 - } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let view = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 50)) view.backgroundColor = .clear @@ -223,7 +220,6 @@ extension NCActivity: UITableViewDataSource { cell.indexPath = indexPath cell.tableComments = comment cell.delegate = self - cell.sizeToFit() // Image let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: comment.actorId) @@ -233,7 +229,7 @@ extension NCActivity: UITableViewDataSource { cell.labelUser.textColor = NCBrandColor.shared.textColor // Date cell.labelDate.text = utility.dateDiff(comment.creationDateTime as Date) - cell.labelDate.textColor = .systemGray4 + cell.labelDate.textColor = .lightGray // Message cell.labelMessage.text = comment.message cell.labelMessage.textColor = NCBrandColor.shared.textColor @@ -244,6 +240,8 @@ extension NCActivity: UITableViewDataSource { cell.buttonMenu.isHidden = true } + cell.sizeToFit() + return cell } @@ -264,11 +262,14 @@ extension NCActivity: UITableViewDataSource { // icon if !activity.icon.isEmpty { + activity.icon = activity.icon.replacingOccurrences(of: ".png", with: ".svg") let fileNameIcon = (activity.icon as NSString).lastPathComponent let fileNameLocalPath = utilityFileSystem.directoryUserData + "/" + fileNameIcon if FileManager.default.fileExists(atPath: fileNameLocalPath) { - if let image = UIImage(contentsOfFile: fileNameLocalPath) { + let image = fileNameIcon.contains(".svg") ? SVGKImage(contentsOfFile: fileNameLocalPath)?.uiImage : UIImage(contentsOfFile: fileNameLocalPath) + + if let image { cell.icon.image = image.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) } } else { diff --git a/iOSClient/Networking/NCService.swift b/iOSClient/Networking/NCService.swift index 057c04179c..a1b9633d3d 100644 --- a/iOSClient/Networking/NCService.swift +++ b/iOSClient/Networking/NCService.swift @@ -22,7 +22,6 @@ // import UIKit -import SVGKit import NextcloudKit import RealmSwift diff --git a/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift b/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift index a4bd06855f..7d76452141 100644 --- a/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift +++ b/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift @@ -21,7 +21,6 @@ import UIKit import NextcloudKit -import SVGKit class NCShareNewUserAddComment: UIViewController, NCShareDetail { diff --git a/iOSClient/Share/NCShareCommentsCell.xib b/iOSClient/Share/NCShareCommentsCell.xib index cceef5a118..243aa83341 100755 --- a/iOSClient/Share/NCShareCommentsCell.xib +++ b/iOSClient/Share/NCShareCommentsCell.xib @@ -1,9 +1,9 @@ - + - + @@ -11,11 +11,11 @@ - - + + - + @@ -26,11 +26,11 @@ @@ -53,8 +53,8 @@ - @@ -93,7 +94,7 @@ - + @@ -102,10 +103,10 @@ - + - + From 2ea1c6737c2b0c40812f297f575588c9be710a1c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 23 Sep 2024 17:48:33 +0200 Subject: [PATCH 19/31] I os18 fix (#3072) --------- Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 24 +++-- ...nViewCommon+CollectionViewDataSource.swift | 4 +- ...+CollectionViewDataSourcePrefetching.swift | 5 +- .../NCCollectionViewCommon+MediaLayout.swift | 27 +++-- .../NCCollectionViewCommon.swift | 102 ++++++------------ .../NCCollectionViewCommonPinchGesture.swift | 89 +++++++++++++++ ...CGridMediaCell.swift => NCMediaCell.swift} | 4 +- .../{NCGridMediaCell.xib => NCMediaCell.xib} | 6 +- .../NCMedia+CollectionViewDataSource.swift | 21 ++-- .../NCMedia+CollectionViewDelegate.swift | 2 +- iOSClient/Media/NCMedia+Command.swift | 6 +- iOSClient/Media/NCMedia.swift | 19 +++- iOSClient/Media/NCMediaDataSource.swift | 10 +- .../Media/NCMediaDownloadThumbnail.swift | 2 +- 14 files changed, 201 insertions(+), 120 deletions(-) create mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift rename iOSClient/Media/Cell/{NCGridMediaCell.swift => NCMediaCell.swift} (96%) rename iOSClient/Media/Cell/{NCGridMediaCell.xib => NCMediaCell.xib} (94%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e119911d42..a0d543ee0f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -609,8 +609,8 @@ F774264A22EB4D0000B23912 /* NCSearchUserDropDownCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */; }; F7743A122C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */; }; F7743A142C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */; }; - F77444F522281649000D5EB0 /* NCGridMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F322281649000D5EB0 /* NCGridMediaCell.swift */; }; - F77444F622281649000D5EB0 /* NCGridMediaCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F77444F422281649000D5EB0 /* NCGridMediaCell.xib */; }; + F77444F522281649000D5EB0 /* NCMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F322281649000D5EB0 /* NCMediaCell.swift */; }; + F77444F622281649000D5EB0 /* NCMediaCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F77444F422281649000D5EB0 /* NCMediaCell.xib */; }; F77444F8222816D5000D5EB0 /* NCPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */; }; F778231E2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */; }; F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */; }; @@ -851,6 +851,7 @@ F7CEE6012BA9A5C9003EFD89 /* NCTrashGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CEE5FF2BA9A5C9003EFD89 /* NCTrashGridCell.swift */; }; F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */; }; F7D1C4AC2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */; }; + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D57C8626317BDA00DE301D /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7CA212C25F1333200826ABB /* NCAccountRequest.storyboard */; }; F7D57C8B26317BDE00DE301D /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CA212B25F1333200826ABB /* NCAccountRequest.swift */; }; @@ -1472,8 +1473,8 @@ F77439621FCD6D9C00662C46 /* es-UY */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-UY"; path = "es-UY.lproj/Localizable.strings"; sourceTree = ""; }; F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDelegate.swift"; sourceTree = ""; }; F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDataSource.swift"; sourceTree = ""; }; - F77444F322281649000D5EB0 /* NCGridMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCGridMediaCell.swift; sourceTree = ""; }; - F77444F422281649000D5EB0 /* NCGridMediaCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCGridMediaCell.xib; sourceTree = ""; }; + F77444F322281649000D5EB0 /* NCMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaCell.swift; sourceTree = ""; }; + F77444F422281649000D5EB0 /* NCMediaCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCMediaCell.xib; sourceTree = ""; }; F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPickerViewController.swift; sourceTree = ""; }; F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+MediaLayout.swift"; sourceTree = ""; }; F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; @@ -1696,6 +1697,7 @@ F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerRichWorkspace.storyboard; sourceTree = ""; }; F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDataSourcePrefetching.swift"; sourceTree = ""; }; F7D2C772246470CA008513AE /* XLForm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XLForm.framework; path = Carthage/Build/iOS/XLForm.framework; sourceTree = ""; }; + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; @@ -2138,8 +2140,8 @@ F720B5B72507B9A5008C94E5 /* Cell */ = { isa = PBXGroup; children = ( - F77444F322281649000D5EB0 /* NCGridMediaCell.swift */, - F77444F422281649000D5EB0 /* NCGridMediaCell.xib */, + F77444F322281649000D5EB0 /* NCMediaCell.swift */, + F77444F422281649000D5EB0 /* NCMediaCell.xib */, ); path = Cell; sourceTree = ""; @@ -2331,6 +2333,7 @@ F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */, F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */, F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBar.swift */, + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */, F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */, F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */, F7E7AEA42BA32C6500512E52 /* NCCollectionViewDownloadThumbnail.swift */, @@ -3731,7 +3734,7 @@ buildActionMask = 2147483647; files = ( F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */, - F77444F622281649000D5EB0 /* NCGridMediaCell.xib in Resources */, + F77444F622281649000D5EB0 /* NCMediaCell.xib in Resources */, F78ACD4421903CF20088454D /* NCListCell.xib in Resources */, F3BB464D2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib in Resources */, F7F4F10727ECDBDB008676F9 /* Inconsolata-Black.ttf in Resources */, @@ -4272,7 +4275,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F77444F522281649000D5EB0 /* NCGridMediaCell.swift in Sources */, + F77444F522281649000D5EB0 /* NCMediaCell.swift in Sources */, F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */, F7E402332BA89551007E5609 /* NCTrash+Networking.swift in Sources */, F7E7AEA72BA32D0000512E52 /* NCCollectionViewUnifiedSearch.swift in Sources */, @@ -4402,6 +4405,7 @@ F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */, F74B6D952A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, F7A8FD522C5E2557006C9CF8 /* NCAccount.swift in Sources */, F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */, @@ -5470,7 +5474,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5536,7 +5540,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 7341493534..1f88511ccc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -38,7 +38,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { guard let metadata = self.dataSource.getMetadata(indexPath: indexPath), let cell = (cell as? NCCellProtocol) else { return } let existsPreview = utility.existsImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) - let ext = global.getSizeExtension(column: self.column) + let ext = global.getSizeExtension(column: self.numberOfColumns) func downloadAvatar(fileName: String, user: String, dispalyName: String?) { if let image = database.getImageAvatarLoaded(fileName: fileName) { @@ -87,7 +87,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } if metadata.hasPreview && metadata.status == global.metadataStatusNormal && !existsPreview { for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { return } - NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: NCGlobal.shared.getSizeExtension(column: self.column))) + NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: NCGlobal.shared.getSizeExtension(column: self.numberOfColumns))) } } } else { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift index 1c173ea538..f2d31a3747 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift @@ -26,8 +26,9 @@ import UIKit extension NCCollectionViewCommon: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { - guard !isSearchingMode else { return } - let ext = global.getSizeExtension(column: self.column) + guard !isSearchingMode, !imageCache.isLoadingCache else { return } + + let ext = global.getSizeExtension(column: self.numberOfColumns) let metadatas = self.dataSource.getMetadatas(indexPaths: indexPaths) let cost = indexPaths.first?.row ?? 0 diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift index ea5f3a8849..ac95c0893e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift @@ -27,11 +27,15 @@ import NextcloudKit extension NCCollectionViewCommon: NCMediaLayoutDelegate { func getColumnCount() -> Int { - if let column = self.layoutForView?.columnPhoto, column > 0 { - return column + if self.numberOfColumns == 0, + let layoutForView = database.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewFiles, serverUrl: self.serverUrl) { + if layoutForView.columnPhoto > 0 { + self.numberOfColumns = layoutForView.columnPhoto + } else { + self.numberOfColumns = 3 + } } - self.layoutForView?.columnPhoto = 3 - return 3 + return self.numberOfColumns } func getLayout() -> String? { @@ -63,11 +67,16 @@ extension NCCollectionViewCommon: NCMediaLayoutDelegate { } func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath, columnCount: Int, typeLayout: String) -> CGSize { - if typeLayout == global.layoutPhotoRatio, - let metadata = self.dataSource.getMetadata(indexPath: indexPath), - metadata.imageSize != CGSize.zero { - return metadata.imageSize + if typeLayout == NCGlobal.shared.layoutPhotoSquare { + return CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) + } else { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return .zero } + + if metadata.imageSize != CGSize.zero { + return metadata.imageSize + } else { + return CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) + } } - return CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 09ed060579..3aa45e0848 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -36,6 +36,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let utility = NCUtility() let utilityFileSystem = NCUtilityFileSystem() let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() var autoUploadFileName = "" var autoUploadDirectory = "" @@ -65,7 +66,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var tabBarSelect: NCCollectionViewCommonSelectTabBar! var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - let maxImageGrid: CGFloat = 7 // DECLARE var layoutKey = "" @@ -83,6 +83,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var emptyDataPortaitOffset: CGFloat = 0 var emptyDataLandscapeOffset: CGFloat = -20 + var lastScale: CGFloat = 1.0 + var currentScale: CGFloat = 1.0 + let maxColumns: Int = 10 + var transitionColumns = false + var numberOfColumns: Int = 0 + var lastNumberOfColumns: Int = 0 + var session: NCSession.Session { NCSession.shared.getSession(controller: tabBarController) } @@ -119,16 +126,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - var column: Int { - if isLayoutPhoto { - return layoutForView?.columnPhoto ?? 3 - } else if isLayoutGrid { - return layoutForView?.columnGrid ?? 3 - } else { - return 0 - } - } - var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } @@ -147,7 +144,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.navigationController?.presentationController?.delegate = self collectionView.alwaysBounceVertical = true - // Color view.backgroundColor = .systemBackground collectionView.backgroundColor = .systemBackground refreshControl.tintColor = NCBrandColor.shared.textColor2 @@ -181,14 +177,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "sectionFooter") collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: mediaSectionFooter, withReuseIdentifier: "sectionFooter") - // Refresh Control collectionView.refreshControl = refreshControl refreshControl.action(for: .valueChanged) { _ in self.database.cleanEtagDirectory(serverUrl: self.serverUrl, account: self.session.account) self.reloadDataSourceNetwork() } - // Long Press on CollectionView let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressCollecationView(_:))) longPressedGesture.minimumPressDuration = 0.5 longPressedGesture.delegate = self @@ -200,6 +194,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS collectionView.dragDelegate = self collectionView.dropDelegate = self + pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) + collectionView.addGestureRecognizer(pinchGesture) + let dropInteraction = UIDropInteraction(delegate: self) self.navigationController?.navigationItem.leftBarButtonItems?.first?.customView?.addInteraction(dropInteraction) @@ -661,7 +658,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func createMenuActions() -> [UIMenuElement] { guard let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return [] } - let columnPhoto = self.layoutForView?.columnPhoto ?? 3 func saveLayout(_ layoutForView: NCDBLayoutForView) { database.setLayoutForView(layoutForView: layoutForView) @@ -669,20 +665,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS setNavigationRightItems() } - if layoutForView.layout != global.layoutPhotoSquare && layoutForView.layout != global.layoutPhotoRatio { - self.attributesZoomIn = .disabled - self.attributesZoomOut = .disabled - } else if CGFloat(columnPhoto) >= maxImageGrid - 1 { - self.attributesZoomIn = [] - self.attributesZoomOut = .disabled - } else if columnPhoto <= 1 { - self.attributesZoomIn = .disabled - self.attributesZoomOut = [] - } else { - self.attributesZoomIn = [] - self.attributesZoomOut = [] - } - let select = UIAction(title: NSLocalizedString("_select_", comment: ""), image: utility.loadImage(named: "checkmark.circle"), attributes: (self.dataSource.isEmpty() || NCNetworking.shared.isOffline) ? .disabled : []) { _ in @@ -713,53 +695,31 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.setNavigationRightItems() } - let menuPhoto = UIMenu(title: "", options: .displayInline, children: [ - UIAction(title: NSLocalizedString("_media_square_", comment: ""), image: utility.loadImage(named: "square.grid.3x3"), state: layoutForView.layout == global.layoutPhotoSquare ? .on : .off) { _ in - layoutForView.layout = self.global.layoutPhotoSquare - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.layoutType = self.global.layoutPhotoSquare + let mediaSquare = UIAction(title: NSLocalizedString("_media_square_", comment: ""), image: utility.loadImage(named: "square.grid.3x3"), state: layoutForView.layout == global.layoutPhotoSquare ? .on : .off) { _ in + layoutForView.layout = self.global.layoutPhotoSquare + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.layoutType = self.global.layoutPhotoSquare - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } - self.reloadDataSource() - self.setNavigationRightItems() - }, - UIAction(title: NSLocalizedString("_media_ratio_", comment: ""), image: utility.loadImage(named: "rectangle.grid.3x2"), state: layoutForView.layout == self.global.layoutPhotoRatio ? .on : .off) { _ in - layoutForView.layout = self.global.layoutPhotoRatio - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.layoutType = self.global.layoutPhotoRatio + self.setNavigationRightItems() + } - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } + let mediaRatio = UIAction(title: NSLocalizedString("_media_ratio_", comment: ""), image: utility.loadImage(named: "rectangle.grid.3x2"), state: layoutForView.layout == self.global.layoutPhotoRatio ? .on : .off) { _ in + layoutForView.layout = self.global.layoutPhotoRatio + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.layoutType = self.global.layoutPhotoRatio - self.reloadDataSource() - self.setNavigationRightItems() - } - ]) - - let menuZoom = UIMenu(title: "", options: .displayInline, children: [ - UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: utility.loadImage(named: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in - layoutForView.columnPhoto = columnPhoto + 1 - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.setNavigationRightItems() - UIView.transition(with: self.collectionView, duration: 0.5, options: .transitionCrossDissolve, animations: { - self.collectionView.reloadData() - }, completion: nil) - }, - UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: utility.loadImage(named: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in - layoutForView.columnPhoto = columnPhoto - 1 - self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) - self.setNavigationRightItems() - UIView.transition(with: self.collectionView, duration: 0.5, options: .transitionCrossDissolve, animations: { - self.collectionView.reloadData() - }, completion: nil) - } - ]) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } + + self.setNavigationRightItems() + } - let viewStyleSubmenu = UIMenu(title: "", options: .displayInline, children: [list, grid, UIMenu(title: NSLocalizedString("_additional_view_options_", comment: ""), children: [menuPhoto, menuZoom])]) + let viewStyleSubmenu = UIMenu(title: "", options: .displayInline, children: [list, grid, mediaSquare, mediaRatio]) let ascending = layoutForView.ascending let ascendingChevronImage = utility.loadImage(named: ascending ? "chevron.up" : "chevron.down") diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift new file mode 100644 index 0000000000..d934c836a4 --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift @@ -0,0 +1,89 @@ +// +// NCCollectionViewCommonPinchGesture.swift +// Nextcloud +// +// Created by Marino Faggiana on 23/09/24. +// Copyright © 2024 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 Foundation +import UIKit + +extension NCCollectionViewCommon { + @objc func handlePinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard isLayoutPhoto else { return } + + func updateNumberOfColumns() { + let originalColumns = numberOfColumns + transitionColumns = true + + if currentScale < 1 && numberOfColumns < maxColumns { + numberOfColumns += 1 + } else if currentScale > 1 && numberOfColumns > 1 { + numberOfColumns -= 1 + } + + if originalColumns != numberOfColumns { + + self.collectionView.transform = .identity + self.currentScale = 1.0 + + UIView.transition(with: self.collectionView, duration: 0.20, options: .transitionCrossDissolve) { + self.collectionView.reloadData() + } completion: { _ in + + if let layoutForView = self.database.getLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewFiles, serverUrl: self.serverUrl) { + layoutForView.columnPhoto = self.numberOfColumns + self.database.setLayoutForView(layoutForView: layoutForView) + } + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.transitionColumns = false + } + } + + switch gestureRecognizer.state { + case .began: + lastScale = gestureRecognizer.scale + lastNumberOfColumns = numberOfColumns + case .changed: + guard !transitionColumns else { + return + } + let scale = gestureRecognizer.scale + let scaleChange = scale / lastScale + + currentScale *= scaleChange + currentScale = max(0.5, min(currentScale, 2.0)) + + updateNumberOfColumns() + + if numberOfColumns > 1 && numberOfColumns < maxColumns { + collectionView.transform = CGAffineTransform(scaleX: currentScale, y: currentScale) + } + + lastScale = scale + case .ended: + currentScale = 1.0 + collectionView.transform = .identity + default: + break + } + } +} diff --git a/iOSClient/Media/Cell/NCGridMediaCell.swift b/iOSClient/Media/Cell/NCMediaCell.swift similarity index 96% rename from iOSClient/Media/Cell/NCGridMediaCell.swift rename to iOSClient/Media/Cell/NCMediaCell.swift index 8ac250a915..1575f21f6d 100644 --- a/iOSClient/Media/Cell/NCGridMediaCell.swift +++ b/iOSClient/Media/Cell/NCMediaCell.swift @@ -1,5 +1,5 @@ // -// NCGridMediaCell.swift +// NCMediaCell.swift // Nextcloud // // Created by Marino Faggiana on 12/02/2019. @@ -23,7 +23,7 @@ import UIKit -class NCGridMediaCell: UICollectionViewCell { +class NCMediaCell: UICollectionViewCell { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageVisualEffect: UIVisualEffectView! diff --git a/iOSClient/Media/Cell/NCGridMediaCell.xib b/iOSClient/Media/Cell/NCMediaCell.xib similarity index 94% rename from iOSClient/Media/Cell/NCGridMediaCell.xib rename to iOSClient/Media/Cell/NCMediaCell.xib index 6c6588149c..c18e6656d8 100644 --- a/iOSClient/Media/Cell/NCGridMediaCell.xib +++ b/iOSClient/Media/Cell/NCMediaCell.xib @@ -1,15 +1,15 @@ - + - + - + diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index b505cd1c10..c00b19c6c5 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -96,8 +96,8 @@ extension NCMedia: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridMediaCell) else { - fatalError("Unable to dequeue NCGridMediaCell with identifier gridCell") + guard let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "mediaCell", for: indexPath) as? NCMediaCell) else { + fatalError("Unable to dequeue MediaCell with identifier mediaCell") } guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return cell } @@ -108,13 +108,14 @@ extension NCMedia: UICollectionViewDataSource { cell.imageItem.image = imageCache cell.date = metadata.date as Date cell.ocId = metadata.ocId + cell.imageStatus.image = nil - if metadata.isVideo { - cell.imageStatus.image = playImage - } else if metadata.isLivePhoto { - cell.imageStatus.image = livePhotoImage - } else { - cell.imageStatus.image = nil + if cell.imageItem.frame.width > 60 { + if metadata.isVideo { + cell.imageStatus.image = playImage + } else if metadata.isLivePhoto { + cell.imageStatus.image = livePhotoImage + } } if isEditMode, fileSelect.contains(metadata.ocId) { @@ -130,7 +131,7 @@ extension NCMedia: UICollectionViewDataSource { DispatchQueue.global(qos: .userInteractive).async { let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) DispatchQueue.main.async { - if let currentCell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, + if let currentCell = collectionView.cellForItem(at: indexPath) as? NCMediaCell, currentCell.ocId == metadata.ocId, let image { currentCell.imageItem.image = image currentCell.backgroundColor = .black @@ -138,8 +139,6 @@ extension NCMedia: UICollectionViewDataSource { } } } - } else { - print("[DEBUG] in cache, cost \(indexPath.row)") } return cell diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index a382a3c988..62e5b2d205 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -28,7 +28,7 @@ import RealmSwift extension NCMedia: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let metadata = dataSource.getMetadata(indexPath: indexPath), - let cell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell else { return } + let cell = collectionView.cellForItem(at: indexPath) as? NCMediaCell else { return } if isEditMode { if let index = fileSelect.firstIndex(of: metadata.ocId) { diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index ff3b1d5f2e..c87ab77f21 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -42,7 +42,7 @@ extension NCMedia { tabBarSelect.selectCount = fileSelect.count if let visibleCells = self.collectionView?.indexPathsForVisibleItems.compactMap({ self.collectionView?.cellForItem(at: $0) }) { - for case let cell as NCGridMediaCell in visibleCells { + for case let cell as NCMediaCell in visibleCells { cell.selected(false) } } @@ -65,7 +65,6 @@ extension NCMedia { } func setTitleDate() { - if let layoutAttributes = collectionView.collectionViewLayout.layoutAttributesForElements(in: collectionView.bounds) { let sortedAttributes = layoutAttributes.sorted { $0.frame.minY < $1.frame.minY || ($0.frame.minY == $1.frame.minY && $0.frame.minX < $1.frame.minX) } @@ -112,16 +111,19 @@ extension NCMedia { self.showOnlyImages = true self.showOnlyVideos = false self.loadDataSource() + self.networkRemoveAll() }, UIAction(title: NSLocalizedString("_media_viewvideo_show_", comment: ""), image: utility.loadImage(named: "video")) { _ in self.showOnlyImages = false self.showOnlyVideos = true self.loadDataSource() + self.networkRemoveAll() }, UIAction(title: NSLocalizedString("_media_show_all_", comment: ""), image: utility.loadImage(named: "photo.on.rectangle")) { _ in self.showOnlyImages = false self.showOnlyVideos = false self.loadDataSource() + self.searchMediaUI() } ]) diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 8237d57498..0c238b94e5 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -102,7 +102,7 @@ class NCMedia: UIViewController { collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: mediaSectionFooter, withReuseIdentifier: "sectionFooter") - collectionView.register(UINib(nibName: "NCGridMediaCell", bundle: nil), forCellWithReuseIdentifier: "gridCell") + collectionView.register(UINib(nibName: "NCMediaCell", bundle: nil), forCellWithReuseIdentifier: "mediaCell") collectionView.alwaysBounceVertical = true collectionView.contentInset = UIEdgeInsets(top: insetsTop, left: 0, bottom: 50, right: 0) collectionView.backgroundColor = .systemBackground @@ -262,13 +262,14 @@ class NCMedia: UIViewController { for index in indices { let indexPath = IndexPath(row: index, section: 0) - if let cell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell, + if let cell = collectionView.cellForItem(at: indexPath) as? NCMediaCell, dataSource.metadatas[index].ocId == cell.ocId { indexPaths.append(indexPath) } } dataSource.removeMetadata(ocIds) + if indexPaths.count == ocIds.count { collectionView.deleteItems(at: indexPaths) } else { @@ -288,12 +289,26 @@ class NCMedia: UIViewController { guard let userInfo = notification.userInfo as NSDictionary?, let ocId = userInfo["ocId"] as? String, let fileExists = userInfo["fileExists"] as? Bool else { return } + var indexPaths: [IndexPath] = [] filesExists.append(ocId) if !fileExists { + if let index = dataSource.metadatas.firstIndex(where: {$0.ocId == ocId}), + let cell = collectionView.cellForItem(at: IndexPath(row: index, section: 0)) as? NCMediaCell, + dataSource.metadatas[index].ocId == cell.ocId { + indexPaths.append(IndexPath(row: index, section: 0)) + } + dataSource.removeMetadata([ocId]) database.deleteMetadataOcId(ocId) + + if !indexPaths.isEmpty { + collectionView.deleteItems(at: indexPaths) + } else { + collectionViewReloadData() + } + collectionViewReloadData() } } diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index b1e7e12357..270f06481a 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -50,13 +50,15 @@ extension NCMedia { guard self.isViewActived, !self.hasRunSearchMedia, !self.isPinchGestureActive, + !self.showOnlyImages, + !self.showOnlyVideos, !isEditMode, NCNetworking.shared.downloadThumbnailQueue.operationCount == 0, let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { return } self.hasRunSearchMedia = true - let limit = max(collectionView.visibleCells.count * 2, 300) + let limit = max(collectionView.visibleCells.count * 3, 300) var lessDate = Date.distantFuture var greaterDate = Date.distantPast let countMetadatas = self.dataSource.metadatas.count @@ -70,7 +72,7 @@ extension NCMedia { if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }), !distant { - firstCellDate = (visibleCells.first as? NCGridMediaCell)?.date + firstCellDate = (visibleCells.first as? NCMediaCell)?.date if firstCellDate == self.dataSource.metadatas.first?.date { lessDate = Date.distantFuture } else { @@ -81,7 +83,7 @@ extension NCMedia { } } - lastCellDate = (visibleCells.last as? NCGridMediaCell)?.date + lastCellDate = (visibleCells.last as? NCMediaCell)?.date if lastCellDate == self.dataSource.metadatas.last?.date { greaterDate = Date.distantPast } else { @@ -106,7 +108,7 @@ extension NCMedia { account: self.session.account, options: options) { account, files, _, error in - if error == .success, let files, self.session.account == account { + if error == .success, let files, self.session.account == account, !self.showOnlyImages, !self.showOnlyVideos { /// Removes all files in `files` that have an `ocId` present in `fileDeleted` var files = files diff --git a/iOSClient/Media/NCMediaDownloadThumbnail.swift b/iOSClient/Media/NCMediaDownloadThumbnail.swift index 89d45c72ee..547242db23 100644 --- a/iOSClient/Media/NCMediaDownloadThumbnail.swift +++ b/iOSClient/Media/NCMediaDownloadThumbnail.swift @@ -56,7 +56,7 @@ class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { let image = NCUtility().getImage(ocId: self.metadata.ocId, etag: self.metadata.etag, ext: NCGlobal.shared.getSizeExtension(column: self.media.numberOfColumns)) DispatchQueue.main.async { - for case let cell as NCGridMediaCell in self.media.collectionView.visibleCells { + for case let cell as NCMediaCell in self.media.collectionView.visibleCells { if cell.ocId == self.metadata.ocId { UIView.transition(with: cell.imageItem, duration: 0.75, options: .transitionCrossDissolve, animations: { cell.imageItem.image = image }, completion: nil) From a0ae82cab2ab6acd0c3e5f41548e23f9ea609b08 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 23 Sep 2024 18:07:44 +0200 Subject: [PATCH 20/31] build 17 Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index a0d543ee0f..ecb2208f69 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5474,7 +5474,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5540,7 +5540,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; From 10cee167cdf194c3d8bd860cdf8243189d4d86c3 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 12:12:13 +0200 Subject: [PATCH 21/31] Cell (#3073) * cod Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana * cleaning Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 +- .../Collection Common/Cell/NCGridCell.swift | 2 +- .../Collection Common/Cell/NCPhotoCell.swift | 3 +- ...nViewCommon+CollectionViewDataSource.swift | 207 +++++++++--------- .../NCCollectionViewCommon.swift | 12 - 5 files changed, 109 insertions(+), 119 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index ecb2208f69..1fcd720e72 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -2309,10 +2309,10 @@ children = ( 370D26AE248A3D7A00121797 /* NCCellProtocol.swift */, F751247A2C42919C00E63DB8 /* NCPhotoCell.swift */, - F751247B2C42919C00E63DB8 /* NCPhotoCell.xib */, F78ACD3F21903CC20088454D /* NCGridCell.swift */, - F78ACD4521903D010088454D /* NCGridCell.xib */, F78ACD4121903CE00088454D /* NCListCell.swift */, + F751247B2C42919C00E63DB8 /* NCPhotoCell.xib */, + F78ACD4521903D010088454D /* NCGridCell.xib */, F78ACD4321903CF20088454D /* NCListCell.xib */, ); path = Cell; diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 16adc3cc42..f7df1fc986 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -38,7 +38,7 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto var ocId = "" var ocIdTransfer = "" var account = "" - private var user = "" + var user = "" weak var gridCellDelegate: NCGridCellDelegate? diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift index 4d1c8d1479..c07ab52f46 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift @@ -33,7 +33,8 @@ class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProt var ocId = "" var ocIdTransfer = "" - private var user = "" + var user = "" + weak var photoCellDelegate: NCPhotoCellDelegate? var fileOcId: String? { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 1f88511ccc..dcc17b4535 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -35,97 +35,14 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let metadata = self.dataSource.getMetadata(indexPath: indexPath), - let cell = (cell as? NCCellProtocol) else { return } - let existsPreview = utility.existsImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return } + let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) let ext = global.getSizeExtension(column: self.numberOfColumns) - func downloadAvatar(fileName: String, user: String, dispalyName: String?) { - if let image = database.getImageAvatarLoaded(fileName: fileName) { - cell.fileAvatarImageView?.contentMode = .scaleAspectFill - cell.fileAvatarImageView?.image = image - } else { - NCNetworking.shared.downloadAvatar(user: user, dispalyName: dispalyName, fileName: fileName, account: metadata.account, cell: cell, view: collectionView) - } - } - /// CONTENT MODE - cell.filePreviewImageView?.layer.borderWidth = 0 - if isLayoutPhoto { - if metadata.isImageOrVideo, existsPreview { - cell.filePreviewImageView?.contentMode = .scaleAspectFill - } else { - cell.filePreviewImageView?.contentMode = .scaleAspectFit - } - } else { - if existsPreview { - cell.filePreviewImageView?.contentMode = .scaleAspectFill - } else { - cell.filePreviewImageView?.contentMode = .scaleAspectFit - } - } - cell.fileAvatarImageView?.contentMode = .center - /// THUMBNAIL - if !metadata.directory { - if metadata.hasPreviewBorder { - cell.filePreviewImageView?.layer.borderWidth = 0.2 - cell.filePreviewImageView?.layer.borderColor = UIColor.lightGray.cgColor - } - - if metadata.name == global.appName { - - if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - cell.filePreviewImageView?.image = image - } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - cell.filePreviewImageView?.image = image - } - - if cell.filePreviewImageView?.image == nil { - if metadata.iconName.isEmpty { - cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() - } else { - cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) - } - if metadata.hasPreview && metadata.status == global.metadataStatusNormal && !existsPreview { - for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { return } - NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: NCGlobal.shared.getSizeExtension(column: self.numberOfColumns))) - } - } - } else { - /// APP NAME - UNIFIED SEARCH - switch metadata.iconName { - case let str where str.contains("contacts"): - cell.filePreviewImageView?.image = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor]) - case let str where str.contains("conversation"): - cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) - case let str where str.contains("calendar"): - cell.filePreviewImageView?.image = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor]) - case let str where str.contains("deck"): - cell.filePreviewImageView?.image = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor]) - case let str where str.contains("mail"): - cell.filePreviewImageView?.image = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor]) - case let str where str.contains("talk"): - cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) - case let str where str.contains("confirm"): - cell.filePreviewImageView?.image = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor]) - case let str where str.contains("pages"): - cell.filePreviewImageView?.image = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor]) - default: - cell.filePreviewImageView?.image = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor]) - } - if !metadata.iconUrl.isEmpty { - if let ownerId = getAvatarFromIconUrl(metadata: metadata) { - let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: ownerId) - downloadAvatar(fileName: fileName, user: ownerId, dispalyName: nil) - } - } - } - } - /// AVATAR - if !metadata.ownerId.isEmpty, - metadata.ownerId != metadata.userId { - // appDelegate.account == metadata.account { - let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) - downloadAvatar(fileName: fileName, user: metadata.ownerId, dispalyName: metadata.ownerDisplayName) + if metadata.hasPreview, + !existsImagePreview, + NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnail)?.metadata.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: ext)) } } @@ -145,6 +62,14 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { var isMounted = false var a11yValues: [String] = [] let metadata = self.dataSource.getMetadata(indexPath: indexPath) ?? tableMetadata() + let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) + let ext = global.getSizeExtension(column: self.numberOfColumns) + + defer { + if !metadata.isSharable() || NCCapabilities.shared.disableSharesView(account: metadata.account) { + cell.hideButtonShare(true) + } + } // LAYOUT PHOTO if isLayoutPhoto { @@ -168,14 +93,20 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { listCell.listCellDelegate = self cell = listCell } - guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return cell } - defer { - if !metadata.isSharable() || NCCapabilities.shared.disableSharesView(account: metadata.account) { - cell.hideButtonShare(true) - } + /// CONTENT MODE + cell.fileAvatarImageView?.contentMode = .center + cell.filePreviewImageView?.layer.borderWidth = 0 + + if existsImagePreview { + cell.filePreviewImageView?.contentMode = .scaleAspectFill + } else { + cell.filePreviewImageView?.contentMode = .scaleAspectFit + } + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return cell } + if metadataFolder != nil { isShare = metadata.permissions.contains(permissions.permissionShared) && !metadataFolder!.permissions.contains(permissions.permissionShared) isMounted = metadata.permissions.contains(permissions.permissionMounted) && !metadataFolder!.permissions.contains(permissions.permissionMounted) @@ -259,6 +190,61 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { // color folder cell.filePreviewImageView?.image = cell.filePreviewImageView?.image?.colorizeFolder(metadata: metadata, tableDirectory: tableDirectory) } else { + + if metadata.hasPreviewBorder { + cell.filePreviewImageView?.layer.borderWidth = 0.2 + cell.filePreviewImageView?.layer.borderColor = UIColor.lightGray.cgColor + } + + if metadata.name == global.appName { + if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.filePreviewImageView?.image = image + } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.filePreviewImageView?.image = image + } + + if cell.filePreviewImageView?.image == nil { + if metadata.iconName.isEmpty { + cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + } else { + cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + } + } + } else { + /// APP NAME - UNIFIED SEARCH + switch metadata.iconName { + case let str where str.contains("contacts"): + cell.filePreviewImageView?.image = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("conversation"): + cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + case let str where str.contains("calendar"): + cell.filePreviewImageView?.image = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("deck"): + cell.filePreviewImageView?.image = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("mail"): + cell.filePreviewImageView?.image = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("talk"): + cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + case let str where str.contains("confirm"): + cell.filePreviewImageView?.image = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("pages"): + cell.filePreviewImageView?.image = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor]) + default: + cell.filePreviewImageView?.image = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor]) + } + if !metadata.iconUrl.isEmpty { + if let ownerId = getAvatarFromIconUrl(metadata: metadata) { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: ownerId) + if let image = database.getImageAvatarLoaded(fileName: fileName) { + cell.fileAvatarImageView?.contentMode = .scaleAspectFill + cell.fileAvatarImageView?.image = image + } else { + NCNetworking.shared.downloadAvatar(user: ownerId, dispalyName: nil, fileName: fileName, account: metadata.account, cell: cell, view: collectionView) + } + } + } + } + let tableLocalFile = database.getResultsTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId))?.first // image local if let tableLocalFile, tableLocalFile.offline { @@ -328,6 +314,18 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { break } + // AVATAR + if !metadata.ownerId.isEmpty, + metadata.ownerId != metadata.userId { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) + if let image = database.getImageAvatarLoaded(fileName: fileName) { + cell.fileAvatarImageView?.contentMode = .scaleAspectFill + cell.fileAvatarImageView?.image = image + } else { + NCNetworking.shared.downloadAvatar(user: metadata.ownerId, dispalyName: metadata.ownerDisplayName, fileName: fileName, account: metadata.account, cell: cell, view: collectionView) + } + } + // URL if metadata.classFile == NKCommon.TypeClassFile.url.rawValue { cell.fileLocalImage?.image = nil @@ -371,15 +369,18 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { cell.setTags(tags: Array(metadata.tags)) // Layout photo - if isLayoutPhoto, sizeImage.width < 120 { - cell.hideImageFavorite(true) - cell.hideImageLocal(true) - cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 10) - if sizeImage.width < 100 { - cell.hideImageItem(true) - cell.hideButtonMore(true) - cell.hideLabelInfo(true) - cell.hideLabelSubinfo(true) + if isLayoutPhoto { + let width = UIScreen.main.bounds.width / CGFloat(self.numberOfColumns) + if width < 120 { + cell.hideImageFavorite(true) + cell.hideImageLocal(true) + cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 10) + if width < 100 { + cell.hideImageItem(true) + cell.hideButtonMore(true) + cell.hideLabelInfo(true) + cell.hideLabelSubinfo(true) + } } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 3aa45e0848..f71d75880c 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -114,18 +114,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS layoutForView?.layout == global.layoutList ? " - " : "" } - var sizeImage: CGSize { - if isLayoutPhoto { - let column = CGFloat(layoutForView?.columnPhoto ?? 3) - return CGSize(width: collectionView.frame.width / column, height: collectionView.frame.width / column) - } else if isLayoutGrid { - let column = CGFloat(layoutForView?.columnGrid ?? 3) - return CGSize(width: collectionView.frame.width / column, height: collectionView.frame.width / column) - } else { - return CGSize(width: 40, height: 40) - } - } - var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } From 04ede45b009a536fbaba8d0f2ccc568f26ff4338 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 14:45:23 +0200 Subject: [PATCH 22/31] import TOPasscodeViewController code Signed-off-by: Marino Faggiana --- Cartfile | 1 - Cartfile.resolved | 1 - Nextcloud.xcodeproj/project.pbxproj | 200 ++++- Share/NCShareExtension.swift | 1 - Share/Share-Bridging-Header.h | 1 + iOSClient/Main/NCPasscode.swift | 1 - iOSClient/Nextcloud-Bridging-Header.h | 1 + iOSClient/SceneDelegate.swift | 1 - .../Settings/E2EE/NCManageE2EEModel.swift | 1 - .../Settings/Settings/NCSettingsModel.swift | 1 - .../Models/TOPasscodeCircleImage.h | 57 ++ .../Models/TOPasscodeCircleImage.m | 75 ++ .../Models/TOPasscodeViewContentLayout.h | 100 +++ .../Models/TOPasscodeViewContentLayout.m | 205 +++++ ...scodeViewControllerAnimatedTransitioning.h | 60 ++ ...scodeViewControllerAnimatedTransitioning.m | 120 +++ .../Models/TOSettingsKeypadImage.h | 54 ++ .../Models/TOSettingsKeypadImage.m | 186 +++++ .../TOPasscodeViewControllerConstants.h | 71 ++ .../TOPasscodeSettingsViewController.h | 108 +++ .../TOPasscodeSettingsViewController.m | 630 ++++++++++++++++ .../TOPasscodeViewController.h | 163 ++++ .../TOPasscodeViewController.m | 710 ++++++++++++++++++ .../Views/Main/TOPasscodeCircleButton.h | 81 ++ .../Views/Main/TOPasscodeCircleButton.m | 236 ++++++ .../Views/Main/TOPasscodeKeypadView.h | 101 +++ .../Views/Main/TOPasscodeKeypadView.m | 432 +++++++++++ .../Views/Main/TOPasscodeView.h | 136 ++++ .../Views/Main/TOPasscodeView.m | 690 +++++++++++++++++ .../Settings/TOPasscodeSettingsKeypadButton.h | 45 ++ .../Settings/TOPasscodeSettingsKeypadButton.m | 103 +++ .../Settings/TOPasscodeSettingsKeypadView.h | 74 ++ .../Settings/TOPasscodeSettingsKeypadView.m | 395 ++++++++++ .../Settings/TOPasscodeSettingsWarningLabel.h | 43 ++ .../Settings/TOPasscodeSettingsWarningLabel.m | 154 ++++ .../Views/Shared/TOPasscodeButtonLabel.h | 61 ++ .../Views/Shared/TOPasscodeButtonLabel.m | 186 +++++ .../Views/Shared/TOPasscodeCircleView.h | 46 ++ .../Views/Shared/TOPasscodeCircleView.m | 89 +++ .../Views/Shared/TOPasscodeFixedInputView.h | 54 ++ .../Views/Shared/TOPasscodeFixedInputView.m | 171 +++++ .../Views/Shared/TOPasscodeInputField.h | 111 +++ .../Views/Shared/TOPasscodeInputField.m | 423 +++++++++++ .../Shared/TOPasscodeVariableInputView.h | 55 ++ .../Shared/TOPasscodeVariableInputView.m | 254 +++++++ 45 files changed, 6675 insertions(+), 13 deletions(-) create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h create mode 100755 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m diff --git a/Cartfile b/Cartfile index 3946734806..315cc9aec0 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1 @@ -github "https://github.com/marinofaggiana/TOPasscodeViewController" "master" binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.5.1 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index a19f7cdae1..1b44f3b861 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1 @@ binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" "3.6.0" -github "marinofaggiana/TOPasscodeViewController" "ed795637acd2b1ef154e011a04ebab4686d0523c" diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 1fcd720e72..ed0bdd1b44 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -324,7 +324,6 @@ F7346E2328B0FEBA006CE2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */; }; F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */ = {isa = PBXBuildFile; productRef = F734B06528E75C0100E180D5 /* TLPhotoPicker */; }; F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */; }; - F737DA992B7B864E0063BAFC /* TOPasscodeViewController.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; }; F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F737DA9F2B7B8AB90063BAFC /* NCLoginNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738D48F2756740100CD1D38 /* NCLoginNavigationController.swift */; }; @@ -585,8 +584,6 @@ F76D3CF12428B40E005DFA87 /* NCViewerPDFSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76D3CF02428B40E005DFA87 /* NCViewerPDFSearch.swift */; }; F76D3CF32428B94E005DFA87 /* NCViewerPDFSearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF22428B94E005DFA87 /* NCViewerPDFSearchCell.xib */; }; F76D3CF52428D0C1005DFA87 /* NCViewerPDF.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF42428D0C0005DFA87 /* NCViewerPDF.storyboard */; }; - F76DA95B277B75A90082465B /* TOPasscodeViewController.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; }; - F76DA95C277B75A90082465B /* TOPasscodeViewController.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F76DA963277B760E0082465B /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F76DA962277B760E0082465B /* Queuer */; }; F76DA969277B77EA0082465B /* DropDown in Frameworks */ = {isa = PBXBuildFile; productRef = F76DA968277B77EA0082465B /* DropDown */; }; F76DEE9728F808AF0041B1C9 /* LockscreenData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76DEE9428F808AF0041B1C9 /* LockscreenData.swift */; }; @@ -852,6 +849,40 @@ F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */; }; F7D1C4AC2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */; }; F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; + F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; + F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; + F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; + F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; + F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; + F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; + F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; + F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; + F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; + F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; + F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; + F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; + F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; + F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; + F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; + F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; + F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; + F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; + F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; + F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; + F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; + F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; + F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; + F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; + F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; + F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; + F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; + F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; + F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; + F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; + F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; + F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; + F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; + F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D57C8626317BDA00DE301D /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7CA212C25F1333200826ABB /* NCAccountRequest.storyboard */; }; F7D57C8B26317BDE00DE301D /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CA212B25F1333200826ABB /* NCAccountRequest.swift */; }; @@ -1080,7 +1111,6 @@ files = ( F7A509262C26D95D00326106 /* RealmSwift in Embed Frameworks */, F78AF1E82BE938C100F3F060 /* MobileVLCKit.xcframework in Embed Frameworks */, - F76DA95C277B75A90082465B /* TOPasscodeViewController.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1698,6 +1728,41 @@ F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDataSourcePrefetching.swift"; sourceTree = ""; }; F7D2C772246470CA008513AE /* XLForm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XLForm.framework; path = Carthage/Build/iOS/XLForm.framework; sourceTree = ""; }; F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; + F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleImage.h; sourceTree = ""; }; + F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleImage.m; sourceTree = ""; }; + F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewContentLayout.h; sourceTree = ""; }; + F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewContentLayout.m; sourceTree = ""; }; + F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerAnimatedTransitioning.h; sourceTree = ""; }; + F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewControllerAnimatedTransitioning.m; sourceTree = ""; }; + F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOSettingsKeypadImage.h; sourceTree = ""; }; + F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOSettingsKeypadImage.m; sourceTree = ""; }; + F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerConstants.h; sourceTree = ""; }; + F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleButton.h; sourceTree = ""; }; + F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleButton.m; sourceTree = ""; }; + F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeKeypadView.h; sourceTree = ""; }; + F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeKeypadView.m; sourceTree = ""; }; + F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeView.h; sourceTree = ""; }; + F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeView.m; sourceTree = ""; }; + F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadButton.h; sourceTree = ""; }; + F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadButton.m; sourceTree = ""; }; + F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadView.h; sourceTree = ""; }; + F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadView.m; sourceTree = ""; }; + F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsWarningLabel.h; sourceTree = ""; }; + F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsWarningLabel.m; sourceTree = ""; }; + F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeButtonLabel.h; sourceTree = ""; }; + F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeButtonLabel.m; sourceTree = ""; }; + F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleView.h; sourceTree = ""; }; + F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleView.m; sourceTree = ""; }; + F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeFixedInputView.h; sourceTree = ""; }; + F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeFixedInputView.m; sourceTree = ""; }; + F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeInputField.h; sourceTree = ""; }; + F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeInputField.m; sourceTree = ""; }; + F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeVariableInputView.h; sourceTree = ""; }; + F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeVariableInputView.m; sourceTree = ""; }; + F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsViewController.h; sourceTree = ""; }; + F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsViewController.m; sourceTree = ""; }; + F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewController.h; sourceTree = ""; }; + F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewController.m; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; @@ -1832,7 +1897,6 @@ F74C863D2AEFBFD9009A1D4A /* LRUCache in Frameworks */, F72AD70F28C24BA1006CB92D /* NextcloudKit in Frameworks */, F33EE6E72BF4C02600CA1A51 /* NIOSSL in Frameworks */, - F737DA992B7B864E0063BAFC /* TOPasscodeViewController.xcframework in Frameworks */, F72CD01227A7E92400E59476 /* JGProgressHUD in Frameworks */, F77CB6A92AA08053000C3CA4 /* OpenSSL in Frameworks */, F760DE092AE66ED00027D78A /* KeychainAccess in Frameworks */, @@ -1894,7 +1958,6 @@ F758A01227A7F03E0069468B /* JGProgressHUD in Frameworks */, F77333882927A72100466E35 /* OpenSSL in Frameworks */, F753BA93281FD8020015BFB6 /* EasyTipView in Frameworks */, - F76DA95B277B75A90082465B /* TOPasscodeViewController.xcframework in Frameworks */, F7160A822BE933390034DCB3 /* RealmSwift in Frameworks */, F76DA963277B760E0082465B /* Queuer in Frameworks */, F72AD70D28C24B93006CB92D /* NextcloudKit in Frameworks */, @@ -2759,6 +2822,7 @@ isa = PBXGroup; children = ( F702F2FC25EE5D2C008F8E80 /* NYMnemonic */, + F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */, F76D364528A4F8BF00214537 /* NCActivityIndicator.swift */, F733598025C1C188002ABA72 /* NCAskAuthorization.swift */, F77C97382953131000FDDD09 /* NCCameraRoll.swift */, @@ -2873,6 +2937,96 @@ path = More; sourceTree = ""; }; + F7D4BF0A2CA2E8D800A5E746 /* Models */ = { + isa = PBXGroup; + children = ( + F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */, + F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */, + F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */, + F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */, + F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */, + F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */, + F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */, + F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */, + ); + path = Models; + sourceTree = ""; + }; + F7D4BF0C2CA2E8D800A5E746 /* Supporting */ = { + isa = PBXGroup; + children = ( + F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */, + ); + path = Supporting; + sourceTree = ""; + }; + F7D4BF132CA2E8D800A5E746 /* Main */ = { + isa = PBXGroup; + children = ( + F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */, + F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */, + F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */, + F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */, + F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */, + F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */, + ); + path = Main; + sourceTree = ""; + }; + F7D4BF1A2CA2E8D800A5E746 /* Settings */ = { + isa = PBXGroup; + children = ( + F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */, + F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */, + F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */, + F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */, + F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */, + F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */, + ); + path = Settings; + sourceTree = ""; + }; + F7D4BF252CA2E8D800A5E746 /* Shared */ = { + isa = PBXGroup; + children = ( + F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */, + F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */, + F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */, + F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */, + F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */, + F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */, + F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */, + F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */, + F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */, + F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */, + ); + path = Shared; + sourceTree = ""; + }; + F7D4BF262CA2E8D800A5E746 /* Views */ = { + isa = PBXGroup; + children = ( + F7D4BF132CA2E8D800A5E746 /* Main */, + F7D4BF1A2CA2E8D800A5E746 /* Settings */, + F7D4BF252CA2E8D800A5E746 /* Shared */, + ); + path = Views; + sourceTree = ""; + }; + F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */ = { + isa = PBXGroup; + children = ( + F7D4BF0A2CA2E8D800A5E746 /* Models */, + F7D4BF0C2CA2E8D800A5E746 /* Supporting */, + F7D4BF262CA2E8D800A5E746 /* Views */, + F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */, + F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */, + F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */, + F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */, + ); + path = TOPasscodeViewController; + sourceTree = ""; + }; F7DFB7E9219C5A0500680748 /* Create cloud */ = { isa = PBXGroup; children = ( @@ -4042,6 +4196,23 @@ F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */, F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */, + F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, + F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, + F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, + F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, + F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, + F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, + F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, + F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, + F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, + F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, + F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, + F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, + F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, + F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, + F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, + F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F74AF3A5247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */, AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */, @@ -4376,6 +4547,23 @@ F724377B2C10B83E00C7C68D /* NCPermissions.swift in Sources */, F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */, F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, + F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, + F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, + F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, + F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, + F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, + F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, + F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, + F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, + F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, + F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, + F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, + F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, + F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, + F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, + F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, + F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */, F78ACD4A21903F850088454D /* NCTrashListCell.swift in Sources */, F7A51E722C721FC00037BCC0 /* NCTransfersProgress.swift in Sources */, diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index 7fdd991270..cae085e73c 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -25,7 +25,6 @@ import UIKit import NextcloudKit -import TOPasscodeViewController enum NCShareExtensionError: Error { case cancel, fileUpload, noAccount, noFiles diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h index c19b600db6..d2918d26c8 100644 --- a/Share/Share-Bridging-Header.h +++ b/Share/Share-Bridging-Header.h @@ -4,3 +4,4 @@ #import "NCEndToEndEncryption.h" #import "UIImage+animatedGIF.h" +#import "TOPasscodeViewController.h" diff --git a/iOSClient/Main/NCPasscode.swift b/iOSClient/Main/NCPasscode.swift index e5155b375b..4590e7b11f 100644 --- a/iOSClient/Main/NCPasscode.swift +++ b/iOSClient/Main/NCPasscode.swift @@ -23,7 +23,6 @@ import UIKit import LocalAuthentication -import TOPasscodeViewController public protocol NCPasscodeDelegate: AnyObject { func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) diff --git a/iOSClient/Nextcloud-Bridging-Header.h b/iOSClient/Nextcloud-Bridging-Header.h index 7797b17220..7a3e46eb78 100644 --- a/iOSClient/Nextcloud-Bridging-Header.h +++ b/iOSClient/Nextcloud-Bridging-Header.h @@ -6,3 +6,4 @@ #import "NYMnemonic.h" #import "UIImage+animatedGIF.h" #import "NCPushNotificationEncryption.h" +#import "TOPasscodeViewController.h" diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index 676a81f755..678d25d758 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -26,7 +26,6 @@ import UIKit import NextcloudKit import WidgetKit import SwiftEntryKit -import TOPasscodeViewController class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? diff --git a/iOSClient/Settings/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/Settings/E2EE/NCManageE2EEModel.swift index a039b091a5..11d7ad4cac 100644 --- a/iOSClient/Settings/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/Settings/E2EE/NCManageE2EEModel.swift @@ -24,7 +24,6 @@ import UIKit import SwiftUI import NextcloudKit -import TOPasscodeViewController import LocalAuthentication class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEndInitializeDelegate, TOPasscodeViewControllerDelegate { diff --git a/iOSClient/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index b0be288994..ee25dfe4cd 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -25,7 +25,6 @@ import Foundation import UIKit import SwiftUI -import TOPasscodeViewController import LocalAuthentication class NCSettingsModel: ObservableObject, ViewOnAppearHandling { diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h new file mode 100644 index 0000000000..6000f1fca6 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h @@ -0,0 +1,57 @@ +// +// TOPasscodeCircleImage.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A subclass of `UIImage` that can procedurally generate both hollow and full circle graphics at any size. + These are used for the 'normal' and 'tapped' states of the passcode circle buttons. + */ +@interface TOPasscodeCircleImage : UIImage + +/** + Generates and returns a `UIImage` of a filled circle at the specified size. + + @param size The diameter of the final circle image + @param inset An inset value that will shrink the size of the circle. This is so it can be overlaid on a hollow circle + without interfering with the anti-aliasing on the outer border. + @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. + Setting this value will increase the dimensions of the final `UIImage`. + @param antialias Whether the circle boundary will be antialiased (Since antialiasing is unnecessary if this circle will overlay another.) + */ ++ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias; + +/** + Generates and returns a `UIImage` of a hollow circle at the specified size. + + @param size The diameter of the final circle image + @param strokeWidth The thickness, in points, of the stroke making up the circle image. + @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. + Setting this value will increase the dimensions of the final `UIImage`. + */ ++ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m new file mode 100644 index 0000000000..4288a33092 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m @@ -0,0 +1,75 @@ +// +// TOPasscodeCircleImage.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleImage.h" + +@implementation TOPasscodeCircleImage + ++ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias +{ + UIImage *image = nil; + CGSize imageSize = (CGSize){size + (padding * 2), size + (padding * 2)}; + + UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0f); + { + CGContextRef context = UIGraphicsGetCurrentContext(); + + if (!antialias) { + CGContextSetShouldAntialias(context, NO); + } + + CGRect rect = (CGRect){padding + inset, padding + inset, size - (inset * 2), size - (inset * 2)}; + UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect]; + [[UIColor blackColor] setFill]; + [ovalPath fill]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + ++ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding +{ + UIImage *image = nil; + CGSize canvasSize = (CGSize){size + (padding * 2), size + (padding * 2)}; + CGSize circleSize = (CGSize){size, size}; + + UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); + { + CGRect circleRect = (CGRect){{padding, padding}, circleSize}; + circleRect = CGRectInset(circleRect, (strokeWidth * 0.5f), (strokeWidth * 0.5f)); + + UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect]; + [[UIColor blackColor] setStroke]; + path.lineWidth = strokeWidth; + [path stroke]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h new file mode 100644 index 0000000000..97191fd6ae --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h @@ -0,0 +1,100 @@ +// +// TOPasscodeViewContentLayout.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import + +/** + Depending on the width of the application window, all of the content views in + the passcode view need to be resized in order to fit in the available space. + + This means that not only does the spacing and sizing of views need to be changed, but + image assets need to be regenerated and font sizes need to change as well. + + This class assumes there will be three major screen sizes, and provides layout + sizes, spacing, and styles in order to resize the passcode view for each one. + + The three screen styles it supports are: + + * Small Screens - iPhone 5/ or iPad 9.7" in 1/4 split screen mode + * Medium Screens - iPhone 6/ or iPad 12.9" in 1/4 split screen mode + * Large Screens - iPhone 6 Plus and all iPads when not in split screen mode. + + */ +@interface TOPasscodeViewContentLayout : NSObject + +/* The width of the PIN view in which this layout object is sizing the content to fit. */ +@property (nonatomic, assign) CGFloat viewWidth; + +/* Extra padding at the bottom in order to shift the content slightly up */ +@property (nonatomic, assign) CGFloat bottomPadding; + +/* The title view at the very top */ +@property (nonatomic, assign) CGFloat titleViewBottomSpacing; // Space from the bottom of the title view to the title label + +/* The Title Label Explaining the Passcode View */ +@property (nonatomic, assign) CGFloat titleLabelBottomSpacing; // Space from the title label to the input view +@property (nonatomic, assign) CGFloat subtitleLabelBottomSpacing; // Space from the subtitle label to the input view + +@property (nonatomic, strong) UIFont *titleLabelFont; // The font of the title label +@property (nonatomic, strong) UIFont *subtitleLabelFont; // The font of the subtitle label + +/* Title Label properties when the view is laid out horizontally */ +@property (nonatomic, assign) CGFloat titleHorizontalLayoutWidth; // When laid out horizontally, the width of the title view +@property (nonatomic, assign) CGFloat titleHorizontalLayoutSpacing; // The amount of spacing between the title label and the passcode keypad +@property (nonatomic, assign) CGFloat titleViewHorizontalBottomSpacing; // Space from the bottom of the title view when iPhone is horizontal +@property (nonatomic, assign) CGFloat titleLabelHorizontalBottomSpacing; // Spacing from the title label to input view in horizontal mode +@property (nonatomic, assign) CGFloat subtitleLabelHorizontalBottomSpacing; // Spacing from the subtitle label to input view in horizontal mode + +/* Circle Row Configuration */ +@property (nonatomic, assign) CGFloat circleRowDiameter; // The diameter of each circle representing a PIN number +@property (nonatomic, assign) CGFloat circleRowSpacing; // The spacing between each circle +@property (nonatomic, assign) CGFloat circleRowBottomSpacing; // Space between the view used to indicate input + +/* Text Field Configuration */ +@property (nonatomic, assign) CGFloat textFieldBorderThickness; // The thickness of the border stroke +@property (nonatomic, assign) CGFloat textFieldBorderRadius; // The corner radius of the border +@property (nonatomic, assign) CGFloat textFieldCircleDiameter; // The size of the circles in the passcode field +@property (nonatomic, assign) CGFloat textFieldCircleSpacing; // The amount of spacing between each circle +@property (nonatomic, assign) CGSize textFieldBorderPadding; // The amount of padding between the circles and the border +@property (nonatomic, assign) NSInteger textFieldNumericCharacterLength; // The amount of circles to have in this field when set to numeric +@property (nonatomic, assign) NSInteger textFieldAlphanumericCharacterLength; // The amount of circles to have in this field when set to alphanumeric +@property (nonatomic, assign) CGFloat submitButtonFontSize; // The font size of the 'OK' button +@property (nonatomic, assign) CGFloat submitButtonSpacing; // The spacing of the 'OK' button from the input + +/* Circle Button Shape and Layout */ +@property (nonatomic, assign) CGFloat circleButtonDiameter; // The size of each PIN button +@property (nonatomic, assign) CGSize circleButtonSpacing; // The vertical/horizontal spacing between buttons +@property (nonatomic, assign) CGFloat circleButtonStrokeWidth; // The thickness of the border line + +/* Circle Button Label */ +@property (nonatomic, strong) UIFont *circleButtonTitleLabelFont; // The font used for the '1' number labels +@property (nonatomic, strong) UIFont *circleButtonLetteringLabelFont; // The font used for the 'ABC' labels +@property (nonatomic, assign) CGFloat circleButtonLabelSpacing; // The vertical spacing between the number and lettering labels +@property (nonatomic, assign) CGFloat circleButtonLetteringSpacing; // The spacing between the 'ABC' characters + +/* Default layout configurations for the various sizes */ ++ (TOPasscodeViewContentLayout *)defaultScreenContentLayout; /* Default layout values. Designed for iPhone 6 Plus and above. */ ++ (TOPasscodeViewContentLayout *)mediumScreenContentLayout; /* For medium screen sizes, like iPhone 6, or 1/4 view on iPad Pro. */ ++ (TOPasscodeViewContentLayout *)smallScreenContentLayout; /* For the smallest screens, like iPhone SE, and 1/4 on standard size iPads/ */ + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m new file mode 100644 index 0000000000..5e8029133c --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m @@ -0,0 +1,205 @@ +// +// TOPasscodeViewContentLayout.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewContentLayout.h" + +@implementation TOPasscodeViewContentLayout + ++ (TOPasscodeViewContentLayout *)defaultScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 414.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 25.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 34.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelBottomSpacing = 34.0f; + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 22.0f]; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 250.0f; + contentLayout.titleHorizontalLayoutSpacing = 35.0f; + contentLayout.titleViewHorizontalBottomSpacing = 20.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 20.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 15.5f; + contentLayout.circleRowSpacing = 30.0f; + contentLayout.circleRowBottomSpacing = 35.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 17.0f]; + contentLayout.subtitleLabelBottomSpacing = 40.0f; + contentLayout.subtitleLabelHorizontalBottomSpacing = 40.0f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 10.0f; + contentLayout.textFieldCircleSpacing = 6.0f; + contentLayout.textFieldBorderPadding = (CGSize){10, 10}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + contentLayout.submitButtonFontSize = 17.0f; + contentLayout.submitButtonSpacing = 4.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 90.0f; + contentLayout.circleButtonSpacing = (CGSize){25.0f, 16.0f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 6.0f; + contentLayout.circleButtonLetteringSpacing = 3.0f; + + return contentLayout; +} + ++ (TOPasscodeViewContentLayout *)mediumScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 375.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 17.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 20.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 19.0f]; + contentLayout.titleLabelBottomSpacing = 23.0f; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 185.0f; + contentLayout.titleHorizontalLayoutSpacing = 16.0f; + contentLayout.titleViewHorizontalBottomSpacing = 18.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 13.5f; + contentLayout.circleRowSpacing = 26.0f; + contentLayout.circleRowBottomSpacing = 21.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 14.0f]; + contentLayout.subtitleLabelHorizontalBottomSpacing = 20.0f; + contentLayout.subtitleLabelBottomSpacing = 20.0f; + + /* Submit Button */ + contentLayout.submitButtonFontSize = 16.0f; + contentLayout.submitButtonSpacing = 4.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 80.0f; + contentLayout.circleButtonSpacing = (CGSize){28.0f, 15.0f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 9.0f; + contentLayout.textFieldCircleSpacing = 5.0f; + contentLayout.textFieldBorderPadding = (CGSize){10, 10}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:36.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:8.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 5.0f; + contentLayout.circleButtonLetteringSpacing = 2.5f; + + return contentLayout; +} + ++ (TOPasscodeViewContentLayout *)smallScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 320.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 12.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 15.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 16.0f]; + contentLayout.titleLabelBottomSpacing = 19.0f; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 185.0f; + contentLayout.titleHorizontalLayoutSpacing = 5.0f; + contentLayout.titleViewHorizontalBottomSpacing = 18.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 12.5f; + contentLayout.circleRowSpacing = 22.0f; + contentLayout.circleRowBottomSpacing = 19.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 12.0f]; + contentLayout.subtitleLabelHorizontalBottomSpacing = 22.0f; + contentLayout.subtitleLabelBottomSpacing = 19.0f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 8.0f; + contentLayout.textFieldCircleSpacing = 4.0f; + contentLayout.textFieldBorderPadding = (CGSize){8, 8}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + + /* Submit Button */ + contentLayout.submitButtonFontSize = 15.0f; + contentLayout.submitButtonSpacing = 3.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 70.0f; + contentLayout.circleButtonSpacing = (CGSize){20.0f, 8.5f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:35.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 4.5f; + contentLayout.circleButtonLetteringSpacing = 2.0f; + + return contentLayout; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h new file mode 100644 index 0000000000..23147a4481 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h @@ -0,0 +1,60 @@ +// +// TOPasscodeViewControllerAnimatedTransitioning.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import + +@class TOPasscodeViewController; + +NS_ASSUME_NONNULL_BEGIN + +/** + An class conforming to `UIViewControllerAnimatedTransitioning` that handles the custom animation + that plays when the passcode view controller is presented on the user's screen. + */ +@interface TOPasscodeViewControllerAnimatedTransitioning : NSObject + +/** The parent passcode view controller that this object will be controlling */ +@property (nonatomic, weak, readonly) TOPasscodeViewController *passcodeViewController; + +/** Whether the controller is being presented or dismissed. The animation is played in reverse when dismissing. */ +@property (nonatomic, assign) BOOL dismissing; + +/** If the correct passcode was successfully entered, this property can be set to YES. When the view controller + is dismissing, the keypad view will also play a zooming out animation to give added context to the dismissal. */ +@property (nonatomic, assign) BOOL success; + +/** + Creates a new instanc of `TOPasscodeViewControllerAnimatedTransitioning` that will control the provided passcode + view controller. + + @param passcodeViewController The passcode view controller in which this object will coordinate the animation upon. + @param dismissing Whether the animation is played to present the view controller, or dismiss it. + @param success Whether the object needs to play an additional zooming animation denoting the passcode was successfully entered. + */ +- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController + dismissing:(BOOL)dismissing + success:(BOOL)success; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m new file mode 100644 index 0000000000..c43ad12e8f --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m @@ -0,0 +1,120 @@ +// +// TOPasscodeViewControllerAnimatedTransitioning.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewControllerAnimatedTransitioning.h" +#import "TOPasscodeViewController.h" +#import "TOPasscodeView.h" + +@interface TOPasscodeViewControllerAnimatedTransitioning () +@property (nonatomic, weak) TOPasscodeViewController *passcodeViewController; +@end + +@implementation TOPasscodeViewControllerAnimatedTransitioning + +- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController dismissing:(BOOL)dismissing success:(BOOL)success +{ + if (self = [super init]) { + _passcodeViewController = passcodeViewController; + _dismissing = dismissing; + _success = success; + } + + return self; +} + +- (NSTimeInterval)transitionDuration:(nullable id )transitionContext +{ + return 0.35f; +} + +- (void)animateTransition:(id )transitionContext +{ + BOOL isPhone = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone; + UIView *containerView = transitionContext.containerView; + UIVisualEffectView *backgroundEffectView = self.passcodeViewController.backgroundEffectView; + UIView *backgroundView = self.passcodeViewController.backgroundView; + UIVisualEffect *backgroundEffect = backgroundEffectView.effect; + TOPasscodeView *passcodeView = self.passcodeViewController.passcodeView; + + // Set the initial properties when presenting + if (!self.dismissing) { + backgroundEffectView.effect = nil; + backgroundView.alpha = 0.0f; + + self.passcodeViewController.view.frame = containerView.bounds; + [containerView addSubview:self.passcodeViewController.view]; + } + else { + UIViewController *baseController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + if (baseController.view.superview == nil) { + [containerView insertSubview:baseController.view atIndex:0]; + } + } + + CGFloat alpha = self.dismissing ? 1.0f : 0.0f; + passcodeView.contentAlpha = alpha; + + // Animate the accessory views + if (isPhone) { + self.passcodeViewController.leftAccessoryButton.alpha = alpha; + self.passcodeViewController.rightAccessoryButton.alpha = alpha; + self.passcodeViewController.cancelButton.alpha = alpha; + self.passcodeViewController.biometricButton.alpha = alpha; + } + + id animationBlock = ^{ + backgroundEffectView.effect = self.dismissing ? nil : backgroundEffect; + backgroundView.alpha = self.dismissing ? 0.0f : 1.0f; + + CGFloat toAlpha = self.dismissing ? 0.0f : 1.0f; + passcodeView.contentAlpha = toAlpha; + if (isPhone) { + self.passcodeViewController.leftAccessoryButton.alpha = toAlpha; + self.passcodeViewController.rightAccessoryButton.alpha = toAlpha; + self.passcodeViewController.cancelButton.alpha = toAlpha; + self.passcodeViewController.biometricButton.alpha = toAlpha; + } + }; + + id completedBlock = ^(BOOL completed) { + backgroundEffectView.effect = backgroundEffect; + [transitionContext completeTransition:completed]; + }; + + // If we're animating out from a successful passcode, play a zooming out animation + // to give some more context + if (self.success && self.dismissing) { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; + animation.duration = [self transitionDuration:transitionContext]; + animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 1)]; + animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + [passcodeView.layer addAnimation:animation forKey:@"transform"]; + } + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0f + options:UIViewAnimationOptionAllowUserInteraction + animations:animationBlock + completion:completedBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h new file mode 100644 index 0000000000..51d2828aae --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h @@ -0,0 +1,54 @@ +// +// TOSettingsKeypadImage.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A subclass of `UIImage` that procedurally generates images for `TOPasscodeSettingsKeypadView`. + This includes background images for each keypad button, and a delete icon for the bottom right corner + of the keypad. + */ +@interface TOSettingsKeypadImage : UIImage + +/** + Generates and returns an image of button background with a raised border in a pseudo-skeuomorphic style. + + @param radius The rounded radius of the button image's corners + @param foregroundColor The fill color of the primary section of the button + @param edgeColor The color of the raised border edge along the bottom. + @param thickness The size of the border running along the bottom + */ ++ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius + foregroundColor:(UIColor *)foregroundColor + edgeColor:(UIColor *)edgeColor + edgeThickness:(CGFloat)thickness; + +/** +Generates and returns a tintable delete icon. + */ ++ (UIImage *)deleteIcon; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m new file mode 100644 index 0000000000..b92b7afb31 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m @@ -0,0 +1,186 @@ +// +// TOSettingsKeypadImage.m +// TOPasscodeViewControllerExample +// +// Created by Tim Oliver on 6/20/17. +// Copyright © 2017 Timothy Oliver. All rights reserved. +// + +#import "TOSettingsKeypadImage.h" + +#define TOP_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define TOP_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define BOTTOM_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) +#define BOTTOM_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) + +@implementation TOSettingsKeypadImage + ++ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius + foregroundColor:(UIColor *)foregroundColor + edgeColor:(UIColor *)edgeColor + edgeThickness:(CGFloat)thickness +{ + CGFloat width = (radius * 2.0f) + 1.0f; + CGFloat height = width + thickness; + + CGRect frame = (CGRect){CGPointZero, {width, height}}; + + UIImage *image = nil; + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + CGContextRef context = UIGraphicsGetCurrentContext(); + + NSShadow* shadow = [[NSShadow alloc] init]; + shadow.shadowColor = edgeColor; + shadow.shadowOffset = CGSizeMake(0, thickness); + shadow.shadowBlurRadius = 0; + + CGRect buttonFrame = frame; + buttonFrame.size.height -= thickness; + + CGContextSaveGState(context); + { + CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, [shadow.shadowColor CGColor]); + UIBezierPath *buttonPath = [[self class] bezierPathWithContinuousRoundedRect:buttonFrame cornerRadius:radius];//bezierPathWithRoundedRect:buttonFrame cornerRadius:radius]; + [foregroundColor setFill]; + [buttonPath fill]; + } + CGContextRestoreGState(context); + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + UIEdgeInsets insets = UIEdgeInsetsMake(radius, radius, radius + thickness, radius); + image = [image resizableImageWithCapInsets:insets]; + + return image; +} + ++ (UIImage *)deleteIcon +{ + UIImage *image = nil; + + CGRect frame = CGRectMake(0, 0, 40.0f, 21.0f); + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + //// DeleteIcon + { + //// Border Drawing + UIBezierPath* borderPath = [UIBezierPath bezierPath]; + [borderPath moveToPoint: CGPointMake(25.73, 1.5)]; + [borderPath addLineToPoint: CGPointMake(25.9, 1.53)]; + [borderPath addCurveToPoint: CGPointMake(28.34, 3.46) controlPoint1: CGPointMake(27.03, 1.86) controlPoint2: CGPointMake(27.93, 2.56)]; + [borderPath addCurveToPoint: CGPointMake(28.67, 6.56) controlPoint1: CGPointMake(28.67, 4.28) controlPoint2: CGPointMake(28.67, 5.04)]; + [borderPath addLineToPoint: CGPointMake(28.64, 14.23)]; + [borderPath addCurveToPoint: CGPointMake(28.35, 17.05) controlPoint1: CGPointMake(28.64, 15.76) controlPoint2: CGPointMake(28.64, 16.37)]; + [borderPath addLineToPoint: CGPointMake(28.31, 17.19)]; + [borderPath addCurveToPoint: CGPointMake(25.86, 19.11) controlPoint1: CGPointMake(27.89, 18.08) controlPoint2: CGPointMake(27, 18.79)]; + [borderPath addCurveToPoint: CGPointMake(21.4, 19.37) controlPoint1: CGPointMake(24.82, 19.37) controlPoint2: CGPointMake(23.34, 19.37)]; + [borderPath addLineToPoint: CGPointMake(11.51, 19.37)]; + [borderPath addCurveToPoint: CGPointMake(9.9, 19.07) controlPoint1: CGPointMake(11.51, 19.37) controlPoint2: CGPointMake(10.41, 19.3)]; + [borderPath addCurveToPoint: CGPointMake(7.38, 17.06) controlPoint1: CGPointMake(9.09, 18.68) controlPoint2: CGPointMake(8.52, 18.14)]; + [borderPath addLineToPoint: CGPointMake(3.92, 13.81)]; + [borderPath addCurveToPoint: CGPointMake(1.87, 11.55) controlPoint1: CGPointMake(2.78, 12.73) controlPoint2: CGPointMake(2.21, 12.19)]; + [borderPath addLineToPoint: CGPointMake(1.79, 11.43)]; + [borderPath addCurveToPoint: CGPointMake(1.82, 9.06) controlPoint1: CGPointMake(1.36, 10.57) controlPoint2: CGPointMake(1.4, 9.92)]; + [borderPath addCurveToPoint: CGPointMake(3.96, 6.68) controlPoint1: CGPointMake(2.25, 8.29) controlPoint2: CGPointMake(2.82, 7.76)]; + [borderPath addLineToPoint: CGPointMake(7.21, 3.61)]; + [borderPath addCurveToPoint: CGPointMake(9.61, 1.67) controlPoint1: CGPointMake(8.35, 2.54) controlPoint2: CGPointMake(8.92, 2)]; + [borderPath addLineToPoint: CGPointMake(9.73, 1.6)]; + [borderPath addCurveToPoint: CGPointMake(11.41, 1.31) controlPoint1: CGPointMake(10.26, 1.37) controlPoint2: CGPointMake(10.84, 1.27)]; + [borderPath addLineToPoint: CGPointMake(21.44, 1.27)]; + [borderPath addCurveToPoint: CGPointMake(25.73, 1.5) controlPoint1: CGPointMake(23.38, 1.27) controlPoint2: CGPointMake(24.85, 1.27)]; + [borderPath closePath]; + [UIColor.blackColor setStroke]; + borderPath.lineWidth = 2.5; + [borderPath stroke]; + + + //// Cross Drawing + UIBezierPath* crossPath = [UIBezierPath bezierPath]; + [crossPath moveToPoint: CGPointMake(15.22, 5.9)]; + [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(15.27, 5.95) controlPoint2: CGPointMake(15.21, 5.88)]; + [crossPath addLineToPoint: CGPointMake(15.22, 5.9)]; + [crossPath closePath]; + [crossPath moveToPoint: CGPointMake(16.18, 10.28)]; + [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(16.22, 10.29) controlPoint2: CGPointMake(16.2, 10.28)]; + [crossPath addLineToPoint: CGPointMake(16.18, 10.28)]; + [crossPath closePath]; + [crossPath moveToPoint: CGPointMake(14.52, 5.35)]; + [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(14.75, 5.46) controlPoint2: CGPointMake(14.93, 5.62)]; + [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.26, 5.94) controlPoint2: CGPointMake(15.32, 5.99)]; + [crossPath addCurveToPoint: CGPointMake(15.43, 6.09) controlPoint1: CGPointMake(15.42, 6.09) controlPoint2: CGPointMake(15.43, 6.09)]; + [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.21, 5.88) controlPoint2: CGPointMake(15.27, 5.95)]; + [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(15.94, 6.59) controlPoint2: CGPointMake(17.66, 8.25)]; + [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(17.91, 8.61) controlPoint2: CGPointMake(17.94, 8.58)]; + [crossPath addCurveToPoint: CGPointMake(21.36, 5.39) controlPoint1: CGPointMake(20.95, 5.68) controlPoint2: CGPointMake(21.14, 5.5)]; + [crossPath addCurveToPoint: CGPointMake(22.67, 5.58) controlPoint1: CGPointMake(21.83, 5.17) controlPoint2: CGPointMake(22.34, 5.26)]; + [crossPath addCurveToPoint: CGPointMake(22.98, 6.89) controlPoint1: CGPointMake(23.09, 5.99) controlPoint2: CGPointMake(23.18, 6.47)]; + [crossPath addCurveToPoint: CGPointMake(22.28, 7.68) controlPoint1: CGPointMake(22.84, 7.14) controlPoint2: CGPointMake(22.65, 7.32)]; + [crossPath addCurveToPoint: CGPointMake(19.68, 10.19) controlPoint1: CGPointMake(22.28, 7.68) controlPoint2: CGPointMake(20.88, 9.03)]; + [crossPath addCurveToPoint: CGPointMake(22.97, 13.47) controlPoint1: CGPointMake(22.66, 13.06) controlPoint2: CGPointMake(22.85, 13.25)]; + [crossPath addCurveToPoint: CGPointMake(22.76, 14.79) controlPoint1: CGPointMake(23.21, 13.95) controlPoint2: CGPointMake(23.11, 14.46)]; + [crossPath addCurveToPoint: CGPointMake(21.35, 15.1) controlPoint1: CGPointMake(22.33, 15.22) controlPoint2: CGPointMake(21.8, 15.31)]; + [crossPath addCurveToPoint: CGPointMake(20.48, 14.4) controlPoint1: CGPointMake(21.07, 14.97) controlPoint2: CGPointMake(20.87, 14.78)]; + [crossPath addCurveToPoint: CGPointMake(17.89, 11.91) controlPoint1: CGPointMake(20.48, 14.4) controlPoint2: CGPointMake(19.08, 13.05)]; + [crossPath addCurveToPoint: CGPointMake(14.5, 15.06) controlPoint1: CGPointMake(14.91, 14.78) controlPoint2: CGPointMake(14.73, 14.95)]; + [crossPath addCurveToPoint: CGPointMake(13.2, 14.87) controlPoint1: CGPointMake(14.04, 15.28) controlPoint2: CGPointMake(13.53, 15.19)]; + [crossPath addCurveToPoint: CGPointMake(12.89, 13.57) controlPoint1: CGPointMake(12.78, 14.47) controlPoint2: CGPointMake(12.69, 13.98)]; + [crossPath addCurveToPoint: CGPointMake(13.42, 12.93) controlPoint1: CGPointMake(13, 13.35) controlPoint2: CGPointMake(13.15, 13.19)]; + [crossPath addCurveToPoint: CGPointMake(13.59, 12.77) controlPoint1: CGPointMake(13.47, 12.88) controlPoint2: CGPointMake(13.53, 12.83)]; + [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(14.12, 12.25) controlPoint2: CGPointMake(15.78, 10.66)]; + [crossPath addCurveToPoint: CGPointMake(12.89, 6.98) controlPoint1: CGPointMake(13.21, 7.39) controlPoint2: CGPointMake(13.01, 7.2)]; + [crossPath addCurveToPoint: CGPointMake(12.77, 6.63) controlPoint1: CGPointMake(12.82, 6.84) controlPoint2: CGPointMake(12.79, 6.73)]; + [crossPath addCurveToPoint: CGPointMake(13.1, 5.66) controlPoint1: CGPointMake(12.72, 6.28) controlPoint2: CGPointMake(12.83, 5.92)]; + [crossPath addCurveToPoint: CGPointMake(14.52, 5.35) controlPoint1: CGPointMake(13.54, 5.24) controlPoint2: CGPointMake(14.07, 5.15)]; + [crossPath closePath]; + [UIColor.blackColor setFill]; + [crossPath fill]; + } + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +/** + Creates a bezier path with the iOS 7 squircle shape. + + A HUGE thanks to the folks at PaintCode for open-sourcing this + https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles + */ ++ (UIBezierPath *)bezierPathWithContinuousRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius +{ + UIBezierPath* path = UIBezierPath.bezierPath; + CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; + CGFloat limitedRadius = MIN(radius, limit); + + [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; + [path addLineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; + [path addCurveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; + [path addLineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; + [path addCurveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; + [path addCurveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; + [path addLineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; + [path addCurveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; + [path addLineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; + [path addCurveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; + [path addCurveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; + [path addLineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; + [path addCurveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; + [path addLineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; + [path addCurveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; + [path addCurveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; + [path addLineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; + [path addCurveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; + [path addLineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; + [path addCurveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; + [path addCurveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; + [path closePath]; + return path; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h new file mode 100644 index 0000000000..3447370059 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h @@ -0,0 +1,71 @@ +// +// TOPasscodeViewControllerConstants.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* The visual style of the asscode view controller */ +typedef NS_ENUM(NSInteger, TOPasscodeViewStyle) { + TOPasscodeViewStyleTranslucentDark, + TOPasscodeViewStyleTranslucentLight, + TOPasscodeViewStyleOpaqueDark, + TOPasscodeViewStyleOpaqueLight +}; + +/* The visual style of the passcode settings view controller. */ +typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewStyle) { + TOPasscodeSettingsViewStyleLight, + TOPasscodeSettingsViewStyleDark +}; + +/* Depending on the amount of horizontal space, the sizing of the elements */ +typedef NS_ENUM(NSInteger, TOPasscodeViewContentSize) { + TOPasscodeViewContentSizeDefault = 414, // Default, 414 points and above (6 Plus, all remaining iPad sizes) + TOPasscodeViewContentSizeMedium = 375, // Greater or equal to 375 points: iPhone 6 / iPad Pro 1/4 split mode + TOPasscodeViewContentSizeSmall = 320 // Greater or equal to 320 points: iPhone SE / iPad 1/4 split mode +}; + +/* The types of passcodes that may be used. */ +typedef NS_ENUM(NSInteger, TOPasscodeType) { + TOPasscodeTypeFourDigits, // 4 Numbers + TOPasscodeTypeSixDigits, // 6 Numbers + TOPasscodeTypeCustomNumeric, // Any length of numbers + TOPasscodeTypeCustomAlphanumeric // Any length of characters +}; + +/* The type of biometrics this controller can handle */ +typedef NS_ENUM(NSInteger, TOPasscodeBiometryType) { + TOPasscodeBiometryTypeTouchID, + TOPasscodeBiometryTypeFaceID +}; + +static inline BOOL TOPasscodeViewStyleIsTranslucent(TOPasscodeViewStyle style) { + return style <= TOPasscodeViewStyleTranslucentLight; +} + +static inline BOOL TOPasscodeViewStyleIsDark(TOPasscodeViewStyle style) { + return style < TOPasscodeViewStyleTranslucentLight || style == TOPasscodeViewStyleOpaqueDark; +} + +static inline NSString *TOPasscodeBiometryTitleForType(TOPasscodeBiometryType type) { + switch (type) { + case TOPasscodeBiometryTypeFaceID: return NSLocalizedString(@"Face ID", @""); + default: return NSLocalizedString(@"Touch ID", @""); + } +} diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h new file mode 100644 index 0000000000..656e617dc7 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h @@ -0,0 +1,108 @@ +// +// TOPasscodeSettingsViewController.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" + +@class TOPasscodeSettingsViewController; + +typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewState) { + TOPasscodeSettingsViewStateEnterCurrentPasscode, + TOPasscodeSettingsViewStateEnterNewPasscode, + TOPasscodeSettingsViewStateConfirmNewPasscode +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + A delegate object in charge of validating and recording the passcodes entered by the user. + */ +@protocol TOPasscodeSettingsViewControllerDelegate + +@optional + +/** Called when the user was prompted to input their current passcode. + Return YES if passcode was right and NO otherwise. + + Returning NO will cause a warning label to appear + */ +- (BOOL)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController + didAttemptCurrentPasscode:(NSString *)passcode; + +/** Called when the user has successfully set a new passcode. At this point, you should save over + the old passcode with the new one. */ +- (void)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController + didChangeToNewPasscode:(NSString *)passcode ofType:(TOPasscodeType)type; + +@end + +// ---------------------------------------------------------------------- + +/** + A standard system-styled view controller that users can use to change the passcode + that they will need to enter for the main passcode view controller. + + This controller allows requiring the user to enter their previous passcode in first, + and has passcode validation by requiring them to enter the new passcode twice. + */ + +@interface TOPasscodeSettingsViewController : UIViewController + +/** Delegate event for controlling and responding to the behavior of this controller */ +@property (nonatomic, weak, nullable) id delegate; + +/** The current state of the controller (confirming old passcode or creating a new one) */ +@property (nonatomic, assign) TOPasscodeSettingsViewState state; + +/** The input type of the passcode */ +@property (nonatomic, assign) TOPasscodeType passcodeType; + +/** The number of incorrect passcode attempts the user has made. Use this property to decide when to disable input. */ +@property (nonatomic, assign) NSInteger failedPasscodeAttemptCount; + +/** Before setting a new passcode, show a UI to validate the existing passcode. (Default is NO) */ +@property (nonatomic, assign) BOOL requireCurrentPasscode; + +/** If set, the view controller will disable input until this date time has been reached */ +@property (nonatomic, strong, nullable) NSDate *disabledInputDate; + +/** Hide the button Options (Default is NO) */ +@property (nonatomic, assign) BOOL hideOptionsButton; + +/* + Create a new instance with the desird light or dark style + + @param style The visual style of the view controller + */ +- (instancetype)init; + +/* + Changes the passcode type and animates if required + + @param passcodeType Change the type of passcode to enter. + @param animated Play a crossfade animation. + */ +- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m new file mode 100644 index 0000000000..0b2d62d2be --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m @@ -0,0 +1,630 @@ +// +// TOPasscodeSettingsViewController.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsViewController.h" +#import "TOPasscodeInputField.h" +#import "TOPasscodeSettingsKeypadView.h" +#import "TOPasscodeSettingsWarningLabel.h" + +const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f; +const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f; +const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f; +const CGFloat kTOPasscodeKeypadMinHeight = 185.0f; // was 165 +const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f; + +@interface TOPasscodeSettingsViewController () + +@property (nonatomic, copy) NSString *potentialPasscode; + +/* Layout Calculations */ +@property (nonatomic, assign) CGFloat verticalMidPoint; +@property (nonatomic, assign) CGRect keyboardFrame; +@property (nonatomic, readonly) CGRect contentOverlapFrame; // Either the keypad or the system keyboard + +/* Views */ +@property (nonatomic, strong) UIView *containerView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *errorLabel; +@property (nonatomic, strong) UIButton *optionsButton; +@property (nonatomic, strong) TOPasscodeInputField *inputField; +@property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView; +@property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel; + +/* Bar Items */ +@property (nonatomic, strong) UIBarButtonItem *nextBarButtonItem; +@property (nonatomic, strong) UIBarButtonItem *doneBarButtonItem; + +/* Style */ +@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; + +@end + +@implementation TOPasscodeSettingsViewController + +#pragma mark - Object Creation - + +- (instancetype)init +{ + if (self = [self initWithNibName:nil bundle:nil]) { + [self setUp]; + } + + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + if (@available(iOS 13.0, *)) { + if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { + self.style = TOPasscodeSettingsViewStyleDark; + } else { + self.style = TOPasscodeSettingsViewStyleLight; + } + } else { + self.style = TOPasscodeSettingsViewStyleLight; + } + + [self applyThemeForStyle:_style]; + + _failedPasscodeAttemptCount = 0; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +#pragma mark - View Set-up - + +- (void)viewDidLoad { + [super viewDidLoad]; + + __weak typeof(self) weakSelf = self; + + self.title = NSLocalizedString(@"Enter Passcode", @""); + + // Create container view + self.containerView = [[UIView alloc] initWithFrame:CGRectZero]; + self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin + | UIViewAutoresizingFlexibleBottomMargin; + [self.view addSubview:self.containerView]; + + // Create title label + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.titleLabel.font = [UIFont systemFontOfSize:17.0f]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.textColor = [UIColor blackColor]; + self.titleLabel.text = @"Enter your passcode"; + self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.titleLabel sizeToFit]; + [self.containerView addSubview:self.titleLabel]; + + // Create number view + self.inputField = [[TOPasscodeInputField alloc] init]; + self.inputField.tintColor = [UIColor blackColor]; + self.inputField.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { [weakSelf inputViewDidCompletePasscode:passcode]; }; + [self.inputField sizeToFit]; + [self.containerView addSubview:self.inputField]; + + // Create keypad view + self.keypadView = [[TOPasscodeSettingsKeypadView alloc] initWithFrame:CGRectZero]; + self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [self.view addSubview:self.keypadView]; + + // Create warning label view + self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero]; + self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + self.warningLabel.hidden = YES; + [self.warningLabel sizeToFit]; + [self.containerView addSubview:self.warningLabel]; + + // Create error label view + self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @""); + self.errorLabel.textAlignment = NSTextAlignmentCenter; + self.errorLabel.font = [UIFont systemFontOfSize:15.0f]; + self.errorLabel.numberOfLines = 0; + self.errorLabel.hidden = YES; + [self.errorLabel sizeToFit]; + [self.containerView addSubview:self.errorLabel]; + + // Create Options button + self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal]; + self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f]; + [self.optionsButton sizeToFit]; + self.optionsButton.hidden = _hideOptionsButton; + [self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.optionsButton]; + + // Add callbacks for the keypad view + self.keypadView.numberButtonTappedHandler = ^(NSInteger number) { + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; + [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; + }; + + self.keypadView.deleteButtonTappedHandler = ^{ [weakSelf.inputField deletePasscodeCharactersOfCount:1 animated:NO]; }; + + // Set height of the container view (This will never change) + CGRect frame = self.containerView.frame; + frame.size.width = self.view.bounds.size.width; + frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) + + CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f); + self.containerView.frame = CGRectIntegral(frame); + + //Work out the vertical offset of the container view assuming the warning label doesn't count + self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) + + kTOPasscodeSettingsLabelInputSpacing; + self.verticalMidPoint *= 0.5f; + + // Bar button items + self.nextBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Next", @"") style:UIBarButtonItemStylePlain target:self action:@selector(nextButtonTapped:)]; + self.doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)]; + + // Apply light/dark mode + [self applyThemeForStyle:self.style]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPasscode : TOPasscodeSettingsViewStateEnterNewPasscode; + [self updateContentForState:self.state type:self.passcodeType animated:NO]; +} + +#pragma mark - View Update - + +- (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type animated:(BOOL)animated +{ + BOOL variableSizePasscode = (type >= TOPasscodeTypeCustomNumeric); + + // Update the visibility of the options button + if (_hideOptionsButton) { + self.optionsButton.hidden = YES; + } else { + self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPasscode); + } + + // Clear the input view + self.inputField.passcode = nil; + + // Disable the input view + self.inputField.enabled = NO; + + //Update the warning label + [self updateWarningLabelForState:state]; + + // Change the input view if needed + if (!variableSizePasscode) { + self.inputField.style = TOPasscodeInputFieldStyleFixed; + self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; + } + else { + self.inputField.style = TOPasscodeInputFieldStyleVariable; + } + + // Update text depending on state + switch (state) { + case TOPasscodeSettingsViewStateEnterCurrentPasscode: + self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; + if (@available(iOS 9.0, *)) { + self.inputField.returnKeyType = UIReturnKeyContinue; + } + else { + self.inputField.returnKeyType = UIReturnKeyNext; + } + break; + case TOPasscodeSettingsViewStateEnterNewPasscode: + self.titleLabel.text = NSLocalizedString(@"Enter a new passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; + if (@available(iOS 9.0, *)) { + self.inputField.returnKeyType = UIReturnKeyContinue; + } + else { + self.inputField.returnKeyType = UIReturnKeyNext; + } + break; + case TOPasscodeSettingsViewStateConfirmNewPasscode: + self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.doneBarButtonItem : nil; + self.inputField.returnKeyType = UIReturnKeyDone; + break; + } + + CGRect frame = CGRectZero; + + // Reload the 'Done' button + [self.inputField reloadInputViews]; + + // Resize text label to fit new text + [self.titleLabel sizeToFit]; + frame = self.titleLabel.frame; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + // Resize passcode view + [self.inputField sizeToFit]; + frame = self.inputField.frame; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.inputField.frame = CGRectIntegral(frame); + + // If we're the alphanumeric type, present the keyboard + if (type == TOPasscodeTypeCustomAlphanumeric) { + self.inputField.enabled = YES; + [self.inputField becomeFirstResponder]; + } + else { + if (self.inputField.isFirstResponder) { + [self.inputField resignFirstResponder]; + } + } + + // If not animated, force a blanket re-layout + if (!animated) { + [self viewDidLayoutSubviews]; + return; + } + + // If animated, perform the animation + [UIView animateWithDuration:0.3f animations:^{ + [self viewDidLayoutSubviews]; + }]; +} + +- (void)updateWarningLabelForState:(TOPasscodeSettingsViewState)state +{ + BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPasscode; + + // Update the warning label + self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0); + self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount; + + CGRect frame = self.warningLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f; + self.warningLabel.frame = frame; +} + +- (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated +{ + // Preserve the current view state + UIView *snapshot = nil; + + BOOL reverseDirection = state < self.state; + + // If animated, take a snapshot of the current container view + if (animated) { + snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO]; + snapshot.frame = self.containerView.frame; + [self.view addSubview:snapshot]; + } + + self.errorLabel.hidden = YES; + + // Update the layout for the new state + self.state = state; + + // Cancel out now if we're not animating + if (!animated) { + return; + } + + // Place the live container off screen to the right + CGFloat multiplier = reverseDirection ? -1.0f : 1.0f; + self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f); + + // Update the options button alpha depending on transition state + self.optionsButton.hidden = _hideOptionsButton; + self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 0.0f : 1.0f; + + // Perform an animation where the snapshot slides off, and the new container slides in + id animationBlock = ^{ + snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f); + self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f); + self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 1.0f : 0.0f; + }; + + // Clean up by removing the snapshot view + id completionBlock = ^(BOOL complete) { + [snapshot removeFromSuperview]; + }; + + // Perform the animation + [UIView animateWithDuration:0.4f + delay:0.0f + usingSpringWithDamping:1.0f + initialSpringVelocity:0.7f + options:0 + animations:animationBlock + completion:completionBlock]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + CGSize viewSize = self.view.bounds.size; + + // Layout the keypad view + CGRect frame = self.keypadView.frame; + frame.size.height = viewSize.height * kTOPasscodeKeypadMaxSizeRatio; + frame.size.height = MAX(frame.size.height, kTOPasscodeKeypadMinHeight); + frame.size.height = MIN(frame.size.height, kTOPasscodeKeypadMaxHeight); + frame.size.width = viewSize.width; + frame.origin.y = viewSize.height; + if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + frame.origin.y -= frame.size.height; + } + + self.keypadView.frame = CGRectIntegral(frame); + + BOOL horizontalLayout = frame.size.height < kTOPasscodeKeypadMinHeight + FLT_EPSILON; + BOOL animated = ([self.view.layer animationForKey:@"bounds.size"] != nil); + [self.keypadView setButtonLabelHorizontalLayout:horizontalLayout animated:animated]; + + CGFloat topContentHeight = self.topLayoutGuide.length; + + // Layout the container view + frame = self.containerView.frame; + frame.origin.y = (((viewSize.height - (topContentHeight + self.contentOverlapFrame.size.height))) * 0.5f) - self.verticalMidPoint; + frame.origin.y += topContentHeight; + self.containerView.frame = CGRectIntegral(frame); + + // Layout the passcode options button + frame = self.optionsButton.frame; + frame.origin.y = CGRectGetMinY(self.contentOverlapFrame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame); + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + self.optionsButton.frame = frame; + + // Set frame of title label + frame = self.titleLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + // Set frame of number pad + frame = self.inputField.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing); + self.inputField.frame = CGRectIntegral(frame); + + // Set the frame for the warning view + frame = self.warningLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; + self.warningLabel.frame = CGRectIntegral(frame); + + // Set the frame of the error view + frame = self.errorLabel.frame; + frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)]; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.errorLabel.frame = CGRectIntegral(frame); +} + +- (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style +{ + BOOL isDark = (style == TOPasscodeSettingsViewStyleDark); + + // Set background color + UIColor *backgroundColor; + if (isDark) { + backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; + } + else { + backgroundColor = [UIColor colorWithRed:235.0f/255.0f green:235.0f/255.0f blue:241.0f/255.0f alpha:1.0f]; + } + self.view.backgroundColor = backgroundColor; + + // Set the style of the keypad view + self.keypadView.style = style; + + // Set the color for the input content + UIColor *inputColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + + // Set the label style + self.titleLabel.textColor = inputColor; + + // Set the number input tint + self.inputField.tintColor = inputColor; + + // Set the tint color of the incorrect warning label + UIColor *warningColor = nil; + if (isDark) { + warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + } + else { + warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + } +} + +#pragma mark - Data Management - +- (void)inputViewDidCompletePasscode:(NSString *)passcode +{ + switch (self.state) { + case TOPasscodeSettingsViewStateEnterCurrentPasscode: + [self validateCurrentPasscodeAttemptWithPasscode:passcode]; + break; + case TOPasscodeSettingsViewStateEnterNewPasscode: + [self didReceiveNewPasscode:passcode]; + break; + case TOPasscodeSettingsViewStateConfirmNewPasscode: + [self confirmNewPasscode:passcode]; + break; + } +} + +- (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode +{ + if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) { + return; + } + + BOOL correct = [self.delegate passcodeSettingsViewController:self didAttemptCurrentPasscode:passcode]; + if (!correct) { + [self.inputField resetPasscodeAnimated:YES playImpact:YES]; + self.failedPasscodeAttemptCount++; + } + else { + [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; + } +} + +- (void)didReceiveNewPasscode:(NSString *)passcode +{ + self.potentialPasscode = passcode; + [self transitionToState:TOPasscodeSettingsViewStateConfirmNewPasscode animated:YES]; +} + +- (void)confirmNewPasscode:(NSString *)passcode +{ + if (![passcode isEqualToString:self.potentialPasscode]) { + [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; + self.errorLabel.hidden = NO; + return; + } + + if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didChangeToNewPasscode:ofType:)]) { + return; + } + + [self.delegate passcodeSettingsViewController:self didChangeToNewPasscode:self.potentialPasscode ofType:self.passcodeType]; +} + +#pragma mark - System Keyboard Handling - +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + self.keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + [self viewDidLayoutSubviews]; +} + +- (CGRect)contentOverlapFrame +{ + if (self.passcodeType < TOPasscodeTypeCustomAlphanumeric) { + return self.keypadView.frame; + } + + // Work out where our view is in relation to the screen + UIWindow *window = self.view.window; + CGRect viewFrame = [self.view.superview convertRect:self.view.frame toView:window]; + + CGFloat overlap = CGRectGetMaxY(viewFrame) - CGRectGetMinY(self.keyboardFrame); + + CGRect overlapFrame = self.keyboardFrame; + overlapFrame.origin.y = MIN(viewFrame.size.height - overlap, viewFrame.size.height); + overlapFrame.size.height = MAX(overlap, 0.0f); + return overlapFrame; +} + +#pragma mark - Button Callbacks - + +- (void)optionsCodeButtonTapped:(id)sender +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + UIAlertActionStyle style = UIAlertActionStyleDefault; + + __weak typeof(self) weakSelf = self; + + NSArray *types = @[@(TOPasscodeTypeFourDigits), + @(TOPasscodeTypeSixDigits), + @(TOPasscodeTypeCustomNumeric), + @(TOPasscodeTypeCustomAlphanumeric) + ]; + + + NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""), + NSLocalizedString(@"6-Digit Numeric Code", @""), + NSLocalizedString(@"Custom Numeric Code", @""), + NSLocalizedString(@"Custom Alphanumeric Code", @"")]; + + // Add all the buttons + for (NSInteger i = 0; i < types.count; i++) { + TOPasscodeType type = [types[i] integerValue]; + if (type == self.passcodeType) { continue; } + + id handler = ^(UIAlertAction *action) { + [weakSelf setPasscodeType:type]; + }; + [alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]]; + } + + // Cancel button + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]]; + + alertController.modalPresentationStyle = UIModalPresentationPopover; + alertController.popoverPresentationController.sourceView = self.optionsButton; + alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds; + alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void)nextButtonTapped:(id)sender +{ + [self inputViewDidCompletePasscode:self.inputField.passcode]; +} + +- (void)doneButtonTapped:(id)sender +{ + [self inputViewDidCompletePasscode:self.inputField.passcode]; +} + +#pragma mark - Accessors - +- (void)setPasscodeType:(TOPasscodeType)passcodeType +{ + [self setPasscodeType:passcodeType animated:NO]; +} + +- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated +{ + if (_passcodeType == passcodeType) { return; } + _passcodeType = passcodeType; + + [self updateContentForState:self.state type:_passcodeType animated:animated]; +} + +- (void)setState:(TOPasscodeSettingsViewState)state +{ + if (_state == state) { return; } + _state = state; + + [self updateContentForState:_state type:self.passcodeType animated:NO]; +} + +- (void)setFailedPasscodeAttemptCount:(NSInteger)failedPasscodeAttemptCount +{ + if (_failedPasscodeAttemptCount == failedPasscodeAttemptCount) { return; } + _failedPasscodeAttemptCount = failedPasscodeAttemptCount; + [self updateWarningLabelForState:self.state]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h new file mode 100644 index 0000000000..f4f6a7023d --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h @@ -0,0 +1,163 @@ +// +// TOPasscodeViewController.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" +#import "TOPasscodeSettingsViewController.h" +#import "TOPasscodeView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeViewController; + +/** + A delegate object in charge of validating the passcodes that the user has entered into the passcode + view controller. + */ +@protocol TOPasscodeViewControllerDelegate + +@optional + +/** + Return YES if the user entered the expected PIN code. Return NO if it was incorrect. + (For security reasons, it is safer to fetch the saved PIN code only when this method is called, and + then discard it immediately. This is why the view controller does not directly store it.) +*/ +- (BOOL)passcodeViewController:(TOPasscodeViewController *)passcodeViewController isCorrectCode:(NSString *)code; + +/** The user tapped the 'Cancel' button. Any dismissing of confidential content should be done in here. */ +- (void)didTapCancelInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** The user successfully entered the correct code, as validated by `isCorrectCode:` */ +- (void)didInputCorrectPasscodeInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** When available, the user tapped the 'Touch ID' button, or the view controller itself automatically initiated + the Touch ID request on display. This method is where you should implement your + own Touch ID validation logic. For security reasons, this controller does not implement the Touch ID logic itself. */ + +- (void)didPerformBiometricValidationRequestInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** Called when the pin view was resized as a result of the view controller being resized. + You can use this to resize your custom header view if necessary. + */ +- (void)passcodeViewController:(TOPasscodeViewController *)passcodeViewController didResizePasscodeViewToWidth:(CGFloat)width; + +@end + + +/** + A view controller that displays an interface for entering a user passcode. + It may be presented modally over another view controller, requiring the user to enter + the passcode correctly before they are able to proceed inside the application. + */ +@interface TOPasscodeViewController : UIViewController + +/** A delegate object, in charge of verifying the PIN code entered by the user */ +@property (nonatomic, weak, nullable) id delegate; + +/** The type of passcode that is expected to be entered. */ +@property (nonatomic, readonly) TOPasscodeType passcodeType; + +/** Will show a 'Touch ID' or 'Face ID' (depending on `biometricType`) button if the user is allowed to log in that way. (Default is NO) */ +@property (nonatomic, assign) BOOL allowBiometricValidation; + +/** Will handle delete button press as delete last symbol (Default is YES) */ +@property (nonatomic, assign) BOOL handleDeletePress; + +/** Set the type of biometrics for this device to update the title of the biometrics button properly. */ +@property (nonatomic, assign) TOPasscodeBiometryType biometryType; + +/** If biometrics are available, automatically ask for it upon presentation (Default is NO) */ +@property (nonatomic, assign) BOOL automaticallyPromptForBiometricValidation; + +/** Optionally change the color of the title text label. */ +@property (nonatomic, strong, nullable) UIColor *titleLabelColor; + +/** Optionally change the tint color of the UI element that indicates input progress (eg the row of circles) */ +@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; + +/** Optionally enable or disable showing the lettering label of all keypad circle buttons. **/ +@property (nonatomic, assign) BOOL keypadButtonShowLettering; + +/** If the style isn't translucent, changes the tint color of the keypad circle button outlines. */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundTintColor; + +/** The color of the text elements in each keypad button */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; + +/** Optionally, the text color of the keypad button text when tapped. Animates back to the base color. */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; + +/** The tint button of the accessory button views at the bottom of the keypad (ie 'Cance' etc) */ +@property (nonatomic, strong, nullable) UIColor *accessoryButtonTintColor; + +/** Controls the transluceny of the PIN background when the style has been set to translucent. */ +@property (nonatomic, readonly) UIVisualEffectView *backgroundEffectView; + +/** Opaque, background view when the style is opaque */ +@property (nonatomic, readonly) UIView *backgroundView; + +/** The keypad and accessory views that are displayed in the center of this view */ +@property (nonatomic, readonly) TOPasscodeView *passcodeView; + +/** The Touch ID button, visible if biometrics is enabled and `leftAccessoryButton` is nil. */ +@property (nonatomic, readonly) UIButton *biometricButton; + +/** The Cancel, visible if `rightAccessoryButton` is nil. */ +@property (nonatomic, readonly) UIButton *cancelButton; + +/** The left accessory button. Setting this will override the 'Touch ID' button. */ +@property (nonatomic, strong, nullable) UIButton *leftAccessoryButton; + +/** The right accessory button. Setting this will override the 'Cancel' button. */ +@property (nonatomic, strong, nullable) UIButton *rightAccessoryButton; + +@property (nonatomic, assign) CGFloat accessoryButtonsVerticalInset; + +/** Whether all of the content views are hidden or not, but the background translucent view remains. + Useful for obscuring the content while the app is suspended. */ +@property (nonatomic, assign) BOOL contentHidden; + +/** + Create a new instance of this view controller with the preset style and passcode type. + + @param type The type of passcode to enter (6-digit/numeric) + */ +- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel; + +/** + Hide everything except the background translucency view. + + @param hidden Whether the content is hidden or not. + @param animated The content will play a crossfade animation. + */ +- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +//! Project version number for TOPasscodeViewController. +FOUNDATION_EXPORT double TOPasscodeViewControllerVersionNumber; + +//! Project version string for TOPasscodeViewController. +FOUNDATION_EXPORT const unsigned char TOPasscodeViewControllerVersionString[]; diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m new file mode 100755 index 0000000000..34d81370ac --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m @@ -0,0 +1,710 @@ +// +// TOPasscodeViewController.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewController.h" +#import "TOPasscodeView.h" +#import "TOPasscodeViewControllerAnimatedTransitioning.h" +#import "TOPasscodeKeypadView.h" +#import "TOPasscodeInputField.h" + +@interface TOPasscodeViewController () + +/* State */ +@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; +@property (nonatomic, assign) CGFloat keyboardHeight; +@property (nonatomic, assign) BOOL passcodeSuccess; +@property (nonatomic, readonly) UIView *leftButton; +@property (nonatomic, readonly) UIView *rightButton; + +/* Views */ +@property (nonatomic, strong, readwrite) UIVisualEffectView *backgroundEffectView; +@property (nonatomic, strong, readwrite) UIView *backgroundView; +@property (nonatomic, strong, readwrite) TOPasscodeView *passcodeView; +@property (nonatomic, strong, readwrite) UIButton *biometricButton; +@property (nonatomic, strong, readwrite) UIButton *cancelButton; + +/* Style */ +@property (nonatomic, assign) TOPasscodeViewStyle style; +@property (nonatomic, assign) BOOL allowCancel; + +@end + +@implementation TOPasscodeViewController + +#pragma mark - Instance Creation - + +- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel +{ + if (self = [super initWithNibName:nil bundle:nil]) { + _passcodeType = type; + _allowCancel = cancel; + [self setUp]; + } + + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + [self setUp]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +#pragma mark - View Setup - + +- (void)setUp +{ + self.transitioningDelegate = self; + self.automaticallyPromptForBiometricValidation = NO; + self.handleDeletePress = YES; + + if (@available(iOS 13.0, *)) { + if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { + self.style = TOPasscodeViewStyleTranslucentDark; + } else { + self.style = TOPasscodeViewStyleTranslucentLight; + } + } else { + self.style = TOPasscodeViewStyleTranslucentLight; + } + + if (TOPasscodeViewStyleIsTranslucent(self.style)) { + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + } + else { + self.modalPresentationStyle = UIModalPresentationFullScreen; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +- (void)setUpBackgroundEffectViewForStyle:(TOPasscodeViewStyle)style +{ + BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); + + // Return if it already exists when it should + if (translucent && self.backgroundEffectView) { return; } + + // Return if it doesn't exist when it shouldn't + if (!translucent && !self.backgroundEffectView) { return; } + + // Remove it if we're now opaque + if (!translucent) { + [self.backgroundEffectView removeFromSuperview]; + self.backgroundEffectView = nil; + return; + } + + // Create it otherwise + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:[self blurEffectStyleForStyle:style]]; + self.backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + self.backgroundEffectView.frame = self.view.bounds; + self.backgroundEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view insertSubview:self.backgroundEffectView atIndex:0]; +} + +- (void)setUpBackgroundViewForStyle:(TOPasscodeViewStyle)style +{ + BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); + + if (!translucent && self.backgroundView) { return; } + + if (translucent && !self.backgroundView) { return; } + + if (translucent) { + [self.backgroundView removeFromSuperview]; + self.backgroundView = nil; + return; + } + + self.backgroundView = [[UIView alloc] initWithFrame:self.view.bounds]; + self.backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view insertSubview:self.backgroundView atIndex:0]; +} + +- (UIBlurEffectStyle)blurEffectStyleForStyle:(TOPasscodeViewStyle)style +{ + switch (self.style) { + case TOPasscodeViewStyleTranslucentDark: return UIBlurEffectStyleDark; + case TOPasscodeViewStyleTranslucentLight: return UIBlurEffectStyleExtraLight; + default: return 0; + } + + return 0; +} + +- (void)setUpAccessoryButtons +{ + UIFont *buttonFont = [UIFont systemFontOfSize:16.0f]; + BOOL isPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad; + + if (!self.leftAccessoryButton && self.allowBiometricValidation && !self.biometricButton) { + self.biometricButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(self.biometryType) forState:UIControlStateNormal]; + [self.biometricButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + + if (isPad) { + self.passcodeView.leftButton = self.biometricButton; + } + else { + [self.view addSubview:self.biometricButton]; + } + } + else { + if (self.leftAccessoryButton) { + [self.biometricButton removeFromSuperview]; + self.biometricButton = nil; + } + } + + if (!self.rightAccessoryButton && !self.cancelButton) { + self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.cancelButton setTitle:NSLocalizedString(@"Cancel", @"Cancel") forState:UIControlStateNormal]; + self.cancelButton.titleLabel.font = buttonFont; + [self.cancelButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + // If cancelling is disabled, we hide the cancel button but we still create it, because it can + // transition to backspace after user input. + self.cancelButton.hidden = !self.allowCancel; + if (isPad) { + self.passcodeView.rightButton = self.cancelButton; + } + else { + [self.view addSubview:self.cancelButton]; + } + } + else { + if (self.rightAccessoryButton) { + [self.cancelButton removeFromSuperview]; + self.cancelButton = nil; + } + } + + [self updateAccessoryButtonFontsForSize:self.view.bounds.size]; +} + +#pragma mark - View Management - +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = [UIColor clearColor]; + self.view.layer.allowsGroupOpacity = NO; + [self setUpBackgroundEffectViewForStyle:self.style]; + [self setUpBackgroundViewForStyle:self.style]; + [self setUpAccessoryButtons]; + [self applyThemeForStyle:self.style]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Automatically trigger biometric validation if available + if (self.allowBiometricValidation && self.automaticallyPromptForBiometricValidation) { + [self accessoryButtonTapped:self.biometricButton]; + } +} + +- (void)viewDidLayoutSubviews +{ + CGSize bounds = self.view.bounds.size; + CGSize maxSize = bounds; + if (@available(iOS 11.0, *)) { + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + if (safeAreaInsets.bottom > 0) { + maxSize.height -= safeAreaInsets.bottom; + } + if (safeAreaInsets.left > 0) { + maxSize.width -= safeAreaInsets.left; + } + if (safeAreaInsets.right > 0) { + maxSize.width -= safeAreaInsets.right; + } + } + + // Resize the pin view to scale to the new size + [self.passcodeView sizeToFitSize:maxSize]; + + // Re-center the pin view + CGRect frame = self.passcodeView.frame; + frame.origin.x = (bounds.width - frame.size.width) * 0.5f; + frame.origin.y = ((bounds.height - self.keyboardHeight) - frame.size.height) * 0.5f; + self.passcodeView.frame = CGRectIntegral(frame); + + // -------------------------------------------------- + + // Update the accessory button sizes + [self updateAccessoryButtonFontsForSize:maxSize]; + + // Re-layout the accessory buttons + [self layoutAccessoryButtonsForSize:maxSize]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self setNeedsStatusBarAppearanceUpdate]; + + // Force an initial layout if the view hasn't been presented yet + [UIView performWithoutAnimation:^{ + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + }]; + + // Show the keyboard if we're entering alphanumeric characters + if (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { + [self.passcodeView.inputField becomeFirstResponder]; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + // Dismiss the keyboard if it is visible + if (self.passcodeView.inputField.isFirstResponder) { + [self.passcodeView.inputField resignFirstResponder]; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return TOPasscodeViewStyleIsDark(self.style) ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; +} + +#pragma mark - View Rotations - +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + // We don't need to do anything special on iPad or if we're using character input + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad || self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { return; } + + // Work out if we need to transition to horizontal + BOOL horizontalLayout = size.height < size.width; + + // Perform layout animation + [self.passcodeView setHorizontalLayout:horizontalLayout animated:coordinator.animated duration:coordinator.transitionDuration]; +} + +#pragma mark - View Styling - +- (void)applyThemeForStyle:(TOPasscodeViewStyle)style +{ + BOOL isDark = TOPasscodeViewStyleIsDark(style); + + // Apply the tint color to the accessory buttons + UIColor *accessoryTintColor = self.accessoryButtonTintColor; + if (!accessoryTintColor) { + accessoryTintColor = isDark ? [UIColor whiteColor] : nil; + } + + self.biometricButton.tintColor = accessoryTintColor; + self.cancelButton.tintColor = accessoryTintColor; + self.leftAccessoryButton.tintColor = accessoryTintColor; + self.rightAccessoryButton.tintColor = accessoryTintColor; + + self.backgroundView.backgroundColor = isDark ? [UIColor colorWithWhite:0.1f alpha:1.0f] : [UIColor whiteColor]; +} + +- (void)updateAccessoryButtonFontsForSize:(CGSize)size +{ + CGFloat width = size.width; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + width = MIN(size.width, size.height); + } + + CGFloat pointSize = 17.0f; + if (width < TOPasscodeViewContentSizeMedium) { + pointSize = 14.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + pointSize = 16.0f; + } + + UIFont *accessoryFont = [UIFont systemFontOfSize:pointSize]; + + self.biometricButton.titleLabel.font = accessoryFont; + self.cancelButton.titleLabel.font = accessoryFont; + self.leftAccessoryButton.titleLabel.font = accessoryFont; + self.rightAccessoryButton.titleLabel.font = accessoryFont; +} + +- (void)verticalLayoutAccessoryButtonsForSize:(CGSize)size +{ + CGFloat width = MIN(size.width, size.height); + + CGFloat verticalInset = 44.0f; + if (width < TOPasscodeViewContentSizeMedium) { + verticalInset = 20.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + verticalInset = 30.0f; + } + + if (self.accessoryButtonsVerticalInset > 0) { + verticalInset = self.accessoryButtonsVerticalInset; + } + + CGFloat inset = self.passcodeView.keypadButtonInset; + CGPoint point = (CGPoint){0.0f, (self.view.bounds.size.height - self.keyboardHeight) - verticalInset}; + if (@available(iOS 11.0, *)) { + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + if (safeAreaInsets.bottom > 0) { + point.y -= safeAreaInsets.bottom; + } + } + + if (self.leftButton) { + [self.leftButton sizeToFit]; + point.x = self.passcodeView.frame.origin.x + inset; + self.leftButton.center = point; + } + + if (self.rightButton) { + [self.rightButton sizeToFit]; + point.x = CGRectGetMaxX(self.passcodeView.frame) - inset; + self.rightButton.center = point; + } +} + +- (void)horizontalLayoutAccessoryButtonsForSize:(CGSize)size +{ + CGRect passcodeViewFrame = self.passcodeView.frame; + CGFloat buttonInset = self.passcodeView.keypadButtonInset; + CGFloat width = MIN(size.width, size.height); + CGFloat verticalInset = 35.0f; + if (width < TOPasscodeViewContentSizeMedium) { + verticalInset = 30.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + verticalInset = 35.0f; + } + + if (self.leftButton) { + [self.leftButton sizeToFit]; + CGRect frame = self.leftButton.frame; + frame.origin.y = (self.view.bounds.size.height - verticalInset) - (frame.size.height * 0.5f); + frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); + self.leftButton.frame = CGRectIntegral(frame); + } + + if (self.rightButton) { + [self.rightButton sizeToFit]; + CGRect frame = self.rightButton.frame; + frame.origin.y = verticalInset - (frame.size.height * 0.5f); + frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); + self.rightButton.frame = CGRectIntegral(frame); + } + + [self.view bringSubviewToFront:self.rightButton]; + [self.view bringSubviewToFront:self.leftButton]; +} + +- (void)layoutAccessoryButtonsForSize:(CGSize)size +{ + // The buttons are always embedded in the keypad view on iPad + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone) { return; } + + if (self.passcodeView.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + [self horizontalLayoutAccessoryButtonsForSize:size]; + } + else { + [self verticalLayoutAccessoryButtonsForSize:size]; + } +} + +#pragma mark - Interactions - +- (void)accessoryButtonTapped:(id)sender +{ + if (sender == self.cancelButton) { + // When entering keyboard input, just leave the button as 'cancel' + if (self.handleDeletePress && self.passcodeType != TOPasscodeTypeCustomAlphanumeric && self.passcodeView.passcode.length > 0) { + [self.passcodeView deleteLastPasscodeCharacterAnimated:YES]; + [self keypadButtonTapped]; + return; + } + + if ([self.delegate respondsToSelector:@selector(didTapCancelInPasscodeViewController:)]) { + [self.delegate didTapCancelInPasscodeViewController:self]; + } + } + else if (sender == self.biometricButton) { + if ([self.delegate respondsToSelector:@selector(didPerformBiometricValidationRequestInPasscodeViewController:)]) { + [self.delegate didPerformBiometricValidationRequestInPasscodeViewController:self]; + } + } +} + +- (void)keypadButtonTapped +{ + NSString *title = nil; + if (self.passcodeView.passcode.length > 0) { + title = NSLocalizedString(@"Delete", @"Delete"); + } else if (self.allowCancel) { + title = NSLocalizedString(@"Cancel", @"Cancel"); + } + [UIView performWithoutAnimation:^{ + if (title != nil) { + [self.cancelButton setTitle:title forState:UIControlStateNormal]; + [self.cancelButton layoutIfNeeded]; + } + self.cancelButton.hidden = (title == nil); + }]; +} + +- (void)didCompleteEnteringPasscode:(NSString *)passcode +{ + if (![self.delegate respondsToSelector:@selector(passcodeViewController:isCorrectCode:)]) { + return; + } + + // Validate the code + BOOL isCorrect = [self.delegate passcodeViewController:self isCorrectCode:passcode]; + if (!isCorrect) { + [self.passcodeView resetPasscodeAnimated:YES playImpact:YES]; + return; + } + + // Hang onto the fact the passcode was successful to play a nicer dismissal animation + self.passcodeSuccess = YES; + + // Perform handler if correctly entered + if ([self.delegate respondsToSelector:@selector(didInputCorrectPasscodeInPasscodeViewController:)]) { + [self.delegate didInputCorrectPasscodeInPasscodeViewController:self]; + } + else { + [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } +} + +#pragma mark - Keyboard Handling - +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + // Extract the keyboard information we need from the notification + CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGFloat animationDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]; + UIViewAnimationOptions animationCurve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; + + // Work out the on-screen height of the keyboard + self.keyboardHeight = self.view.bounds.size.height - keyboardFrame.origin.y; + self.keyboardHeight = MAX(self.keyboardHeight, 0.0f); + + // Set that the view needs to be laid out + [self.view setNeedsLayout]; + + if (animationDuration < FLT_EPSILON) { + return; + } + + // Animate the content sliding up and down with the keyboard + [UIView animateWithDuration:animationDuration + delay:0.0f + options:animationCurve + animations:^{ [self.view layoutIfNeeded]; } + completion:nil]; +} + +#pragma mark - Transitioning Delegate - +- (nullable id )animationControllerForPresentedController:(UIViewController *)presented + presentingController:(UIViewController *)presenting + sourceController:(UIViewController *)source +{ + return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:NO success:NO]; +} + +- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed +{ + return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:YES success:self.passcodeSuccess]; +} + +#pragma mark - Convenience Accessors - +- (UIView *)leftButton +{ + return self.leftAccessoryButton ? self.leftAccessoryButton : self.biometricButton; +} + +- (UIView *)rightButton +{ + return self.rightAccessoryButton ? self.rightAccessoryButton : self.cancelButton; +} + +#pragma mark - Public Accessors - +- (TOPasscodeView *)passcodeView +{ + if (_passcodeView) { return _passcodeView; } + + _passcodeView = [[TOPasscodeView alloc] initWithStyle:self.style passcodeType:self.passcodeType]; + _passcodeView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | + UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [_passcodeView sizeToFit]; + _passcodeView.center = self.view.center; + [self.view addSubview:_passcodeView]; + + __weak typeof(self) weakSelf = self; + _passcodeView.passcodeCompletedHandler = ^(NSString *passcode) { + [weakSelf didCompleteEnteringPasscode:passcode]; + }; + + _passcodeView.passcodeDigitEnteredHandler = ^{ + [weakSelf keypadButtonTapped]; + }; + + // Set initial layout to horizontal if we're rotated on an iPhone + if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric && UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + CGSize boundsSize = self.view.bounds.size; + _passcodeView.horizontalLayout = boundsSize.width > boundsSize.height; + } + + return _passcodeView; +} + +- (void)setStyle:(TOPasscodeViewStyle)style +{ + if (style == _style) { return; } + _style = style; + + self.passcodeView.style = style; + [self setUpBackgroundEffectViewForStyle:style]; +} + +- (void)setAllowBiometricValidation:(BOOL)allowBiometricValidation +{ + if (_allowBiometricValidation == allowBiometricValidation) { + return; + } + + _allowBiometricValidation = allowBiometricValidation; + [self setUpAccessoryButtons]; + [self applyThemeForStyle:self.style]; +} + +- (void)setTitleLabelColor:(UIColor *)titleLabelColor +{ + self.passcodeView.titleLabelColor = titleLabelColor; +} + +- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor +{ + self.passcodeView.subtitleLabelColor = subtitleLabelColor; +} + +- (UIColor *)titleLabelColor { return self.passcodeView.titleLabelColor; } + +- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor +{ + self.passcodeView.inputProgressViewTintColor = inputProgressViewTintColor; +} + +- (UIColor *)inputProgressViewTintColor { return self.passcodeView.inputProgressViewTintColor; } + +- (void)setKeypadButtonBackgroundTintColor:(UIColor *)keypadButtonBackgroundTintColor +{ + self.passcodeView.keypadButtonBackgroundColor = keypadButtonBackgroundTintColor; +} + +- (void)setKeypadButtonShowLettering:(BOOL)keypadButtonShowLettering +{ + self.passcodeView.keypadView.showLettering = keypadButtonShowLettering; +} + +- (UIColor *)keypadButtonBackgroundTintColor { return self.passcodeView.keypadButtonBackgroundColor; } + +- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor +{ + self.passcodeView.keypadButtonTextColor = keypadButtonTextColor; +} + +- (UIColor *)keypadButtonTextColor { return self.passcodeView.keypadButtonTextColor; } + +- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor +{ + self.passcodeView.keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; +} + +- (UIColor *)keypadButtonHighlightedTextColor { return self.passcodeView.keypadButtonHighlightedTextColor; } + +- (void)setAccessoryButtonTintColor:(UIColor *)accessoryButtonTintColor +{ + if (accessoryButtonTintColor == _accessoryButtonTintColor) { return; } + _accessoryButtonTintColor = accessoryButtonTintColor; + [self applyThemeForStyle:self.style]; +} + +- (void)setBiometryType:(TOPasscodeBiometryType)biometryType +{ + if (_biometryType == biometryType) { return; } + + _biometryType = biometryType; + + if (self.biometricButton) { + [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(_biometryType) forState:UIControlStateNormal]; + } +} + +- (void)setContentHidden:(BOOL)contentHidden +{ + [self setContentHidden:contentHidden animated:NO]; +} + +- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated +{ + if (hidden == _contentHidden) { return; } + _contentHidden = hidden; + + void (^setViewsHiddenBlock)(BOOL) = ^(BOOL hidden) { + self.passcodeView.hidden = hidden; + self.leftButton.hidden = hidden; + self.rightButton.hidden = hidden; + }; + + void (^completionBlock)(BOOL) = ^(BOOL complete) { + setViewsHiddenBlock(hidden); + }; + + if (!animated) { + completionBlock(YES); + return; + } + + // Make sure the views are visible before the animation + setViewsHiddenBlock(NO); + + void (^animationBlock)(void) = ^{ + CGFloat alpha = hidden ? 0.0f : 1.0f; + self.passcodeView.contentAlpha = alpha; + self.leftButton.alpha = alpha; + self.rightButton.alpha = alpha; + }; + + // Animate + [UIView animateWithDuration:0.4f animations:animationBlock completion:completionBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h new file mode 100644 index 0000000000..b44bfc7dc1 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h @@ -0,0 +1,81 @@ +// +// TOPasscodeCircleButton.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeCircleView; +@class TOPasscodeButtonLabel; + +NS_ASSUME_NONNULL_BEGIN + +/** + A UI control representing a single PIN code button for the keypad, + including the number, lettering (eg 'ABC'), and circle border. + */ +@interface TOPasscodeCircleButton : UIControl + +// Alpha value that properly controls the necessary subviews +@property (nonatomic, assign) CGFloat contentAlpha; + +// Required to be set before this view can be properly rendered +@property (nonatomic, strong) UIImage *backgroundImage; +@property (nonatomic, strong) UIImage *hightlightedBackgroundImage; +@property (nonatomic, strong) UIVibrancyEffect *vibrancyEffect; + +// Properties with default values +@property (nonatomic, strong) UIColor *textColor; +@property (nonatomic, strong, nullable) UIColor *highlightedTextColor; +@property (nonatomic, strong) UIFont *numberFont; +@property (nonatomic, strong) UIFont *letteringFont; +@property (nonatomic, assign) CGFloat letteringCharacterSpacing; +@property (nonatomic, assign) CGFloat letteringVerticalSpacing; + +@property (nonatomic, readonly) NSString *numberString; +@property (nonatomic, readonly) NSString *letteringString; + +// The internal views +@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; +@property (nonatomic, readonly) TOPasscodeCircleView *circleView; +@property (nonatomic, readonly) UIVisualEffectView *vibrancyView; + +// Callback handler +@property (nonatomic, copy) void (^buttonTappedHandler)(void); + +/** + Create a new instance of the class with the supplied number and lettering string + + @param numberString The string of the number to display in this button (eg '1'). + @param letteringString The string of the lettering to display underneath. + */ +- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString; + +/** + Set the background of the button to be the filled circle instead of hollow. + + @param highlighted When YES, the circle is full, when NO, it is hollow. + @param animated When animated, the transition is a crossfade. + */ +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m new file mode 100644 index 0000000000..790bfe0db3 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m @@ -0,0 +1,236 @@ +// +// TOPasscodeCircleButton.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeCircleButton () + +@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; +@property (nonatomic, strong, readwrite) TOPasscodeCircleView *circleView; +@property (nonatomic, strong, readwrite) UIVisualEffectView *vibrancyView; + +@property (nonatomic, readwrite, copy) NSString *numberString; +@property (nonatomic, readwrite, copy) NSString *letteringString; + +@end + +@implementation TOPasscodeCircleButton + +- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString +{ + if (self = [super init]) { + _numberString = numberString; + _letteringString = letteringString; + _contentAlpha = 1.0f; + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + self.userInteractionEnabled = YES; + + _textColor = [UIColor whiteColor]; + + [self setUpSubviews]; + [self setUpViewInteraction]; +} + +- (void)setUpSubviews +{ + if (!self.circleView) { + self.circleView = [[TOPasscodeCircleView alloc] initWithFrame:self.bounds]; + [self addSubview:self.circleView]; + } + + if (!self.buttonLabel) { + self.buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:self.bounds]; + self.buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.buttonLabel.userInteractionEnabled = NO; + self.buttonLabel.textColor = self.textColor; + self.buttonLabel.numberString = self.numberString; + self.buttonLabel.letteringString = self.letteringString; + [self addSubview:self.buttonLabel]; + } + + if (!self.vibrancyView) { + self.vibrancyView = [[UIVisualEffectView alloc] initWithEffect:nil]; + self.vibrancyView.userInteractionEnabled = NO; + [self.vibrancyView.contentView addSubview:self.circleView]; + [self addSubview:self.vibrancyView]; + } +} + +- (void)setUpViewInteraction +{ + if (self.allTargets.count) { return; } + + [self addTarget:self action:@selector(buttonDidTouchDown:) forControlEvents:UIControlEventTouchDown]; + [self addTarget:self action:@selector(buttonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; + [self addTarget:self action:@selector(buttonDidDragInside:) forControlEvents:UIControlEventTouchDragEnter]; + [self addTarget:self action:@selector(buttonDidDragOutside:) forControlEvents:UIControlEventTouchDragExit]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.vibrancyView.frame = self.bounds; + self.circleView.frame = self.vibrancyView ? self.vibrancyView.bounds : self.bounds; + self.buttonLabel.frame = self.bounds; + [self bringSubviewToFront:self.buttonLabel]; +} + +#pragma mark - User Interaction - + +- (void)buttonDidTouchDown:(id)sender +{ + if (self.buttonTappedHandler) { self.buttonTappedHandler(); } + [self setHighlighted:YES animated:NO]; +} + +- (void)buttonDidTouchUpInside:(id)sender { [self setHighlighted:NO animated:YES]; } +- (void)buttonDidDragInside:(id)sender { [self setHighlighted:YES animated:NO]; } +- (void)buttonDidDragOutside:(id)sender { [self setHighlighted:NO animated:YES]; } + +#pragma mark - Animated Accessors - + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + [self.circleView setHighlighted:highlighted animated:animated]; + + if (!self.highlightedTextColor) { return; } + + void (^textFadeBlock)(void) = ^{ + self.buttonLabel.textColor = highlighted ? self.highlightedTextColor : self.textColor; + }; + + if (!animated) { + textFadeBlock(); + return; + } + + [UIView transitionWithView:self.buttonLabel + duration:0.6f + options:UIViewAnimationOptionTransitionCrossDissolve + animations:textFadeBlock + completion:nil]; +} + +#pragma mark - Accessors - + +- (void)setBackgroundImage:(UIImage *)backgroundImage +{ + self.circleView.circleImage = backgroundImage; + CGRect frame = self.frame; + frame.size = backgroundImage.size; + self.frame = CGRectIntegral(frame); +} + +- (UIImage *)backgroundImage { return self.circleView.circleImage; } + +/***********************************************************/ + +- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect +{ + if (_vibrancyEffect == vibrancyEffect) { return; } + _vibrancyEffect = vibrancyEffect; + self.vibrancyView.effect = _vibrancyEffect; +} + +/***********************************************************/ + +- (void)setHightlightedBackgroundImage:(UIImage *)hightlightedBackgroundImage +{ + self.circleView.highlightedCircleImage = hightlightedBackgroundImage; +} + +- (UIImage *)hightlightedBackgroundImage { return self.circleView.highlightedCircleImage; } + +/***********************************************************/ + +- (void)setNumberFont:(UIFont *)numberFont +{ + self.buttonLabel.numberLabelFont = numberFont; + [self setNeedsLayout]; +} + +- (UIFont *)numberFont { return self.buttonLabel.numberLabelFont; } + +/***********************************************************/ + +- (void)setLetteringFont:(UIFont *)letteringFont +{ + self.buttonLabel.letteringLabelFont = letteringFont; + [self setNeedsLayout]; +} + +- (UIFont *)letteringFont { return self.buttonLabel.letteringLabelFont; } + +/***********************************************************/ + +- (void)setLetteringVerticalSpacing:(CGFloat)letteringVerticalSpacing +{ + self.buttonLabel.letteringVerticalSpacing = letteringVerticalSpacing; + [self.buttonLabel setNeedsLayout]; +} + +- (CGFloat)letteringVerticalSpacing { return self.buttonLabel.letteringVerticalSpacing; } + +/***********************************************************/ + +- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing +{ + self.buttonLabel.letteringCharacterSpacing = letteringCharacterSpacing; +} + +- (CGFloat)letteringCharacterSpacing { return self.buttonLabel.letteringCharacterSpacing; } + +/***********************************************************/ + +- (void)setTextColor:(UIColor *)textColor +{ + if (textColor == _textColor) { return; } + _textColor = textColor; + + self.buttonLabel.textColor = _textColor; +} + +/***********************************************************/ + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + if (_contentAlpha == contentAlpha) { + return; + } + + _contentAlpha = contentAlpha; + + self.buttonLabel.alpha = contentAlpha; + self.circleView.alpha = contentAlpha; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h new file mode 100644 index 0000000000..4db6bb9a88 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h @@ -0,0 +1,101 @@ +// +// TOPasscodeKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeCircleButton; + +/** + A view encompassing 9 circle buttons, making up a keypad view for entering PIN numbers. + Can be laid out vertically or horizontally. + */ +@interface TOPasscodeKeypadView : UIView + +/** The type of layout for the buttons (Default is vertical) */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/** The vibrancy effect to be applied to each button background */ +@property (nonatomic, strong, nullable) UIVibrancyEffect *vibrancyEffect; + +/** The size of each input button */ +@property (nonatomic, assign) CGFloat buttonDiameter; + +/** The stroke width of the buttons */ +@property (nonatomic, assign) CGFloat buttonStrokeWidth; + +/** The spacing between the buttons. Default is (CGSize){25,15} */ +@property (nonatomic, assign) CGSize buttonSpacing; + +/** The font of the number in each button */ +@property (nonatomic, strong) UIFont *buttonNumberFont; + +/** The font of the lettering label */ +@property (nonatomic, strong) UIFont *buttonLetteringFont; + +/** The spacing between the lettering and the number label */ +@property (nonatomic, assign) CGFloat buttonLabelSpacing; + +/** The spacing between the letters in the lettering label */ +@property (nonatomic, assign) CGFloat buttonLetteringSpacing; + +/** Show the 'ABC' lettering under the numbers */ +@property (nonatomic, assign) BOOL showLettering; + +/** The spacing in points between the letters */ +@property (nonatomic, assign) CGFloat letteringSpacing; + +/** The tint color of the button backgrounds */ +@property (nonatomic, strong) UIColor *buttonBackgroundColor; + +/** The color of the text elements in each button */ +@property (nonatomic, strong) UIColor *buttonTextColor; + +/** Optionally the color of text when it's tapped. */ +@property (nonatomic, strong, nullable) UIColor *buttonHighlightedTextColor; + +/** The alpha value of all non-translucent views */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/** Accessory views placed on either side of the '0' button */ +@property (nonatomic, strong, nullable) UIView *leftAccessoryView; +@property (nonatomic, strong, nullable) UIView *rightAccessoryView; + +/** The controls making up each of the button views */ +@property (nonatomic, readonly) NSArray *keypadButtons; + +/** The block that is triggered whenever a user taps one of the buttons */ +@property (nonatomic, copy) void (^buttonTappedHandler)(NSInteger buttonNumber); + +/* + Perform an animation to transition to a new layout. + + @param horizontalLayout The content is laid out horizontally. + @param animated Whether the transition is animated + @param duration The animation length of the transition. + */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m new file mode 100644 index 0000000000..a9cf0bb511 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m @@ -0,0 +1,432 @@ +// +// TOPasscodeKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeKeypadView.h" +#import "TOPasscodeCircleImage.h" +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeKeypadView() + +/* Passcode buttons */ +@property (nonatomic, strong, readwrite) NSArray *keypadButtons; + +/* The '0' button for the different layouts */ +@property (nonatomic, strong) TOPasscodeCircleButton *verticalZeroButton; +@property (nonatomic, strong) TOPasscodeCircleButton *horizontalZeroButton; + +/* Images */ +@property (nonatomic, strong) UIImage *buttonImage; +@property (nonatomic, strong) UIImage *tappedButtonImage; + +@end + +@implementation TOPasscodeKeypadView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.userInteractionEnabled = YES; + _buttonDiameter = 81.0f; + _buttonSpacing = (CGSize){25,15}; + _buttonStrokeWidth = 1.5f; + _showLettering = YES; + _buttonNumberFont = nil; + _buttonLetteringFont = nil; + _buttonLabelSpacing = FLT_MIN; + _buttonLetteringSpacing = FLT_MIN; + [self sizeToFit]; + } + + return self; +} + +- (TOPasscodeCircleButton *)makeCircleButtonWithNumber:(NSInteger)number letteringString:(NSString *)letteringString +{ + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; + + TOPasscodeCircleButton *circleButton = [[TOPasscodeCircleButton alloc] initWithNumberString:numberString letteringString:letteringString]; + circleButton.backgroundImage = self.buttonImage; + circleButton.hightlightedBackgroundImage = self.tappedButtonImage; + circleButton.vibrancyEffect = self.vibrancyEffect; + + // Add handler for when button is tapped + __weak typeof(self) weakSelf = self; + circleButton.buttonTappedHandler = ^{ + if (weakSelf.buttonTappedHandler) { + weakSelf.buttonTappedHandler(number); + } + }; + + return circleButton; +} + +- (void)setUpButtons +{ + NSMutableArray *buttons = [NSMutableArray array]; + + NSInteger numberOfButtons = 11; // 1-9 are normal, 10 is the vertical '0', 11 is the horizontal '0' + NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", + @"MNO", @"PQRS", @"TUV", @"WXYZ"]; + + for (NSInteger i = 0; i < numberOfButtons; i++) { + // Work out the button number text + NSInteger buttonNumber = i + 1; + if (buttonNumber == 10 || buttonNumber == 11) { buttonNumber = 0; } + + // Work out the lettering text + NSString *letteringString = nil; + if (self.showLettering && i > 0 && i-1 < letteredTitles.count) { // (Skip 1 and 0) + letteringString = letteredTitles[i-1]; + } + + // Create a new button and add it to this view + TOPasscodeCircleButton *circleButton = [self makeCircleButtonWithNumber:buttonNumber letteringString:letteringString]; + [self addSubview:circleButton]; + [buttons addObject:circleButton]; + + if (!self.showLettering) { + circleButton.buttonLabel.verticallyCenterNumberLabel = YES; // Center the digit in the middle + } + + // Hang onto the 0 button if it's the vertical one + // And center the text + if (i == 9) { + self.verticalZeroButton = circleButton; + + // Hide the button if it's not vertically laid out + if (self.horizontalLayout) { + self.verticalZeroButton.contentAlpha = 0.0f; + self.verticalZeroButton.hidden = YES; + } + } + else if (i == 10) { + self.horizontalZeroButton = circleButton; + + // Hide the button if it's not horizontally laid out + if (!self.horizontalLayout) { + self.horizontalZeroButton.contentAlpha = 0.0f; + self.horizontalZeroButton.hidden = YES; + } + } + } + + _keypadButtons = [NSArray arrayWithArray:buttons]; +} + +- (void)sizeToFit +{ + CGFloat padding = 2.0f; + + CGRect frame = self.frame; + if (self.horizontalLayout) { + frame.size.width = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.width * 3); + frame.size.height = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.height * 2); + } + else { + frame.size.width = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.width * 2); + frame.size.height = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.height * 3); + } + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + NSInteger i = 0; + CGPoint origin = CGPointZero; + for (TOPasscodeCircleButton *button in self.keypadButtons) { + // Set the button frame + CGRect frame = button.frame; + frame.origin = origin; + button.frame = frame; + + // Work out the next offset + CGFloat horizontalOffset = frame.size.width + self.buttonSpacing.width; + origin.x += horizontalOffset; + + i++; + + // If we're at the end of the row, move to the next one + if (i % 3 == 0) { + origin.x = 0.0f; + origin.y = origin.y + frame.size.height + self.buttonSpacing.height; + } + } + + // Lay out the vertical button + CGRect frame = self.verticalZeroButton.frame; + frame.origin.x += (frame.size.width + self.buttonSpacing.width); + self.verticalZeroButton.frame = frame; + + // Lay out the horizontal button + frame = self.horizontalZeroButton.frame; + frame.origin.x = (frame.size.width + self.buttonSpacing.width) * 3.0f; + frame.origin.y = frame.size.height + self.buttonSpacing.height; + self.horizontalZeroButton.frame = frame; + + // Layout the accessory buttons + CGFloat midPointY = CGRectGetMidY(self.verticalZeroButton.frame); + + if (self.leftAccessoryView) { + CGRect leftButtonFrame = self.keypadButtons.firstObject.frame; + CGFloat midPointX = CGRectGetMidX(leftButtonFrame); + + [self.leftAccessoryView sizeToFit]; + self.leftAccessoryView.center = (CGPoint){midPointX, midPointY}; + } + + if (self.rightAccessoryView) { + CGRect rightButtonFrame = self.keypadButtons[2].frame; + CGFloat midPointX = CGRectGetMidX(rightButtonFrame); + + [self.rightAccessoryView sizeToFit]; + self.rightAccessoryView.center = (CGPoint){midPointX, midPointY}; + } +} + +#pragma mark - Style Accessors - +- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect +{ + if (vibrancyEffect == _vibrancyEffect) { return; } + _vibrancyEffect = vibrancyEffect; + + for (TOPasscodeCircleButton *button in self.keypadButtons) { + button.vibrancyEffect = _vibrancyEffect; + } +} + +#pragma mark - Lazy Getters - +- (UIImage *)buttonImage +{ + if (!_buttonImage) { + _buttonImage = [TOPasscodeCircleImage hollowCircleImageOfSize:self.buttonDiameter strokeWidth:self.buttonStrokeWidth padding:1.0f]; + } + + return _buttonImage; +} + +- (UIImage *)tappedButtonImage +{ + if (!_tappedButtonImage) { + _tappedButtonImage = [TOPasscodeCircleImage circleImageOfSize:self.buttonDiameter inset:self.buttonStrokeWidth * 0.5f padding:1.0f antialias:YES]; + } + + return _tappedButtonImage; +} + +- (NSArray *)keypadButtons +{ + if (_keypadButtons) { return _keypadButtons; } + [self setUpButtons]; + return _keypadButtons; +} + +#pragma mark - Audio Delegate Protocol - +- (BOOL)enableInputClicksWhenVisible +{ + return YES; +} + +#pragma mark - Public Layout Setters - + +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (horizontalLayout== _horizontalLayout) { + return; + } + + _horizontalLayout = horizontalLayout; + + // Resize itself now so the frame value is up to date externally + [self sizeToFit]; + + // Set initial animation state + self.verticalZeroButton.hidden = NO; + self.horizontalZeroButton.hidden = NO; + + self.verticalZeroButton.contentAlpha = _horizontalLayout ? 1.0f : 0.0f; + self.horizontalZeroButton.contentAlpha = _horizontalLayout ? 0.0f : 1.0f; + + void (^animationBlock)(void) = ^{ + self.verticalZeroButton.contentAlpha = self.horizontalLayout ? 0.0f : 1.0f; + self.horizontalZeroButton.contentAlpha = self.horizontalLayout ? 1.0f : 0.0f; + }; + + void (^completionBlock)(BOOL) = ^(BOOL complete) { + self.verticalZeroButton.hidden = self.horizontalLayout; + self.horizontalZeroButton.hidden = self.horizontalLayout; + }; + + // Don't animate if not needed + if (!animated) { + animationBlock(); + completionBlock(YES); + return; + } + + // Perform animation + [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; +} + +- (void)updateButtonsForCurrentState +{ + for (TOPasscodeCircleButton *circleButton in self.keypadButtons) { + circleButton.backgroundImage = self.buttonImage; + circleButton.hightlightedBackgroundImage = self.tappedButtonImage; + circleButton.numberFont = self.buttonNumberFont; + circleButton.letteringFont = self.buttonLetteringFont; + circleButton.letteringVerticalSpacing = self.buttonLabelSpacing; + circleButton.letteringCharacterSpacing = self.buttonLetteringSpacing; + circleButton.tintColor = self.buttonBackgroundColor; + circleButton.textColor = self.buttonTextColor; + circleButton.highlightedTextColor = self.buttonHighlightedTextColor; + if (!_showLettering) { + circleButton.buttonLabel.letteringLabel.text = nil; + circleButton.buttonLabel.verticallyCenterNumberLabel = YES; + } + } + + [self setNeedsLayout]; +} + +- (void)setButtonDiameter:(CGFloat)buttonDiameter +{ + if (_buttonDiameter == buttonDiameter) { return; } + _buttonDiameter = buttonDiameter; + _tappedButtonImage = nil; + _buttonImage = nil; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonSpacing:(CGSize)buttonSpacing +{ + if (CGSizeEqualToSize(_buttonSpacing, buttonSpacing)) { return; } + _buttonSpacing = buttonSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonStrokeWidth:(CGFloat)buttonStrokeWidth +{ + if (_buttonStrokeWidth== buttonStrokeWidth) { return; } + _buttonStrokeWidth = buttonStrokeWidth; + _tappedButtonImage = nil; + _buttonImage = nil; + [self updateButtonsForCurrentState]; +} + +- (void)setShowLettering:(BOOL)showLettering +{ + if (_showLettering == showLettering) { return; } + _showLettering = showLettering; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonNumberFont:(UIFont *)buttonNumberFont +{ + if (_buttonNumberFont == buttonNumberFont) { return; } + _buttonNumberFont = buttonNumberFont; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLetteringFont:(UIFont *)buttonLetteringFont +{ + if (buttonLetteringFont == _buttonLetteringFont) { return; } + _buttonLetteringFont = buttonLetteringFont; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLabelSpacing:(CGFloat)buttonLabelSpacing +{ + if (buttonLabelSpacing == _buttonLabelSpacing) { return; } + _buttonLabelSpacing = buttonLabelSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLetteringSpacing:(CGFloat)buttonLetteringSpacing +{ + if (buttonLetteringSpacing == _buttonLetteringSpacing) { return; } + _buttonLetteringSpacing = buttonLetteringSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor +{ + if (buttonBackgroundColor == _buttonBackgroundColor) { return; } + _buttonBackgroundColor = buttonBackgroundColor; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonTextColor:(UIColor *)buttonTextColor +{ + if (buttonTextColor == _buttonTextColor) { return; } + _buttonTextColor = buttonTextColor; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonHighlightedTextColor:(UIColor *)buttonHighlightedTextColor +{ + if (buttonHighlightedTextColor == _buttonHighlightedTextColor) { return; } + _buttonHighlightedTextColor = buttonHighlightedTextColor; + [self updateButtonsForCurrentState]; +} + +- (void)setLeftAccessoryView:(UIView *)leftAccessoryView +{ + if (_leftAccessoryView == leftAccessoryView) { return; } + _leftAccessoryView = leftAccessoryView; + [self addSubview:_leftAccessoryView]; + [self setNeedsLayout]; +} + +- (void)setRightAccessoryView:(UIView *)rightAccessoryView +{ + if (_rightAccessoryView == rightAccessoryView) { return; } + _rightAccessoryView = rightAccessoryView; + [self addSubview:_rightAccessoryView]; + [self setNeedsLayout]; +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + + for (TOPasscodeCircleButton *button in self.keypadButtons) { + // Skip whichever '0' button is not presently being used + if ((self.horizontalLayout && button == self.verticalZeroButton) || + (!self.horizontalLayout && button == self.horizontalZeroButton)) + { + continue; + } + + button.contentAlpha = contentAlpha; + } +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h new file mode 100644 index 0000000000..b027ee853b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h @@ -0,0 +1,136 @@ +// +// TOPasscodeView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" +#import "TOPasscodeKeypadView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeCircleButton; +@class TOPasscodeInputField; +@class TOPasscodeKeypadView; +@class TOPasscodeViewContentLayout; + +/** + The passcode view is the primary content view for the passcode view controller. + On iPad, every view except the background view is a subview of this view. + On iPhone, the auxiliary buttons ('Touch ID', 'Cancel') are managed by the view controller. + */ +@interface TOPasscodeView : UIView + +/* The visual style of the view */ +@property (nonatomic, assign) TOPasscodeViewStyle style; + +/* The type of passcode being managed by it */ +@property (nonatomic, readonly) TOPasscodeType passcodeType; + +/* Whether the content is laid out vertically or horizontally (iPhone only) */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/* The text in the title view (Default is 'Enter Passcode') */ +@property (nonatomic, copy) NSString *titleText; +@property (nonatomic, copy) NSString *subtitleText; + +/* Customizable Accessory Views */ +@property (nonatomic, strong, nullable) UIView *titleView; +@property (nonatomic, strong, nullable) UIButton *leftButton; +@property (nonatomic, strong, nullable) UIButton *rightButton; + +/* The default views always shown in this view */ +@property (nonatomic, readonly) UILabel *titleLabel; +@property (nonatomic, readonly) UILabel *subtitleLabel; +@property (nonatomic, readonly) TOPasscodeInputField *inputField; +@property (nonatomic, readonly) TOPasscodeKeypadView *keypadView; + +/* Overrides for theming the various elements. */ +@property (nonatomic, strong, nullable) UIColor *titleLabelColor; +@property (nonatomic, strong, nullable) UIColor *subtitleLabelColor; +@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; + +/* Horizontal inset from edge of keypad view to button center */ +@property (nonatomic, readonly) CGFloat keypadButtonInset; + +/* An animatable property for animating the non-translucent subviews */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/* The passcode currently entered into this view */ +@property (nonatomic, copy, nullable) NSString *passcode; + +/* The default layout object controlling the + sizing and placement of all this view's child elements. */ +@property (nonatomic, strong, null_resettable) TOPasscodeViewContentLayout *defaultContentLayout; + +/* As needed, additional layout objects that will be checked and used in priority over the default content layout. */ +@property (nonatomic, strong, nullable) NSArray *contentLayouts; + +/* Callback triggered each time the user taps a key */ +@property (nonatomic, copy, nullable) void (^passcodeDigitEnteredHandler)(void); + +/* Callback triggered when the user has finished entering the passcode */ +@property (nonatomic, copy, nullable) void (^passcodeCompletedHandler)(NSString *passcode); + +/* + Create a new instance with one of the style types + + @param style The visual style of the passcode view. + @param type The type of passcode to accept. + */ +- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type; + +/* + Resize the view and all subviews for the optimum size to fit a super view of the suplied width. + + @param size The size of the view to which this view. + */ +- (void)sizeToFitSize:(CGSize)size; + +/* + Reset the passcode to nil and optionally play animation / vibration to match + + @param animated Play a shaking animation to reset the passcode. + @param impact On supported devices, play a small reset vibration as well. + */ +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; + +/* + Delete the last character from the passcode + + @param animated Whether the delete operation is animated or not. + */ +- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated; + +/* + Animate the transition between horizontal and vertical layouts + + @param horizontalLayout Whether to lay out the content vertically or horizontally. + @param animated Whether the transition is animated or not. + @param duration The duration of the animation + */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m new file mode 100644 index 0000000000..aad42bf5f1 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m @@ -0,0 +1,690 @@ +// +// TOPasscodeView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeView.h" +#import "TOPasscodeViewContentLayout.h" +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeInputField.h" +#import "TOPasscodeKeypadView.h" + +@interface TOPasscodeView () + +/* The current layout object used to configure this view */ +@property (nonatomic, weak) TOPasscodeViewContentLayout *currentLayout; + +/* The main views */ +@property (nonatomic, strong, readwrite) UILabel *titleLabel; +@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; +@property (nonatomic, strong, readwrite) TOPasscodeInputField *inputField; +@property (nonatomic, strong, readwrite) TOPasscodeKeypadView *keypadView; + +/* The type of passcode we're displaying */ +@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; + +@end + +@implementation TOPasscodeView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type +{ + if (self = [super initWithFrame:CGRectMake(0,0,320,393)]) { + _style = style; + _passcodeType = type; + [self setUp]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + if (self = [super initWithCoder:aDecoder]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + // Set up default properties + self.userInteractionEnabled = YES; + _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; + _currentLayout = _defaultContentLayout; + _contentLayouts = @[[TOPasscodeViewContentLayout mediumScreenContentLayout], + [TOPasscodeViewContentLayout smallScreenContentLayout]]; + _titleText = NSLocalizedString(@"Enter Passcode", @""); + + // Start configuring views + [self setUpViewForType:self.passcodeType]; + + // Set the default layout for the views + [self updateSubviewsForContentLayout:_defaultContentLayout]; + + // Configure the theme of all of the views + [self applyThemeForStyle:_style]; +} + +#pragma mark - View Layout - +- (void)verticallyLayoutSubviews +{ + CGSize viewSize = self.frame.size; + CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; + + CGRect frame = CGRectZero; + CGFloat y = 0.0f; + + // Title View + if (self.titleView) { + frame = self.titleView.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.titleView.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.titleViewBottomSpacing; + } + + // Title Label + frame = self.titleLabel.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.titleLabel.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.titleLabelBottomSpacing; + + // Circle Row View + [self.inputField sizeToFit]; + frame = self.inputField.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.inputField.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.circleRowBottomSpacing; + + // Subtitle Label + frame = self.subtitleLabel.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.subtitleLabel.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.subtitleLabelBottomSpacing; + + // PIN Pad View + if (self.keypadView) { + frame = self.keypadView.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.keypadView.frame = CGRectIntegral(frame); + } + + // If the keypad view is hidden, lay out the left button manually + if (!self.keypadView && self.leftButton) { + frame = self.leftButton.frame; + frame.origin.x = 0.0f; + frame.origin.y = y; + self.leftButton.frame = frame; + } + + // If the keypad view is hidden, lay out the right button manually + if (!self.keypadView && self.rightButton) { + frame = self.rightButton.frame; + frame.origin.x = viewSize.width - frame.size.width; + frame.origin.y = y; + self.rightButton.frame = frame; + } +} + +- (void)horizontallyLayoutSubviews +{ + CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; + CGRect frame = CGRectZero; + + // Work out the y offset, assuming the input field is in the middle + frame.origin.y = midViewSize.height - (self.inputField.frame.size.height * 0.5f); + frame.origin.y -= (self.titleLabel.frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); + + // Include offset for title view if present + if (self.titleView) { + frame.origin.y -= (self.titleView.frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); + } + + // Set initial Y offset + frame.origin.y = MAX(frame.origin.y, 0.0f); + + // Set frame of title view + if (self.titleView) { + frame.size = self.titleView.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.titleView.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); + } + + // Set frame of title label + frame.size = self.titleLabel.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.subtitleLabelHorizontalBottomSpacing); + + // Set frame of subtitle label + frame.size = self.subtitleLabel.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.subtitleLabel.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); + + // Set frame of the input field + frame.size = self.inputField.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.inputField.frame = CGRectIntegral(frame); + + // Set the frame of the keypad view + frame.size = self.keypadView.frame.size; + frame.origin.y = 0.0f; + frame.origin.x = self.currentLayout.titleHorizontalLayoutWidth + self.currentLayout.titleHorizontalLayoutSpacing; + self.keypadView.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + if (self.horizontalLayout) { + [self horizontallyLayoutSubviews]; + } + else { + [self verticallyLayoutSubviews]; + } +} + +- (void)sizeToFitSize:(CGSize)size +{ + CGFloat width = size.width; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + width = MIN(size.width, size.height); + } + + NSMutableArray *layouts = [NSMutableArray array]; + [layouts addObject:self.defaultContentLayout]; + [layouts addObjectsFromArray:self.contentLayouts]; + + // Loop through each layout (in ascending order) and pick the best one to fit this view + TOPasscodeViewContentLayout *contentLayout = self.defaultContentLayout; + for (TOPasscodeViewContentLayout *layout in layouts) { + if (width >= layout.viewWidth) { + contentLayout = layout; + break; + } + } + + // Set the new layout + self.currentLayout = contentLayout; + + // Resize the views to fit + [self sizeToFit]; +} + +- (void)verticalSizeToFit +{ + CGRect frame = self.frame; + frame.size.width = 0.0f; + frame.size.height = 0.0f; + + [self.keypadView sizeToFit]; + [self.inputField sizeToFit]; + + if (self.keypadView) { + frame.size.width = self.keypadView.frame.size.width; + } + else { + frame.size.width = self.inputField.frame.size.width; + } + + // Add height for the title view + if (self.titleView) { + frame.size.height += self.titleView.frame.size.height; + frame.size.height += self.currentLayout.titleViewBottomSpacing; + } + + // Add height for the title label + CGRect titleFrame = self.titleLabel.frame; + titleFrame.size = [self.titleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; + self.titleLabel.frame = titleFrame; + + frame.size.height += titleFrame.size.height; + frame.size.height += self.currentLayout.titleLabelBottomSpacing; + + // Add height for the subtitle label + CGRect subtitleFrame = self.subtitleLabel.frame; + subtitleFrame.size = [self.subtitleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; + self.subtitleLabel.frame = subtitleFrame; + + frame.size.height += subtitleFrame.size.height; + frame.size.height += self.currentLayout.subtitleLabelBottomSpacing; + + // Add height for the circle rows + frame.size.height += self.inputField.frame.size.height; + frame.size.height += self.currentLayout.circleRowBottomSpacing; + + // Add height for the keypad + if (self.keypadView) { + frame.size.height += self.keypadView.frame.size.height; + } + else { // If no keypad, just factor in the accessory buttons + [self.leftButton sizeToFit]; + [self.rightButton sizeToFit]; + + CGFloat maxHeight = 0.0f; + maxHeight = MAX(self.leftButton.frame.size.height, 0.0f); + maxHeight = MAX(self.rightButton.frame.size.height, maxHeight); + + frame.size.height += maxHeight; + } + + // Add extra padding at the bottom + frame.size.height += self.currentLayout.bottomPadding; + + // Set the frame back + self.frame = CGRectIntegral(frame); +} + +- (void)horizontalSizeToFit +{ + CGRect frame = self.frame; + + [self.keypadView sizeToFit]; + [self.inputField sizeToFit]; + + frame.size.width = self.currentLayout.titleHorizontalLayoutWidth; + frame.size.width += self.currentLayout.titleHorizontalLayoutSpacing; + frame.size.width += self.keypadView.frame.size.width; + + frame.size.height = self.keypadView.frame.size.height; + + self.frame = CGRectIntegral(frame); +} + +- (void)sizeToFit +{ + if (self.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + [self horizontalSizeToFit]; + } + else { + [self verticalSizeToFit]; + } +} + +#pragma mark - View Setup - +- (void)setUpViewForType:(TOPasscodeType)type +{ + __weak typeof(self) weakSelf = self; + + self.backgroundColor = [UIColor clearColor]; + + // Set up title label + if (self.titleLabel == nil) { + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + } + self.titleLabel.text = self.titleText; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.numberOfLines = 0; + [self.titleLabel sizeToFit]; + [self addSubview:self.titleLabel]; + + // Set up the passcode style + TOPasscodeInputFieldStyle style = TOPasscodeInputFieldStyleFixed; + if (type >= TOPasscodeTypeCustomNumeric) { + style = TOPasscodeInputFieldStyleVariable; + } + + // Set up input field + if (self.inputField == nil) { + self.inputField = [[TOPasscodeInputField alloc] initWithStyle:style]; + } + self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { + if (weakSelf.passcodeCompletedHandler) { + weakSelf.passcodeCompletedHandler(passcode); + } + }; + + // Configure the input field based on the exact passcode type + if (style == TOPasscodeInputFieldStyleFixed) { + self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; + } + else { + self.inputField.showSubmitButton = (self.passcodeType == TOPasscodeTypeCustomNumeric); + self.inputField.enabled = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric); + } + + [self addSubview:self.inputField]; + + // Set up subtitle label + if (self.subtitleLabel == nil) { + self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + } + self.subtitleLabel.text = self.subtitleText; + self.subtitleLabel.textAlignment = NSTextAlignmentCenter; + self.subtitleLabel.numberOfLines = 0; + [self.subtitleLabel sizeToFit]; + [self addSubview:self.subtitleLabel]; + + // Set up pad row + if (type != TOPasscodeTypeCustomAlphanumeric) { + if (self.keypadView == nil) { + self.keypadView = [[TOPasscodeKeypadView alloc] init]; + } + self.keypadView.buttonTappedHandler = ^(NSInteger button) { + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)button]; + [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; + + if (weakSelf.passcodeDigitEnteredHandler) { + weakSelf.passcodeDigitEnteredHandler(); + } + }; + [self addSubview:self.keypadView]; + } + else { + [self.keypadView removeFromSuperview]; + self.keypadView = nil; + } +} + +- (void)updateSubviewsForContentLayout:(TOPasscodeViewContentLayout *)contentLayout +{ + // Title View + self.titleLabel.font = contentLayout.titleLabelFont; + + // Subtitle View + self.subtitleLabel.font = contentLayout.subtitleLabelFont; + + // Circle Row View + self.inputField.fixedInputView.circleDiameter = contentLayout.circleRowDiameter; + self.inputField.fixedInputView.circleSpacing = contentLayout.circleRowSpacing; + + // Text Field Input Row + NSInteger maximumInputLength = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) ? + contentLayout.textFieldAlphanumericCharacterLength : + contentLayout.textFieldNumericCharacterLength; + + self.inputField.variableInputView.outlineThickness = contentLayout.textFieldBorderThickness; + self.inputField.variableInputView.outlineCornerRadius = contentLayout.textFieldBorderRadius; + self.inputField.variableInputView.circleDiameter = contentLayout.textFieldCircleDiameter; + self.inputField.variableInputView.circleSpacing = contentLayout.textFieldCircleSpacing; + self.inputField.variableInputView.outlinePadding = contentLayout.textFieldBorderPadding; + self.inputField.variableInputView.maximumVisibleLength = maximumInputLength; + + // Submit button + self.inputField.submitButtonSpacing = contentLayout.submitButtonSpacing; + self.inputField.submitButtonFontSize = contentLayout.submitButtonFontSize; + + // Keypad + self.keypadView.buttonNumberFont = contentLayout.circleButtonTitleLabelFont; + self.keypadView.buttonLetteringFont = contentLayout.circleButtonLetteringLabelFont; + self.keypadView.buttonLetteringSpacing = contentLayout.circleButtonLetteringSpacing; + self.keypadView.buttonLabelSpacing = contentLayout.circleButtonLabelSpacing; + self.keypadView.buttonSpacing = contentLayout.circleButtonSpacing; + self.keypadView.buttonDiameter = contentLayout.circleButtonDiameter; +} + +- (void)applyThemeForStyle:(TOPasscodeViewStyle)style +{ + BOOL isTranslucent = TOPasscodeViewStyleIsTranslucent(style); + BOOL isDark = TOPasscodeViewStyleIsDark(style); + + // Set title label color + UIColor *titleLabelColor = self.titleLabelColor; + if (titleLabelColor == nil) { + titleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.titleLabel.textColor = titleLabelColor; + + // Set subtitle label color + UIColor *subtitleLabelColor = self.subtitleLabelColor; + if (subtitleLabelColor == nil) { + subtitleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.subtitleLabel.textColor = subtitleLabelColor; + + // Add/remove the translucency effect to the buttons + if (isTranslucent) { + UIBlurEffect *blurEffect = [self blurEffectForStyle:style]; + UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect]; + self.inputField.visualEffectView.effect = vibrancyEffect; + self.keypadView.vibrancyEffect = vibrancyEffect; + } + else { + self.inputField.visualEffectView.effect = nil; + self.keypadView.vibrancyEffect = nil; + } + + // Set keyboard style of the input field + self.inputField.keyboardAppearance = isDark ? UIKeyboardAppearanceDark : UIKeyboardAppearanceDefault; + + UIColor *defaultTintColor = isDark ? [UIColor colorWithWhite:0.85 alpha:1.0f] : [UIColor colorWithWhite:0.3 alpha:1.0f]; + + // Set the tint color of the circle row view + UIColor *circleRowColor = self.inputProgressViewTintColor; + if (circleRowColor == nil) { + circleRowColor = defaultTintColor; + } + self.inputField.tintColor = defaultTintColor; + + // Set the tint color of the keypad buttons + UIColor *keypadButtonBackgroundColor = self.keypadButtonBackgroundColor; + if (keypadButtonBackgroundColor == nil) { + keypadButtonBackgroundColor = defaultTintColor; + } + self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; + + // Set the color of the keypad button labels + UIColor *buttonTextColor = self.keypadButtonTextColor; + if (buttonTextColor == nil) { + buttonTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.keypadView.buttonTextColor = buttonTextColor; + + // Set the highlight color of the keypad button + UIColor *buttonHighlightedTextColor = self.keypadButtonHighlightedTextColor; + if (buttonHighlightedTextColor == nil) { + if (isTranslucent) { + buttonHighlightedTextColor = isDark ? nil : [UIColor whiteColor]; + } + else { + buttonHighlightedTextColor = isDark ? [UIColor blackColor] : [UIColor whiteColor]; + } + } + self.keypadView.buttonHighlightedTextColor = buttonHighlightedTextColor; +} + +#pragma mark - Passcode Management - +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact +{ + [self.inputField resetPasscodeAnimated:animated playImpact:impact]; +} + +- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated +{ + [self.inputField deletePasscodeCharactersOfCount:1 animated:animated]; +} + +#pragma mark - Internal Style Management - +- (UIBlurEffect *)blurEffectForStyle:(TOPasscodeViewStyle)style +{ + switch (style) { + case TOPasscodeViewStyleTranslucentDark: + return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + case TOPasscodeViewStyleTranslucentLight: + return [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]; + default: return nil; + } + + return nil; +} + +#pragma mark - Accessors - +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (horizontalLayout == _horizontalLayout) { return; } + _horizontalLayout = horizontalLayout; + [self.keypadView setHorizontalLayout:horizontalLayout animated:animated duration:duration]; + [self.inputField setHorizontalLayout:horizontalLayout animated:animated duration:duration]; +} + +- (void)setDefaultContentLayout:(TOPasscodeViewContentLayout *)defaultContentLayout +{ + if (defaultContentLayout == _defaultContentLayout) { return; } + _defaultContentLayout = defaultContentLayout; + + if (!_defaultContentLayout) { + _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; + } +} + +- (void)setCurrentLayout:(TOPasscodeViewContentLayout *)currentLayout +{ + if (_currentLayout == currentLayout) { return; } + _currentLayout = currentLayout; + + // Update the views + [self updateSubviewsForContentLayout:currentLayout]; +} + +- (void)setStyle:(TOPasscodeViewStyle)style +{ + if (style == _style) { return; } + _style = style; + [self applyThemeForStyle:style]; +} + +- (void)setTitleLabelColor:(UIColor *)titleLabelColor +{ + if (titleLabelColor == _titleLabelColor) { return; } + _titleLabelColor = titleLabelColor; + self.titleLabel.textColor = titleLabelColor; +} + +- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor +{ + if (subtitleLabelColor == _subtitleLabelColor) { return; } + _subtitleLabelColor = subtitleLabelColor; + self.subtitleLabel.textColor = subtitleLabelColor; +} + +- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor +{ + if (inputProgressViewTintColor == _inputProgressViewTintColor) { return; } + _inputProgressViewTintColor = inputProgressViewTintColor; + self.inputField.tintColor = inputProgressViewTintColor; +} + +- (void)setKeypadButtonBackgroundColor:(UIColor *)keypadButtonBackgroundColor +{ + if (keypadButtonBackgroundColor == _keypadButtonBackgroundColor) { return; } + _keypadButtonBackgroundColor = keypadButtonBackgroundColor; + self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; +} + +- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor +{ + if (keypadButtonTextColor == _keypadButtonTextColor) { return; } + _keypadButtonTextColor = keypadButtonTextColor; + self.keypadView.buttonTextColor = keypadButtonTextColor; +} + +- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor +{ + if (keypadButtonHighlightedTextColor == _keypadButtonHighlightedTextColor) { return; } + _keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; + self.keypadView.buttonHighlightedTextColor = keypadButtonHighlightedTextColor; +} + +- (void)setLeftButton:(UIButton *)leftButton +{ + if (leftButton == _leftButton) { return; } + _leftButton = leftButton; + + if (self.keypadView) { + self.keypadView.leftAccessoryView = leftButton; + } + else { + [self addSubview:_leftButton]; + } +} + +- (void)setRightButton:(UIButton *)rightButton +{ + if (rightButton == _rightButton) { return; } + _rightButton = rightButton; + + if (self.keypadView) { + self.keypadView.rightAccessoryView = rightButton; + } + else { + [self addSubview:_rightButton]; + } +} + +- (CGFloat)keypadButtonInset +{ + UIView *button = self.keypadView.keypadButtons.firstObject; + return CGRectGetMidX(button.frame); +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + + self.titleView.alpha = contentAlpha; + self.titleLabel.alpha = contentAlpha; + self.subtitleLabel.alpha = contentAlpha; + self.inputField.contentAlpha = contentAlpha; + self.keypadView.contentAlpha = contentAlpha; + self.keypadView.leftAccessoryView.alpha = contentAlpha; + self.keypadView.rightAccessoryView.alpha = contentAlpha; + self.leftButton.alpha = contentAlpha; + self.rightButton.alpha = contentAlpha; +} + +- (void)setPasscode:(NSString *)passcode +{ + [self.inputField setPasscode:passcode]; +} + +- (NSString *)passcode +{ + return self.inputField.passcode; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h new file mode 100644 index 0000000000..8179622f61 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h @@ -0,0 +1,45 @@ +// +// TOPasscodeSettingsKeypadButton.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeButtonLabel; + +/** + A single button view that is used in a number keypad views styled in + a pseudo-skeuomorphic style. + */ +@interface TOPasscodeSettingsKeypadButton : UIButton + +/** Background Images */ +@property (nonatomic, strong) UIImage *buttonBackgroundImage; +@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; + +/* Inset of the label view from the bottom to account for the bevel */ +@property (nonatomic, assign) CGFloat bottomInset; + +/* The button label containing the number and lettering */ +@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; + ++ (TOPasscodeSettingsKeypadButton *)button; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m new file mode 100644 index 0000000000..fa41bd4587 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m @@ -0,0 +1,103 @@ +// +// TOPasscodeSettingsKeypadButton.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsKeypadButton.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeSettingsKeypadButton () + +@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; + +@end + +@implementation TOPasscodeSettingsKeypadButton + ++ (TOPasscodeSettingsKeypadButton *)button +{ + TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton buttonWithType:UIButtonTypeCustom]; + button.frame = CGRectMake(0,0,100,60); + return button; +} + +#pragma mark - Lazy Accessor - +- (TOPasscodeButtonLabel *)buttonLabel +{ + if (_buttonLabel) { return _buttonLabel; } + + CGRect frame = self.bounds; + frame.size.height -= self.bottomInset; + + _buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:frame]; + _buttonLabel.userInteractionEnabled = NO; + _buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_buttonLabel]; + + return _buttonLabel; +} + +#pragma mark - Layout Accessor - +- (void)setBottomInset:(CGFloat)bottomInset +{ + _bottomInset = bottomInset; + + CGRect frame = self.bounds; + frame.size.height -= _bottomInset; + self.buttonLabel.frame = frame; + [self setNeedsLayout]; +} + +#pragma mark - Control Accessor - +- (void)setEnabled:(BOOL)enabled +{ + [super setEnabled:enabled]; + self.buttonLabel.alpha = enabled ? 1.0f : 0.5f; +} + +#pragma mark - Background Image Accessor - + +- (void)setHighlighted:(BOOL)highlighted { + [self.layer removeAllAnimations]; + [UIView transitionWithView:self + duration:0.25 + options:UIViewAnimationOptionTransitionCrossDissolve | + UIViewAnimationOptionAllowAnimatedContent | + UIViewAnimationOptionAllowUserInteraction + animations:^{ + [super setHighlighted:highlighted]; + } completion:nil]; +} + +- (void)setButtonBackgroundImage:(UIImage *)buttonBackgroundImage +{ + [self setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; +} + +- (UIImage *)buttonBackgroundImage { return [self backgroundImageForState:UIControlStateNormal]; } + +- (void)setButtonTappedBackgroundImage:(UIImage *)buttonTappedBackgroundImage +{ + [self setBackgroundImage:buttonTappedBackgroundImage forState:UIControlStateHighlighted]; +} + +- (UIImage *)buttonTappedBackgroundImage { return [self backgroundImageForState:UIControlStateHighlighted]; } + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h new file mode 100644 index 0000000000..13234bd07b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h @@ -0,0 +1,74 @@ +// +// TOPasscodeSettingsKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A keypad view of 9 buttons that allow numerical input on both iPad and iPhone. + Designed to match the base system, with a pseudo-skeuomorphical styling. + */ +@interface TOPasscodeSettingsKeypadView : UIView + +/* Whether the control is allowing input */ +@property (nonatomic, assign) BOOL enabled; + +/* Whether the view is currently light mode or dark. */ +@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; + +/* The color of the separator line */ +@property (nonatomic, strong) UIColor *separatorLineColor; + +/* Labels in the buttons are laid out horizontally */ +@property (nonatomic, assign) BOOL buttonLabelHorizontalLayout; + +/* If overridden, the foreground color of the buttons */ +@property (nonatomic, assign) CGFloat keypadButtonBorderThickness; + +/* Untapped background images */ +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonForegroundColor; +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonBorderColor; + +/* Tapped background images */ +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonTappedForegroundColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonTappedBorderColor; + +/* Button label styling */ +@property (nonatomic, strong) UIFont *keypadButtonNumberFont; +@property (nonatomic, strong) UIFont *keypadButtonLetteringFont; +@property (nonatomic, strong) UIColor *keypadButtonLabelTextColor; +@property (nonatomic, assign) CGFloat keypadButtonVerticalSpacing; +@property (nonatomic, assign) CGFloat keypadButtonHorizontalSpacing; +@property (nonatomic, assign) CGFloat keypadButtonLetteringSpacing; + +/* Callback handlers */ +@property (nonatomic, copy) void (^numberButtonTappedHandler)(NSInteger number); +@property (nonatomic, copy) void (^deleteButtonTappedHandler)(void); + +/* In really small sizes, set the keypad labels to horizontal */ +- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m new file mode 100644 index 0000000000..eda81bb61b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m @@ -0,0 +1,395 @@ +// +// TOPasscodeSettingsKeypadView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsKeypadView.h" +#import "TOPasscodeSettingsKeypadButton.h" +#import "TOPasscodeButtonLabel.h" +#import "TOSettingsKeypadImage.h" + +const CGFloat kTOPasscodeSettingsKeypadButtonInnerSpacing = 7.0f; +const CGFloat kTOPasscodeSettingsKeypadButtonOuterSpacing = 7.0f; +const CGFloat kTOPasscodeSettingsKeypadCornderRadius = 12.0f; + +@interface TOPasscodeSettingsKeypadView () + +@property (nonatomic, strong) UIView *separatorView; +@property (nonatomic, strong) NSArray *keypadButtons; +@property (nonatomic, strong) UIButton *deleteButton; + +@property (nonatomic, strong) UIImage *buttonBackgroundImage; +@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; + +@end + +@implementation TOPasscodeSettingsKeypadView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + /* Button label styling */ + _keypadButtonNumberFont = [UIFont systemFontOfSize:32.0f weight:UIFontWeightRegular]; + _keypadButtonLetteringFont = [UIFont systemFontOfSize:11.0f weight:UIFontWeightRegular]; + _keypadButtonVerticalSpacing = 2.0f; + _keypadButtonHorizontalSpacing = 3.0f; + _keypadButtonLetteringSpacing = 2.0f; + + CGSize viewSize = self.frame.size; + CGFloat height = 1.0f / [[UIScreen mainScreen] scale]; + self.separatorView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero,{viewSize.width, height}}]; + self.separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self addSubview:self.separatorView]; + + [self setUpKeypadButtons]; + [self setUpDeleteButton]; + + [self setUpDefaultValuesForStye:_style]; + [self applyTheme]; +} + +- (void)setUpKeypadButtons +{ + NSInteger numberOfButtons = 10; + NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", + @"MNO", @"PQRS", @"TUV", @"WXYZ"]; + + NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:10]; + for (NSInteger i = 0; i < numberOfButtons; i++) { + NSInteger number = (i+1) % 10; // Wrap around 0 at the end + TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton button]; + button.buttonLabel.numberString = [NSString stringWithFormat:@"%ld", (long)number]; + button.bottomInset = 2.0f; + button.tag = number; + + if (i > 0) { + NSInteger j = i - 1; + if (j < letteredTitles.count) { + button.buttonLabel.letteringString = letteredTitles[j]; + } + } + + [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown]; + + [self addSubview:button]; + [buttons addObject:button]; + } + + self.keypadButtons = [NSArray arrayWithArray:buttons]; +} + +- (void)setUpDeleteButton +{ + UIImage *deleteIcon = [TOSettingsKeypadImage deleteIcon]; + self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.deleteButton setImage:deleteIcon forState:UIControlStateNormal]; + self.deleteButton.contentMode = UIViewContentModeCenter; + self.deleteButton.frame = (CGRect){CGPointZero, deleteIcon.size}; + self.deleteButton.tintColor = [UIColor blackColor]; + [self.deleteButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:self.deleteButton]; +} + +- (void)setUpDefaultValuesForStye:(TOPasscodeSettingsViewStyle)style +{ + BOOL isDark = style == TOPasscodeSettingsViewStyleDark; + + // Keypad label + self.keypadButtonLabelTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + + self.keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.35f alpha:1.0f] : [UIColor whiteColor]; + self.keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.45f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; + + // Button border color + UIColor *borderColor = nil; + if (isDark) { + borderColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; + } + else { + borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; + } + self.keypadButtonBorderColor = borderColor; + + // Background Color + UIColor *backgroundColor = nil; + if (isDark) { + backgroundColor = [UIColor colorWithWhite:0.18f alpha:1.0f]; + } + else { + backgroundColor = [UIColor colorWithRed:220.0f/255.0f green:225.0f/255.0f blue:232.0f/255.0f alpha:1.0f]; + } + self.backgroundColor = backgroundColor; + + // Separator lines + UIColor *separatorColor = nil; + if (isDark) { + separatorColor = [UIColor colorWithWhite:0.25f alpha:1.0f]; + } + else { + separatorColor = [UIColor colorWithWhite:0.7f alpha:1.0f]; + } + self.separatorView.backgroundColor = separatorColor; + + self.deleteButton.tintColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; +} + +- (void)setUpImagesIfNeeded +{ + if (self.buttonBackgroundImage && self.buttonTappedBackgroundImage) { + return; + } + + if (self.buttonBackgroundImage == nil) { + self.buttonBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius + foregroundColor:self.keypadButtonForegroundColor + edgeColor:self.keypadButtonBorderColor + edgeThickness:2.0f]; + } + + if (self.buttonTappedBackgroundImage == nil) { + self.buttonTappedBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius + foregroundColor:self.keypadButtonTappedForegroundColor + edgeColor:self.keypadButtonBorderColor + edgeThickness:2.0f]; + } + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.buttonBackgroundImage = self.buttonBackgroundImage; + button.buttonTappedBackgroundImage = self.buttonTappedBackgroundImage; + } +} + +- (void)applyTheme +{ + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.buttonLabel.textColor = self.keypadButtonLabelTextColor; + button.buttonLabel.letteringCharacterSpacing = self.keypadButtonLetteringSpacing; + button.buttonLabel.letteringVerticalSpacing = self.keypadButtonVerticalSpacing; + button.buttonLabel.letteringHorizontalSpacing = self.keypadButtonHorizontalSpacing; + button.buttonLabel.numberLabelFont = self.keypadButtonNumberFont; + button.buttonLabel.letteringLabelFont = self.keypadButtonLetteringFont; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self setUpImagesIfNeeded]; + + CGFloat outerSpacing = kTOPasscodeSettingsKeypadButtonOuterSpacing; + CGFloat innerSpacing = kTOPasscodeSettingsKeypadButtonInnerSpacing; + + CGSize viewSize = self.bounds.size; + CGSize buttonSize = CGSizeZero; + + viewSize.width -= (outerSpacing * 2.0f); + viewSize.height -= (outerSpacing * 2.0f); + + // Pull the buttons up to avoid overlapping the home indicator on iPhone X + if (@available(iOS 11.0, *)) { + viewSize.height -= self.safeAreaInsets.bottom; + } + + // Four rows of three buttons + buttonSize.width = floorf((viewSize.width - (innerSpacing * 2.0f)) / 3.0f); + buttonSize.height = floorf((viewSize.height - (innerSpacing * 3.0f)) / 4.0f); + + CGPoint point = CGPointMake(outerSpacing, outerSpacing); + CGRect buttonFrame = (CGRect){point, buttonSize}; + + NSInteger i = 0; + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.frame = buttonFrame; + buttonFrame.origin.x += buttonFrame.size.width + innerSpacing; + + if (++i % 3 == 0) { + buttonFrame.origin.x = outerSpacing; + buttonFrame.origin.y += buttonFrame.size.height + innerSpacing; + } + + if (button == self.keypadButtons.lastObject) { + button.frame = buttonFrame; + } + } + + //Layout delete button + CGSize boundsSize = self.bounds.size; + + // Adjust for home indicator on iPhone X + if (@available(iOS 11.0, *)) { + boundsSize.height -= self.safeAreaInsets.bottom; + } + + CGRect frame = self.deleteButton.frame; + frame.size = buttonSize; + frame.origin.x = boundsSize.width - (outerSpacing + buttonSize.width * 0.5f); + frame.origin.x -= (CGRectGetWidth(frame) * 0.5f); + frame.origin.y = boundsSize.height - (outerSpacing + buttonSize.height * 0.5f); + frame.origin.y -= (CGRectGetHeight(frame) * 0.5f); + self.deleteButton.frame = frame; +} + +#pragma mark - Interaction - +- (void)buttonTapped:(id)sender +{ + // Handler for the delete button + if (sender == self.deleteButton) { + if (self.deleteButtonTappedHandler) { + self.deleteButtonTappedHandler(); + } + return; + } + + // Handler for the keypad buttons + UIButton *button = (UIButton *)sender; + NSInteger number = button.tag; + + [[UIDevice currentDevice] playInputClick]; + + if (self.numberButtonTappedHandler) { + self.numberButtonTappedHandler(number); + } +} + +#pragma mark - Accessors - + +- (void)setStyle:(TOPasscodeSettingsViewStyle)style +{ + if (style == _style) { + return; + } + + _style = style; + [self setUpDefaultValuesForStye:_style]; + [self applyTheme]; +} + +#pragma mark - Label Layout - +- (void)setButtonLabelHorizontalLayout:(BOOL)buttonLabelHorizontalLayout +{ + [self setButtonLabelHorizontalLayout:buttonLabelHorizontalLayout animated:NO]; +} + +- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated +{ + if (horizontal == _buttonLabelHorizontalLayout) { return; } + + _buttonLabelHorizontalLayout = horizontal; + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + if (!animated) { + button.buttonLabel.horizontalLayout = horizontal; + continue; + } + + UIView *snapshotView = [button.buttonLabel snapshotViewAfterScreenUpdates:NO]; + snapshotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [button addSubview:snapshotView]; + + button.buttonLabel.horizontalLayout = horizontal; + [button.buttonLabel setNeedsLayout]; + [button.buttonLabel layoutIfNeeded]; + + [button.buttonLabel.layer removeAllAnimations]; + for (CALayer *sublayer in button.buttonLabel.layer.sublayers) { + [sublayer removeAllAnimations]; + } + + button.buttonLabel.alpha = 0.0f; + [UIView animateWithDuration:0.4f animations:^{ + button.buttonLabel.alpha = 1.0f; + snapshotView.alpha = 0.0f; + snapshotView.center = button.buttonLabel.center; + } completion:^(BOOL complete) { + [snapshotView removeFromSuperview]; + }]; + } +} + +#pragma mark - Null Resettable Accessors - +- (void)setKeypadButtonForegroundColor:(nullable UIColor *)keypadButtonForegroundColor +{ + if (keypadButtonForegroundColor == _keypadButtonForegroundColor) { return; } + _keypadButtonForegroundColor = keypadButtonForegroundColor; + + if (_keypadButtonForegroundColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + _keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.3f alpha:1.0f] : [UIColor whiteColor]; + } + + self.buttonBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setKeypadButtonBorderColor:(nullable UIColor *)keypadButtonBorderColor +{ + if (keypadButtonBorderColor == _keypadButtonBorderColor) { return; } + _keypadButtonBorderColor = keypadButtonBorderColor; + + if (_keypadButtonBorderColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + UIColor *borderColor = nil; + if (isDark) { + borderColor = [UIColor colorWithWhite:0.2 alpha:1.0f]; + } + else { + borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; + } + _keypadButtonBorderColor = borderColor; + } + + self.buttonBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setKeypadButtonTappedForegroundColor:(nullable UIColor *)keypadButtonTappedForegroundColor +{ + if (keypadButtonTappedForegroundColor == _keypadButtonTappedForegroundColor) { return; } + _keypadButtonTappedForegroundColor = keypadButtonTappedForegroundColor; + + if (_keypadButtonTappedForegroundColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + _keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.4f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; + } + + self.buttonTappedBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setEnabled:(BOOL)enabled +{ + _enabled = enabled; + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.enabled = enabled; + } + + self.deleteButton.enabled = enabled; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h new file mode 100644 index 0000000000..5455a70586 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h @@ -0,0 +1,43 @@ +// +// TOPasscodeSettingsWarningLabel.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +/** + When a user enters an incorrect passcode in the settings interface, + this view is displayed to show the number of failed attempts. + */ +@interface TOPasscodeSettingsWarningLabel : UIImageView + +/** The number of incorrect passcode attempts to display */ +@property (nonatomic, assign) NSInteger numberOfWarnings; + +/** The font of the text */ +@property (nonatomic, strong) UIFont *textFont; + +/** The background color of the view */ +@property (nonatomic, strong) UIColor *backgroundColor; + +/** Set the padding around the label */ +@property (nonatomic, assign) CGSize textPadding; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m new file mode 100644 index 0000000000..266f360be6 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m @@ -0,0 +1,154 @@ +// +// TOPasscodeSettingsWarningLabel.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsWarningLabel.h" + +@interface TOPasscodeSettingsWarningLabel () +@property (nonatomic, strong) UILabel *label; +@end + +@implementation TOPasscodeSettingsWarningLabel + +@synthesize backgroundColor = __backgroundColor; + +#pragma mark - View Setup - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + _numberOfWarnings = 0; + _textPadding = CGSizeMake(14.0f, 6.0f); + + self.tintColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + + self.label = [[UILabel alloc] initWithFrame:CGRectZero]; + self.label.backgroundColor = [UIColor clearColor]; + self.label.textAlignment = NSTextAlignmentCenter; + self.label.textColor = [UIColor whiteColor]; + self.label.font = [UIFont systemFontOfSize:15.0f]; + [self setTextForCount:0]; + [self.label sizeToFit]; + [self addSubview:self.label]; +} + +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self setBackgroundImageIfNeeded]; +} + +#pragma mark - View Layout - + +- (void)sizeToFit +{ + [super sizeToFit]; + [self.label sizeToFit]; + + CGRect labelFrame = self.label.frame; + CGRect frame = self.frame; + + labelFrame = CGRectInset(labelFrame, -self.textPadding.width, -self.textPadding.height); + frame.size = labelFrame.size; + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + CGRect frame = self.frame; + CGRect labelFrame = self.label.frame; + + labelFrame.origin.x = (CGRectGetWidth(frame) - CGRectGetWidth(labelFrame)) * 0.5f; + labelFrame.origin.y = (CGRectGetHeight(frame) - CGRectGetHeight(labelFrame)) * 0.5f; + self.label.frame = labelFrame; +} + +#pragma mark - View State Handling - + +- (void)setTextForCount:(NSInteger)count +{ + NSString *text = nil; + if (count == 1) { + text = NSLocalizedString(@"1 Failed Passcode Attempt", @""); + } + else { + text = [NSString stringWithFormat:NSLocalizedString(@"%d Failed Passcode Attempts", @""), count]; + } + self.label.text = text; + + [self sizeToFit]; +} + +#pragma mark - Background Image Managements - + +- (void)setBackgroundImageIfNeeded +{ + // Don't bother if we're not in a view + if (self.superview == nil) { return; } + + // Compare the view height and don't proceed if + if (lround(self.image.size.height) == lround(self.frame.size.height)) { return; } + + // Create the image + self.image = [[self class] roundedBackgroundImageWithHeight:self.frame.size.height]; +} + ++ (UIImage *)roundedBackgroundImageWithHeight:(CGFloat)height +{ + UIImage *image = nil; + CGRect frame = CGRectZero; + frame.size.width = height + 1.0; + frame.size.height = height; + + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:height * 0.5f]; + [[UIColor blackColor] setFill]; + [path fill]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + CGFloat halfHeight = height * 0.5f; + UIEdgeInsets insets = UIEdgeInsetsMake(halfHeight, halfHeight, halfHeight, halfHeight); + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + image = [image resizableImageWithCapInsets:insets]; + return image; +} + +#pragma mark - Accessors - + +- (void)setNumberOfWarnings:(NSInteger)numberOfWarnings +{ + _numberOfWarnings = numberOfWarnings; + [self setTextForCount:_numberOfWarnings]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h new file mode 100644 index 0000000000..12b4d00fef --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h @@ -0,0 +1,61 @@ +// +// TOPasscodeButtonLabel.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A view that manages two label subviews: a larger label showing a single number + and a smaller label showing lettering as well. + */ +@interface TOPasscodeButtonLabel : UIView + +// Draws the lettering label to the side +@property (nonatomic, assign) BOOL horizontalLayout; + +// The strings of both labels +@property (nonatomic, copy) NSString *numberString; +@property (nonatomic, copy, nullable) NSString *letteringString; + +// The color of both labels +@property (nonatomic, strong) UIColor *textColor; + +// The label views +@property (nonatomic, readonly) UILabel *numberLabel; +@property (nonatomic, readonly) UILabel *letteringLabel; + +// The fonts for each label (In case they are nil) +@property (nonatomic, strong) UIFont *numberLabelFont; +@property (nonatomic, strong) UIFont *letteringLabelFont; + +// Has initial default values +@property (nonatomic, assign) CGFloat letteringCharacterSpacing; +@property (nonatomic, assign) CGFloat letteringVerticalSpacing; +@property (nonatomic, assign) CGFloat letteringHorizontalSpacing; + +// Whether the number label is centered vertically or not (NO by default) +@property (nonatomic, assign) BOOL verticallyCenterNumberLabel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m new file mode 100644 index 0000000000..d3df12ab45 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m @@ -0,0 +1,186 @@ +// +// TOPasscodeButtonLabel.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeButtonLabel () + +@property (nonatomic, strong, readwrite) UILabel *numberLabel; +@property (nonatomic, strong, readwrite) UILabel *letteringLabel; + +@end + +@implementation TOPasscodeButtonLabel + +#pragma mark - View Setup - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _letteringVerticalSpacing = 6.0f; + _letteringCharacterSpacing = 3.0f; + _letteringHorizontalSpacing = 5.0f; + _numberLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; + _letteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + [self setUpViews]; + } + + return self; +} + +- (void)setUpViews +{ + if (!self.numberLabel) { + self.numberLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.numberLabel.text = self.numberString; + self.numberLabel.textColor = self.textColor; + self.numberLabel.font = self.numberLabelFont; + [self.numberLabel sizeToFit]; + [self addSubview:self.numberLabel]; + } + + // Create the lettering string only if we have a lettering value for it + if (!self.letteringLabel && self.letteringString.length > 0) { + self.letteringLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.letteringLabel.textColor = self.textColor; + self.letteringLabel.font = self.letteringLabelFont; + [self.letteringLabel sizeToFit]; + [self addSubview:self.letteringLabel]; + [self updateLetteringLabelText]; + } +} + +#pragma mark - View Layout - + +- (void)updateLetteringLabelText +{ + if (self.letteringString.length == 0) { + return; + } + + NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:self.letteringString]; + [attrStr addAttribute:NSKernAttributeName value:@(_letteringCharacterSpacing) range:NSMakeRange(0, attrStr.length-1)]; + self.letteringLabel.attributedText = attrStr; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize viewSize = self.bounds.size; + + [self.numberLabel sizeToFit]; + [self.letteringLabel sizeToFit]; + + CGFloat numberVerticalHeight = self.numberLabelFont.capHeight; + CGFloat letteringVerticalHeight = self.letteringLabelFont.capHeight; + CGFloat textTotalHeight = (numberVerticalHeight+2.0f) + self.letteringVerticalSpacing + (letteringVerticalHeight+2.0f); + + CGRect frame = self.numberLabel.frame; + frame.size.height = ceil(numberVerticalHeight) + 2.0f; + frame.origin.x = ceilf((viewSize.width - frame.size.width) * 0.5f); + + if (!self.horizontalLayout && !self.verticallyCenterNumberLabel) { + frame.origin.y = floorf((viewSize.height - textTotalHeight) * 0.5f); + } + else { + frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); + } + self.numberLabel.frame = CGRectIntegral(frame); + + if (self.letteringLabel) { + CGFloat y = CGRectGetMaxY(frame); + y += self.letteringVerticalSpacing; + + frame = self.letteringLabel.frame; + frame.size.height = ceil(letteringVerticalHeight) + 2.0f; + + if (!self.horizontalLayout) { + frame.origin.y = floorf(y); + frame.origin.x = (viewSize.width - frame.size.width) * 0.5f; + } + else { + frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); + frame.origin.x = CGRectGetMaxX(self.numberLabel.frame) + self.letteringHorizontalSpacing; + } + + self.letteringLabel.frame = CGRectIntegral(frame); + } +} + +#pragma mark - Accessors - + +- (void)setTextColor:(UIColor *)textColor +{ + if (textColor == _textColor) { return; } + _textColor = textColor; + + self.numberLabel.textColor = _textColor; + self.letteringLabel.textColor = _textColor; +} +/***********************************************************/ + +- (void)setNumberString:(NSString *)numberString +{ + self.numberLabel.text = numberString; + [self setNeedsLayout]; +} + +- (NSString *)numberString { return self.numberLabel.text; } + +/***********************************************************/ + +- (void)setLetteringString:(NSString *)letteringString +{ + _letteringString = [letteringString copy]; + [self setUpViews]; + [self updateLetteringLabelText]; + [self setNeedsLayout]; +} + +/***********************************************************/ + +- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing +{ + _letteringCharacterSpacing = letteringCharacterSpacing; + [self updateLetteringLabelText]; +} + +/***********************************************************/ + +- (void)setNumberLabelFont:(UIFont *)numberLabelFont +{ + if (_numberLabelFont == numberLabelFont) { return; } + _numberLabelFont = numberLabelFont; + self.numberLabel.font = _numberLabelFont; +} + +/***********************************************************/ + +- (void)setLetteringLabelFont:(UIFont *)letteringLabelFont +{ + if (_letteringLabelFont == letteringLabelFont) { return; } + _letteringLabelFont = letteringLabelFont; + self.letteringLabel.font = letteringLabelFont; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h new file mode 100644 index 0000000000..5dc9ca2251 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h @@ -0,0 +1,46 @@ +// +// TOPasscodeCircleView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A view containing two circle image views that can animate + between filled and hollow, whilst maintaining compatibility + with translucency views. + */ +@interface TOPasscodeCircleView : UIView + +/* The circle patterns used for neutral and highlighted states. */ +@property (nonatomic, strong) UIImage *circleImage; +@property (nonatomic, strong) UIImage *highlightedCircleImage; + +/* Whether the highlighted view is visible. */ +@property (nonatomic, assign) BOOL isHighlighted; + +/* Animate the circle to be highlighted */ +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m new file mode 100644 index 0000000000..3bb5de974a --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m @@ -0,0 +1,89 @@ +// +// TOPasscodeCircleView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleView.h" + +@interface TOPasscodeCircleView () +@property (nonatomic, strong) UIImageView *bottomView; +@property (nonatomic, strong) UIImageView *topView; +@end + +@implementation TOPasscodeCircleView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.userInteractionEnabled = NO; + + self.bottomView = [[UIImageView alloc] initWithFrame:self.bounds]; + self.bottomView.userInteractionEnabled = NO; + self.bottomView.contentMode = UIViewContentModeCenter; + self.bottomView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:self.bottomView]; + + self.topView = [[UIImageView alloc] initWithFrame:self.bounds]; + self.topView.userInteractionEnabled = NO; + self.topView.contentMode = UIViewContentModeCenter; + self.topView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.topView.alpha = 0.0f; + [self addSubview:self.topView]; + } + + return self; +} + +- (void)setIsHighlighted:(BOOL)isHighlighted +{ + [self setHighlighted:isHighlighted animated:NO]; +} + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + if (highlighted == self.isHighlighted) { return; } + + _isHighlighted = highlighted; + + void (^animationBlock)(void) = ^{ + self.topView.alpha = highlighted ? 1.0f : 0.0f; + }; + + if (!animated) { + animationBlock(); + return; + } + + [UIView animateWithDuration:0.45f animations:animationBlock]; +} + +- (void)setCircleImage:(UIImage *)circleImage +{ + _circleImage = circleImage; + self.bottomView.image = circleImage; +} + +- (void)setHighlightedCircleImage:(UIImage *)highlightedCircleImage +{ + _highlightedCircleImage = highlightedCircleImage; + self.topView.image = highlightedCircleImage; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h new file mode 100644 index 0000000000..a448d8ca0b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h @@ -0,0 +1,54 @@ +// +// TOPasscodeFixedInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeCircleView; + +/** + A basic content view showing a row of circles that can be used to represent + a fixed size passcode. + */ +@interface TOPasscodeFixedInputView : UIView + +/* The size of each circle in this view (Default is 16) */ +@property (nonatomic, assign) CGFloat circleDiameter; + +/* The spacing between each circle (Default is 25.0f) */ +@property (nonatomic, assign) CGFloat circleSpacing; + +/* The number of circles in this view (Default is 4) */ +@property (nonatomic, assign) NSInteger length; + +/* The number of highlighted circles */ +@property (nonatomic, assign) NSInteger highlightedLength; + +/* The circle views managed by this view */ +@property (nonatomic, strong, readonly) NSArray *circleViews; + +/* Init with a set number of circles */ +- (instancetype)initWithLength:(NSInteger)length; + +/* Set the number of highlighted circles */ +- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m new file mode 100644 index 0000000000..e4dac42805 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m @@ -0,0 +1,171 @@ +// +// TOPasscodeFixedInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeFixedInputView.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeCircleImage.h" + +@interface TOPasscodeFixedInputView () + +@property (nonatomic, strong, readwrite) NSArray *circleViews; +@property (nonatomic, strong) UIImage *circleImage; +@property (nonatomic, strong) UIImage *highlightedCircleImage; + +@end + +@implementation TOPasscodeFixedInputView + +#pragma mark - Object Creation - + +- (instancetype)initWithLength:(NSInteger)length +{ + if (self = [self initWithFrame:CGRectZero]) { + _length = length; + } + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _circleSpacing = 25.0f; + _circleDiameter = 16.0f; + _length = 4; + } + + return self; +} + +#pragma mark - View Configuration - + +- (void)sizeToFit +{ + // Resize the view to encompass the circles + CGRect frame = self.frame; + frame.size.width = (_circleDiameter * _length) + (_circleSpacing * (_length - 1)) + 2.0f; + frame.size.height = _circleDiameter + 2.0f; + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + CGRect frame = CGRectZero; + frame.size = (CGSize){self.circleDiameter + 2.0f, self.circleDiameter + 2.0f}; + + for (TOPasscodeCircleView *circleView in self.circleViews) { + circleView.frame = frame; + frame.origin.x += self.circleDiameter + self.circleSpacing; + } +} + +#pragma mark - State Configuration - + +- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated +{ + NSInteger i = 0; + for (TOPasscodeCircleView *circleView in self.circleViews) { + [circleView setHighlighted:(i < highlightedLength) animated:animated]; + i++; + } +} + +#pragma mark - Circle View Configuration - + +- (void)setCircleViewsForLength:(NSInteger)length +{ + NSMutableArray *circleViews = [NSMutableArray array]; + if (self.circleViews) { + [circleViews addObjectsFromArray:self.circleViews]; + } + + [UIView performWithoutAnimation:^{ + while (circleViews.count != length) { + // Remove any extra circle views + if (circleViews.count > length) { + TOPasscodeCircleView *lastCircle = circleViews.lastObject; + [lastCircle removeFromSuperview]; + [circleViews removeLastObject]; + continue; + } + + // Add any new circle views + TOPasscodeCircleView *newCircleView = [[TOPasscodeCircleView alloc] init]; + [self setImagesOfCircleView:newCircleView]; + [self addSubview:newCircleView]; + [circleViews addObject:newCircleView]; + } + + self.circleViews = [NSArray arrayWithArray:circleViews]; + [self setNeedsLayout]; + [self layoutIfNeeded]; + }]; +} + +- (void)setCircleImagesForDiameter:(CGFloat)diameter +{ + self.circleImage = [TOPasscodeCircleImage hollowCircleImageOfSize:diameter strokeWidth:1.2f padding:1.0f]; + self.highlightedCircleImage = [TOPasscodeCircleImage circleImageOfSize:diameter inset:0.5f padding:1.0f antialias:YES]; + + for (TOPasscodeCircleView *circleView in self.circleViews) { + [self setImagesOfCircleView:circleView]; + } +} + +- (void)setImagesOfCircleView:(TOPasscodeCircleView *)circleView +{ + circleView.circleImage = self.circleImage; + circleView.highlightedCircleImage = self.highlightedCircleImage; +} + +#pragma mark - Accessors - + +- (NSArray *)circleViews +{ + if (_circleViews) { return _circleViews; } + _circleViews = [NSArray array]; + [self setCircleViewsForLength:self.length]; + [self setCircleImagesForDiameter:self.circleDiameter]; + return _circleViews; +} + +- (void)setCircleDiameter:(CGFloat)circleDiameter +{ + if (circleDiameter == _circleDiameter) { return; } + _circleDiameter = circleDiameter; + [self setCircleImagesForDiameter:_circleDiameter]; + [self sizeToFit]; +} + +- (void)setLength:(NSInteger)length +{ + if (_length == length) { return; } + _length = length; + [self setCircleViewsForLength:length]; +} + +- (void)setHighlightedLength:(NSInteger)highlightedLength +{ + [self setHighlightedLength:highlightedLength animated:NO]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h new file mode 100644 index 0000000000..05103b27d9 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h @@ -0,0 +1,111 @@ +// +// TOPasscodeInputField.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +#import "TOPasscodeFixedInputView.h" +#import "TOPasscodeVariableInputView.h" + + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, TOPasscodeInputFieldStyle) { + TOPasscodeInputFieldStyleFixed, // The passcode explicitly requires a specific number of characters (Shows hollow circles) + TOPasscodeInputFieldStyleVariable // The passcode can be any arbitrary number of characters (Shows an empty rectangle) +}; + +/** + An abstract input view capable of receiving different types of passcodes. + When a fixed character passcode is specified, the view shows a row of circles. + When a variable passcode is specified, a rounded rectangle is shown. + */ +@interface TOPasscodeInputField : UIView + +/* The visual effects view used to control the vibrancy of the input field */ +@property (nonatomic, strong, readonly) UIVisualEffectView *visualEffectView; + +/* The input style of this control */ +@property (nonatomic, assign) TOPasscodeInputFieldStyle style; + +/* A row of hollow circles at a preset length. Valid only when `style` is set to `fixed` */ +@property (nonatomic, readonly, nullable) TOPasscodeFixedInputView *fixedInputView; + +/* A rounded rectangle representing a passcode of arbitrary length. Valid only when `style` is set to `variable`. */ +@property (nonatomic, readonly, nullable) TOPasscodeVariableInputView *variableInputView; + +/* The 'submit' button shown when `showSubmitButton` is true. */ +@property (nonatomic, readonly, nullable) UIButton *submitButton; + +/* Shows an 'OK' button next to the view when characters have been added. */ +@property (nonatomic, assign) BOOL showSubmitButton; + +/* The amount of spacing between the 'OK' button and the passcode field */ +@property (nonatomic, assign) CGFloat submitButtonSpacing; + +/* The amount of spacing between the 'OK' button and the passcode field */ +@property (nonatomic, assign) CGFloat submitButtonVerticalSpacing; + +/* The font size of the submit button */ +@property (nonatomic, assign) CGFloat submitButtonFontSize; + +/* The current passcode entered into this view */ +@property (nonatomic, copy, nullable) NSString *passcode; + +/* If this view is directly receiving input, this can change the `UIKeyboard` appearance. */ +@property (nonatomic, assign) UIKeyboardAppearance keyboardAppearance; + +/* The type of button used for the 'Done' button in the keyboard */ +@property(nonatomic, assign) UIReturnKeyType returnKeyType; + +/* The alpha value of the views in this view (For tranclucent styling) */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/* Whether the view may be tapped to enable character input (Default is NO) */ +@property (nonatomic, assign) BOOL enabled; + +/** Called when the number of digits has been entered, or the user tapped 'Done' on the keyboard */ +@property (nonatomic, copy) void (^passcodeCompletedHandler)(NSString *code); + +/** Horizontal layout. The 'OK' button will be placed under the text field */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/* Init with the target length needed for this passcode */ +- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style; + +/* Replace the passcode with this one, and animate the transition. */ +- (void)setPasscode:(nullable NSString *)passcode animated:(BOOL)animated; + +/* Add additional characters to the end of the passcode, and animate if desired. */ +- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated; + +/* Delete a number of characters from the end, animated if desired. */ +- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated; + +/* Plays a shaking animation and resets the passcode back to empty */ +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; + +/* Animates the OK button changing location. */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m new file mode 100644 index 0000000000..0b3227c0ea --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m @@ -0,0 +1,423 @@ +// +// TOPasscodeInputField.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeInputField.h" + +#import "TOPasscodeVariableInputView.h" +#import "TOPasscodeFixedInputView.h" + +#import + +@interface TOPasscodeInputField () + +// Convenience getters +@property (nonatomic, readonly) UIView *inputField; // Returns whichever input field is currently visible +@property (nonatomic, readonly) NSInteger maximumPasscodeLength; // The mamximum number of characters allowed (0 if uncapped) + +@property (nonatomic, strong, readwrite) TOPasscodeFixedInputView *fixedInputView; +@property (nonatomic, strong, readwrite) TOPasscodeVariableInputView *variableInputView; +@property (nonatomic, strong, readwrite) UIButton *submitButton; +@property (nonatomic, strong, readwrite) UIVisualEffectView *visualEffectView; + +@end + +@implementation TOPasscodeInputField + +#pragma mark - View Set-up - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + [self setUpForStyle:TOPasscodeInputFieldStyleFixed]; + } + + return self; +} + +- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style +{ + if (self = [self initWithFrame:CGRectZero]) { + _style = style; + [self setUp]; + [self setUpForStyle:style]; + } + + return self; +} + +- (void)setUp +{ + self.backgroundColor = [UIColor clearColor]; + _submitButtonSpacing = 4.0f; + _submitButtonVerticalSpacing = 5.0f; + + _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:nil]; + [self addSubview:_visualEffectView]; +} + +- (void)setUpForStyle:(TOPasscodeInputFieldStyle)style +{ + if (self.inputField) { + [self.inputField removeFromSuperview]; + self.variableInputView = nil; + self.fixedInputView = nil; + } + + if (style == TOPasscodeInputFieldStyleVariable) { + self.variableInputView = [[TOPasscodeVariableInputView alloc] init]; + [self.visualEffectView.contentView addSubview:self.variableInputView]; + } + else { + self.fixedInputView = [[TOPasscodeFixedInputView alloc] init]; + [self.visualEffectView.contentView addSubview:self.fixedInputView]; + } + + // Set the frame for the currently visible input view + [self.inputField sizeToFit]; + + // Size this view to match + [self sizeToFit]; +} + +#pragma mark - View Layout - +- (void)sizeToFit +{ + // Resize the view to encompass the current input view + CGRect frame = self.frame; + [self.inputField sizeToFit]; + frame.size = self.inputField.frame.size; + if (self.horizontalLayout) { + frame.size.height += self.submitButtonVerticalSpacing + CGRectGetHeight(self.submitButton.frame); + } + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.visualEffectView.frame = self.inputField.bounds; + + if (!self.submitButton) { return; } + + [self.submitButton sizeToFit]; + [self bringSubviewToFront:self.submitButton]; + + CGRect frame = self.submitButton.frame; + if (!self.horizontalLayout) { + frame.origin.x = CGRectGetMaxX(self.bounds) + self.submitButtonSpacing; + frame.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frame)) * 0.5f; + } + else { + frame.origin.x = (CGRectGetWidth(self.frame) - frame.size.width) * 0.5f; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + self.submitButtonVerticalSpacing; + } + self.submitButton.frame = CGRectIntegral(frame); +} + +#pragma mark - Interaction - +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + if (!self.enabled) { return; } + self.contentAlpha = 0.5f; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + if (!self.enabled) { return; } + [UIView animateWithDuration:0.3f animations:^{ + self.contentAlpha = 1.0f; + }]; + [self becomeFirstResponder]; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + CGRect frame = self.bounds; + frame.size.width += self.submitButton.frame.size.width + (self.submitButtonSpacing * 2.0f); + frame.size.height += self.submitButtonVerticalSpacing; + + if (CGRectContainsPoint(frame, point)) { + return YES; + } + return NO; +} + +- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + if ([[super hitTest:point withEvent:event] isEqual:self.submitButton]) { + if (CGRectContainsPoint(self.submitButton.frame, point)) { + return self.submitButton; + } else { + return self; + } + } + + return [super hitTest:point withEvent:event]; +} + +#pragma mark - Text Input Protocol - +- (BOOL)canBecomeFirstResponder { return self.enabled; } + +- (BOOL)hasText { return self.passcode.length > 0; } + +- (void)insertText:(NSString *)text +{ + if ([text isEqualToString:@"\n"]) { + if (self.passcodeCompletedHandler) { self.passcodeCompletedHandler(self.passcode); } + return; + } + + [self appendPasscodeCharacters:text animated:NO]; +} +- (void)deleteBackward +{ + [self deletePasscodeCharactersOfCount:1 animated:YES]; +} + +- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; } + +- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } + +- (UIReturnKeyType)returnKeyType { return UIReturnKeyGo; } + +- (BOOL)enablesReturnKeyAutomatically { return YES; } + +#pragma mark - Text Input - +- (void)setPasscode:(NSString *)passcode animated:(BOOL)animated +{ + if (passcode == self.passcode) { return; } + _passcode = passcode; + + BOOL passcodeIsComplete = NO; + if (self.fixedInputView) { + [self.fixedInputView setHighlightedLength:_passcode.length animated:animated]; + passcodeIsComplete = _passcode.length >= self.maximumPasscodeLength; + } + else { + [self.variableInputView setLength:_passcode.length animated:animated]; + } + + if (self.submitButton) { + self.submitButton.hidden = (_passcode.length == 0); + [self bringSubviewToFront:self.submitButton]; + } + + if (passcodeIsComplete && self.passcodeCompletedHandler) { + self.passcodeCompletedHandler(_passcode); + } + + [self reloadInputViews]; +} + +- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated +{ + if (characters == nil) { return; } + if (self.maximumPasscodeLength > 0 && self.passcode.length >= self.maximumPasscodeLength) { return; } + + if (_passcode == nil) { _passcode = @""; } + [self setPasscode:[_passcode stringByAppendingString:characters] animated:animated]; +} + +- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated +{ + if (deleteCount <= 0 || self.passcode.length <= 0) { return; } + [self setPasscode:[self.passcode substringToIndex:(self.passcode.length - 1)] animated:animated]; +} + +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact +{ + [self setPasscode:nil animated:animated]; + + // Play a negative impact effect + if (@available(iOS 9.0, *)) { + // https://stackoverflow.com/questions/41444274/how-to-check-if-haptic-engine-uifeedbackgenerator-is-supported + if (impact) { AudioServicesPlaySystemSoundWithCompletion(1521, nil); } + } + + if (!animated) { return; } + + CGPoint center = self.center; + CGPoint offset = center; + offset.x -= self.frame.size.width * 0.3f; + + // Play the view sliding out and then springing back in + id completionBlock = ^(BOOL finished) { + [UIView animateWithDuration:1.0f + delay:0.0f + usingSpringWithDamping:0.15f + initialSpringVelocity:10.0f + options:0 animations:^{ + self.center = center; + }completion:nil]; + }; + + [UIView animateWithDuration:0.05f animations:^{ + self.center = offset; + }completion:completionBlock]; + + if (!self.submitButton) { return; } + + [UIView animateWithDuration:0.7f animations:^{ + self.submitButton.alpha = 0.0f; + } completion:^(BOOL complete) { + self.submitButton.alpha = 1.0f; + self.submitButton.hidden = YES; + }]; +} + +#pragma mark - Button Callbacks - +- (void)submitButtonTapped:(id)sender +{ + if (self.passcodeCompletedHandler) { + self.passcodeCompletedHandler(self.passcode); + } +} + +#pragma mark - Private Accessors - +- (UIView *)inputField +{ + if (self.fixedInputView) { + return (UIView *)self.fixedInputView; + } + + return (UIView *)self.variableInputView; +} + +- (NSInteger)maximumPasscodeLength +{ + if (self.style == TOPasscodeInputFieldStyleFixed) { + return self.fixedInputView.length; + } + + return 0; +} + +#pragma mark - Public Accessors - + +- (void)setShowSubmitButton:(BOOL)showSubmitButton +{ + if (_showSubmitButton == showSubmitButton) { + return; + } + + _showSubmitButton = showSubmitButton; + + if (!_showSubmitButton) { + [self.submitButton removeFromSuperview]; + self.submitButton = nil; + return; + } + + self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.submitButton setTitle:@"OK" forState:UIControlStateNormal]; + [self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.submitButton.titleLabel setFont:[UIFont systemFontOfSize:18.0f]]; + self.submitButton.hidden = YES; + [self addSubview:self.submitButton]; + + [self setNeedsLayout]; +} + +- (void)setSubmitButtonSpacing:(CGFloat)submitButtonSpacing +{ + if (submitButtonSpacing == _submitButtonSpacing) { return; } + _submitButtonSpacing = submitButtonSpacing; + [self setNeedsLayout]; +} + +- (void)setSubmitButtonFontSize:(CGFloat)submitButtonFontSize +{ + if (submitButtonFontSize == _submitButtonFontSize) { return; } + _submitButtonFontSize = submitButtonFontSize; + self.submitButton.titleLabel.font = [UIFont systemFontOfSize:_submitButtonFontSize]; + [self.submitButton sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setStyle:(TOPasscodeInputFieldStyle)style +{ + if (style == _style) { return; } + _style = style; + [self setUpForStyle:_style]; +} + +- (void)setPasscode:(NSString *)passcode +{ + [self setPasscode:passcode animated:NO]; +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + self.inputField.alpha = contentAlpha; + self.submitButton.alpha = contentAlpha; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (_horizontalLayout == horizontalLayout) { + return; + } + + UIView *snapshotView = nil; + + if (self.submitButton && self.submitButton.hidden == NO && animated) { + snapshotView = [self.submitButton snapshotViewAfterScreenUpdates:NO]; + snapshotView.frame = self.submitButton.frame; + [self addSubview:snapshotView]; + } + + _horizontalLayout = horizontalLayout; + + if (!animated || !self.submitButton) { + [self sizeToFit]; + [self setNeedsLayout]; + return; + } + + self.submitButton.alpha = 0.0f; + [self setNeedsLayout]; + [self layoutIfNeeded]; + + id animationBlock = ^{ + self.submitButton.alpha = 1.0f; + snapshotView.alpha = 0.0f; + }; + + id completionBlock = ^(BOOL complete) { + [snapshotView removeFromSuperview]; + [self bringSubviewToFront:self.submitButton]; + }; + + [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h new file mode 100644 index 0000000000..737f76761f --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h @@ -0,0 +1,55 @@ +// +// TOPasscodeVariableInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +/** + A basic content view showing a rounded rectangle containing circles + that can be used to represent a variable size passcode. + */ +@interface TOPasscodeVariableInputView : UIImageView + +/* The thickness of the stroke around the view (Default is 1.5) */ +@property (nonatomic, assign) CGFloat outlineThickness; + +/* The corner radius of the stroke (Default is 5) */ +@property (nonatomic, assign) CGFloat outlineCornerRadius; + +/* The size of each circle bullet point representing a passcoded character (Default is 10) */ +@property (nonatomic, assign) CGFloat circleDiameter; + +/* The spacing between each circle (Default is 15) */ +@property (nonatomic, assign) CGFloat circleSpacing; + +/* The padding between the circles and the outer outline (Default is {10,10}) */ +@property (nonatomic, assign) CGSize outlinePadding; + +/* The maximum number of circles to show (This will indicate the view's width) (Default is 12) */ +@property (nonatomic, assign) NSInteger maximumVisibleLength; + +/* Set the number of characters entered into this view (May be larger than `maximumVisibleLength`) */ +@property (nonatomic, assign) NSInteger length; + +/* Set the number of characters represented by this field, animated if desired */ +- (void)setLength:(NSInteger)length animated:(BOOL)animated; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m new file mode 100644 index 0000000000..1dc0783b91 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m @@ -0,0 +1,254 @@ +// +// TOPasscodeVariableInputView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeVariableInputView.h" +#import "TOPasscodeCircleImage.h" + +@interface TOPasscodeVariableInputView () + +@property (nonatomic, strong) UIImage *backgroundImage; // The outline image for this view +@property (nonatomic, strong) UIImage *circleImage; // The circle image representing a single character + +@property (nonatomic, strong) NSMutableArray *circleViews; + +@end + +@implementation TOPasscodeVariableInputView + +#pragma mark - Class Creation - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _outlineThickness = 1.0f; + _outlineCornerRadius = 5.0f; + _circleDiameter = 11.0f; + _circleSpacing = 7.0f; + _outlinePadding = (CGSize){10,10}; + _maximumVisibleLength = 12; + } + + return self; +} + +#pragma mark - View Setup - +- (void)setUpImageForCircleViews +{ + if (self.circleImage != nil) { return; } + + self.circleImage = [TOPasscodeCircleImage circleImageOfSize:_circleDiameter inset:0.0f padding:1.0f antialias:YES]; + for (UIImageView *circleView in self.circleViews) { + circleView.image = self.circleImage; + [circleView sizeToFit]; + } +} + +- (void)setUpCircleViewsForLength:(NSInteger)length +{ + // Set up the number of circle views if needed + if (self.circleViews.count == length) { return; } + + if (self.circleViews == nil) { + self.circleViews = [NSMutableArray arrayWithCapacity:_maximumVisibleLength]; + } + + // Reduce the number of views + while (self.circleViews.count > length) { + UIImageView *circleView = self.circleViews.lastObject; + [circleView removeFromSuperview]; + [self.circleViews removeLastObject]; + } + + // Increase the number of views + [UIView performWithoutAnimation:^{ + while (self.circleViews.count < length) { + UIImageView *circleView = [[UIImageView alloc] initWithImage:self.circleImage]; + circleView.alpha = 0.0f; + [self addSubview:circleView]; + [self.circleViews addObject:circleView]; + } + }]; +} + +- (void)setUpBackgroundImage +{ + if (self.backgroundImage != nil) { return; } + + self.backgroundImage = [[self class] backgroundImageWithThickness:_outlineThickness cornerRadius:_outlineCornerRadius]; + self.image = self.backgroundImage; +} + +#pragma mark - View Layout - + +- (void)sizeToFit +{ + CGRect frame = self.frame; + + // Calculate the width + frame.size.width = self.outlineThickness * 2.0f; + frame.size.width += (self.outlinePadding.width * 2.0f); + frame.size.width += (self.maximumVisibleLength * (self.circleDiameter+2.0f)); // +2 for padding + frame.size.width += ((self.maximumVisibleLength - 1) * self.circleSpacing); + + // Height + frame.size.height = self.outlineThickness * 2.0f; + frame.size.height += self.outlinePadding.height * 2.0f; + frame.size.height += self.circleDiameter; + + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // Genearate the background image if we don't have one yet + [self setUpBackgroundImage]; + + // Set up the circle view image + [self setUpImageForCircleViews]; + + // Set up the circle views + [self setUpCircleViewsForLength:self.maximumVisibleLength]; + + // Layout the circle views for the current length + CGRect frame = CGRectZero; + frame.size = self.circleImage.size; + frame.origin.y = CGRectGetMidY(self.bounds) - (frame.size.height * 0.5f); + frame.origin.x = self.outlinePadding.width + self.outlineThickness; + + for (UIImageView *circleView in self.circleViews) { + circleView.frame = frame; + frame.origin.x += frame.size.width + self.circleSpacing; + } +} + +#pragma mark - Accessors - + +- (void)setOutlineThickness:(CGFloat)outlineThickness +{ + if (_outlineThickness == outlineThickness) { return; } + _outlineThickness = outlineThickness; + self.backgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setOutlineCornerRadius:(CGFloat)outlineCornerRadius +{ + if (_outlineCornerRadius == outlineCornerRadius) { return; } + _outlineCornerRadius = outlineCornerRadius; + self.backgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setCircleDiameter:(CGFloat)circleDiameter +{ + if (_circleDiameter == circleDiameter) { return; } + _circleDiameter = circleDiameter; + self.circleImage = nil; + [self setUpImageForCircleViews]; +} + +- (void)setCircleSpacing:(CGFloat)circleSpacing +{ + if (_circleSpacing == circleSpacing) { return; } + _circleSpacing = circleSpacing; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setOutlinePadding:(CGSize)outlinePadding +{ + if (CGSizeEqualToSize(outlinePadding, _outlinePadding)) { return; } + _outlinePadding = outlinePadding; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setMaximumVisibleLength:(NSInteger)maximumVisibleLength +{ + if (_maximumVisibleLength == maximumVisibleLength) { return; } + _maximumVisibleLength = maximumVisibleLength; + [self setUpCircleViewsForLength:maximumVisibleLength]; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setLength:(NSInteger)length +{ + [self setLength:length animated:NO]; +} + +- (void)setLength:(NSInteger)length animated:(BOOL)animated +{ + if (length == _length) { return; } + + _length = length; + + void (^animationBlock)(void) = ^{ + NSInteger i = 0; + for (UIImageView *circleView in self.circleViews) { + circleView.alpha = i < length ? 1.0f : 0.0f; + i++; + } + }; + + if (!animated) { + animationBlock(); + return; + } + + [UIView animateWithDuration:0.4f animations:animationBlock]; +} + +#pragma mark - Image Creation - + ++ (UIImage *)backgroundImageWithThickness:(CGFloat)thickness cornerRadius:(CGFloat)radius +{ + CGFloat inset = thickness / 2.0f; + CGFloat dimension = (radius * 2.0f) + 2.0f; + + CGRect frame = CGRectZero; + frame.origin = CGPointMake(inset, inset); + frame.size = CGSizeMake(dimension, dimension); + + CGSize canvasSize = frame.size; + canvasSize.width += thickness; + canvasSize.height += thickness; + + UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); + { + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius]; + path.lineWidth = thickness; + [[UIColor blackColor] setStroke]; + [path stroke]; + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + UIEdgeInsets insets = UIEdgeInsetsMake(radius+1, radius+1, radius+1, radius+1); + image = [image resizableImageWithCapInsets:insets]; + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +@end From 4eb2c821643b87203f9b899f8ea3b9b75ae39b18 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 14:54:59 +0200 Subject: [PATCH 23/31] carhage removed Signed-off-by: Marino Faggiana --- Cartfile | 1 - Cartfile.resolved | 1 - Nextcloud.xcodeproj/project.pbxproj | 21 +++++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) delete mode 100644 Cartfile delete mode 100644 Cartfile.resolved diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 315cc9aec0..0000000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.5.1 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index 1b44f3b861..0000000000 --- a/Cartfile.resolved +++ /dev/null @@ -1 +0,0 @@ -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" "3.6.0" diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index ed0bdd1b44..c9fcd1cef6 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -691,8 +691,6 @@ F78ACD4B21903F850088454D /* NCTrashListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD4921903F850088454D /* NCTrashListCell.xib */; }; F78ACD52219046DC0088454D /* NCSectionFirstHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD51219046DC0088454D /* NCSectionFirstHeader.swift */; }; F78ACD54219047D40088454D /* NCSectionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD53219047D40088454D /* NCSectionFooter.xib */; }; - F78AF1E72BE938C100F3F060 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */; }; - F78AF1E82BE938C100F3F060 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F78B87E72B62527100C65ADC /* NCMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */; }; F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnail.swift */; }; F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78C6FDD296D677300C952C3 /* NCContextMenu.swift */; }; @@ -883,6 +881,7 @@ F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */ = {isa = PBXBuildFile; productRef = F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D57C8626317BDA00DE301D /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7CA212C25F1333200826ABB /* NCAccountRequest.storyboard */; }; F7D57C8B26317BDE00DE301D /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CA212B25F1333200826ABB /* NCAccountRequest.swift */; }; @@ -1110,7 +1109,6 @@ dstSubfolderSpec = 10; files = ( F7A509262C26D95D00326106 /* RealmSwift in Embed Frameworks */, - F78AF1E82BE938C100F3F060 /* MobileVLCKit.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1943,6 +1941,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */, F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */, F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */, F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */, @@ -1967,7 +1966,6 @@ F7F623B52A5EF4D30022D3D4 /* Gzip in Frameworks */, F75EAED826D2552E00F4320E /* MarqueeLabel in Frameworks */, F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */, - F78AF1E72BE938C100F3F060 /* MobileVLCKit.xcframework in Frameworks */, F73ADD1C265546890069EA0D /* SwiftEntryKit in Frameworks */, F76B649E2ADFFDEC00014640 /* LRUCache in Frameworks */, ); @@ -3608,6 +3606,7 @@ F7160A812BE933390034DCB3 /* RealmSwift */, F33EE6E02BF4BDA500CA1A51 /* NIOSSL */, F33EE6EF2BF4C0FF00CA1A51 /* NIO */, + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */, ); productName = "Crypto Cloud"; productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */; @@ -3782,6 +3781,7 @@ F3A0479C2BD268B500658E7B /* XCRemoteSwiftPackageReference "PopupView" */, F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */, F33EE6EE2BF4C0FF00CA1A51 /* XCRemoteSwiftPackageReference "swift-nio" */, + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */, ); productRefGroup = F7F67B9F1A24D27800EE80DA; projectDirPath = ""; @@ -6054,6 +6054,14 @@ version = 3.3.0; }; }; + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tylerjonesio/vlckit-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.5.1; + }; + }; F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marinofaggiana/Mantis"; @@ -6536,6 +6544,11 @@ package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; productName = Parchment; }; + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { + isa = XCSwiftPackageProductDependency; + package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; + productName = VLCKitSPM; + }; F7D56B192972405500FA46C4 /* Mantis */ = { isa = XCSwiftPackageProductDependency; package = F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */; From 9f37f50d8c96f28b344354973bcf4a6318f81003 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 14:58:02 +0200 Subject: [PATCH 24/31] README Signed-off-by: Marino Faggiana --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 83b21d387a..02ebbe64a5 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,10 @@ branch. Maybe start working on [starter issues](https://github.com/nextcloud/ios Easy starting points are also reviewing [pull requests](https://github.com/nextcloud/ios/pulls) -### Xcode 15 Project Setup +### Xcode 16 Project Setup #### Dependencies -After forking a repository you have to build the dependencies. Dependencies are managed with Carthage version 0.38.0 or later. -Run - -``` -carthage update --use-xcframeworks --platform iOS -``` -to fetch and compile the dependencies. - In order to build the project in Xcode you will also need a file `GoogleService-Info.plist` at the root of the repository which contains the Firebase configuration. For development work you can use a mock version found [here](https://github.com/firebase/quickstart-ios/blob/master/mock-GoogleService-Info.plist). ### Creating Pull requests From f2e776a9401148d2f0534fd07fd3acff35d8eb8f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 15:59:26 +0200 Subject: [PATCH 25/31] Build 18 Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index c9fcd1cef6..9bc9b7a892 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5662,7 +5662,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5728,7 +5728,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; From 396dc761de7dd3ec26eca854025e851fc826993d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 16:39:10 +0200 Subject: [PATCH 26/31] use develop NextcloudKit Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 9bc9b7a892..178f28272f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -6026,7 +6026,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = sessions; + branch = develop; kind = branch; }; }; From b9866d1c782536a61897a00062bcbf5c54851a82 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 17:48:53 +0200 Subject: [PATCH 27/31] iPad Signed-off-by: Marino Faggiana --- .../Main/Collection Common/NCCollectionViewCommon.swift | 7 ++++++- iOSClient/Media/NCMedia.swift | 7 ++++++- iOSClient/Media/NCMediaLayout.swift | 4 ---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index f71d75880c..d8279ca640 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -85,7 +85,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 - let maxColumns: Int = 10 + var maxColumns: Int { + let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) + let column = Int(screenWidth / 44) + + return column + } var transitionColumns = false var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 0c238b94e5..fbe14e7486 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -70,7 +70,12 @@ class NCMedia: UIViewController { var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 - let maxColumns: Int = 10 + var maxColumns: Int { + let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) + let column = Int(screenWidth / 44) + + return column + } var transitionColumns = false var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 diff --git a/iOSClient/Media/NCMediaLayout.swift b/iOSClient/Media/NCMediaLayout.swift index fccb724155..4e71d4cc0f 100644 --- a/iOSClient/Media/NCMediaLayout.swift +++ b/iOSClient/Media/NCMediaLayout.swift @@ -117,10 +117,6 @@ public class NCMediaLayout: UICollectionViewLayout { columnCount = delegate.getColumnCount() (delegate as? NCMedia)?.buildMediaPhotoVideo(columnCount: columnCount) - if UIDevice.current.userInterfaceIdiom == .phone, - (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) { - columnCount += 2 - } // Initialize variables headersAttribute.removeAll(keepingCapacity: false) From 51c7ded3eb11867f2b21b08fef1e446ebdc20a79 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 18:09:28 +0200 Subject: [PATCH 28/31] code improved Signed-off-by: Marino Faggiana --- iOSClient/Media/NCMediaDataSource.swift | 27 ++++++++++++------- .../Media/NCMediaDownloadThumbnail.swift | 4 ++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/iOSClient/Media/NCMediaDataSource.swift b/iOSClient/Media/NCMediaDataSource.swift index 270f06481a..25aca0d9ee 100644 --- a/iOSClient/Media/NCMediaDataSource.swift +++ b/iOSClient/Media/NCMediaDataSource.swift @@ -27,8 +27,9 @@ import RealmSwift extension NCMedia { func loadDataSource() { + let session = self.session DispatchQueue.global().async { - if let metadatas = self.database.getResultsMetadatas(predicate: self.imageCache.getMediaPredicate(filterLivePhotoFile: true, session: self.session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos), sortedByKeyPath: "date") { + if let metadatas = self.database.getResultsMetadatas(predicate: self.imageCache.getMediaPredicate(filterLivePhotoFile: true, session: session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos), sortedByKeyPath: "date") { self.dataSource = NCMediaDataSource(metadatas: metadatas) } self.collectionViewReloadData() @@ -46,6 +47,8 @@ extension NCMedia { // MARK: - Search media @objc func searchMediaUI(_ distant: Bool = false) { + let session = self.session + self.lockQueue.sync { guard self.isViewActived, !self.hasRunSearchMedia, @@ -108,7 +111,7 @@ extension NCMedia { account: self.session.account, options: options) { account, files, _, error in - if error == .success, let files, self.session.account == account, !self.showOnlyImages, !self.showOnlyVideos { + if error == .success, let files, session.account == account, !self.showOnlyImages, !self.showOnlyVideos { /// Removes all files in `files` that have an `ocId` present in `fileDeleted` var files = files @@ -123,7 +126,7 @@ extension NCMedia { self.collectionViewReloadData() } - DispatchQueue.global(qos: .background).async { + DispatchQueue.global(qos: .userInteractive).async { self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in self.database.addMetadatas(metadatas) @@ -131,13 +134,17 @@ extension NCMedia { self.collectionViewReloadData() } - if let firstCellDate, let lastCellDate, self.isViewActived { - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.imageCache.getMediaPredicate(filterLivePhotoFile: false, session: self.session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos)]) - - if let resultsMetadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: predicate) { - for metadata in resultsMetadatas where !self.filesExists.contains(metadata.ocId) { - if NCNetworking.shared.fileExistsQueue.operations.filter({ ($0 as? NCOperationFileExists)?.ocId == metadata.ocId }).isEmpty { - NCNetworking.shared.fileExistsQueue.addOperation(NCOperationFileExists(metadata: metadata)) + DispatchQueue.main.async { + if let firstCellDate, let lastCellDate, self.isViewActived { + DispatchQueue.global(qos: .background).async { + let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.imageCache.getMediaPredicate(filterLivePhotoFile: false, session: session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos)]) + + if let resultsMetadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: predicate) { + for metadata in resultsMetadatas where !self.filesExists.contains(metadata.ocId) { + if NCNetworking.shared.fileExistsQueue.operations.filter({ ($0 as? NCOperationFileExists)?.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.fileExistsQueue.addOperation(NCOperationFileExists(metadata: metadata)) + } + } } } } diff --git a/iOSClient/Media/NCMediaDownloadThumbnail.swift b/iOSClient/Media/NCMediaDownloadThumbnail.swift index 547242db23..dec47c5ab9 100644 --- a/iOSClient/Media/NCMediaDownloadThumbnail.swift +++ b/iOSClient/Media/NCMediaDownloadThumbnail.swift @@ -29,10 +29,12 @@ class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { var metadata: NCMediaDataSource.Metadata let utilityFileSystem = NCUtilityFileSystem() let media: NCMedia + var session: NCSession.Session init(metadata: NCMediaDataSource.Metadata, media: NCMedia) { self.metadata = metadata self.media = media + self.session = media.session } override func start() { @@ -46,7 +48,7 @@ class NCMediaDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { NextcloudKit.shared.downloadPreview(fileId: tableMetadata.fileId, etag: etagResource, - account: media.session.account, + account: self.session.account, options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, data, _, _, etag, error in if error == .success, let data { From 05f02cde45b7f71af593bef36bc24d57f52b4af5 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 18:21:52 +0200 Subject: [PATCH 29/31] Pinch Gesture improved Signed-off-by: Marino Faggiana --- .../NCCollectionViewCommonPinchGesture.swift | 6 ++++-- iOSClient/Media/NCMediaPinchGesture.swift | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift index d934c836a4..275e3768c8 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift @@ -80,8 +80,10 @@ extension NCCollectionViewCommon { lastScale = scale case .ended: - currentScale = 1.0 - collectionView.transform = .identity + UIView.animate(withDuration: 0.30) { + self.currentScale = 1.0 + self.collectionView.transform = .identity + } default: break } diff --git a/iOSClient/Media/NCMediaPinchGesture.swift b/iOSClient/Media/NCMediaPinchGesture.swift index 306d57d76e..cbf71a9d57 100644 --- a/iOSClient/Media/NCMediaPinchGesture.swift +++ b/iOSClient/Media/NCMediaPinchGesture.swift @@ -80,8 +80,10 @@ extension NCMedia { lastScale = scale case .ended: - currentScale = 1.0 - collectionView.transform = .identity + UIView.animate(withDuration: 0.30) { + self.currentScale = 1.0 + self.collectionView.transform = .identity + } default: break } From c4d889d8a9ac7da825a679a162c2d1b9d16b050e Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 18:53:54 +0200 Subject: [PATCH 30/31] normalized Signed-off-by: Marino Faggiana --- .../Collection Common/Cell/NCGridCell.swift | 17 +++--- .../Collection Common/Cell/NCListCell.swift | 53 +++++++++---------- .../Collection Common/Cell/NCListCell.xib | 30 +++++------ .../Collection Common/Cell/NCPhotoCell.swift | 27 +++------- ...nViewCommon+CollectionViewDataSource.swift | 20 +------ 5 files changed, 55 insertions(+), 92 deletions(-) diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index f7df1fc986..89d20139fc 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -99,25 +99,26 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto accessibilityValue = nil isAccessibilityElement = true + imageItem.image = nil imageItem.layer.cornerRadius = 6 imageItem.layer.masksToBounds = true - + imageSelect.isHidden = true + imageSelect.image = NCImageCache.shared.getImageCheckedYes() + imageStatus.image = nil + imageFavorite.image = nil + imageLocal.image = nil + labelTitle.text = "" + labelInfo.text = "" + labelSubinfo.text = "" imageVisualEffect.layer.cornerRadius = 6 imageVisualEffect.clipsToBounds = true imageVisualEffect.alpha = 0.5 - imageSelect.isHidden = true - imageSelect.image = NCImageCache.shared.getImageCheckedYes() - let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) longPressedGesture.minimumPressDuration = 0.5 longPressedGesture.delegate = self longPressedGesture.delaysTouchesBegan = true self.addGestureRecognizer(longPressedGesture) - - labelTitle.text = "" - labelInfo.text = "" - labelSubinfo.text = "" } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index a94ec59a9a..3ef20857bd 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -110,47 +110,42 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto override func awakeFromNib() { super.awakeFromNib() + initCell() + } - imageItem.layer.cornerRadius = 6 - imageItem.layer.masksToBounds = true + override func prepareForReuse() { + super.prepareForReuse() + initCell() + } - // use entire cell as accessibility element + func initCell() { accessibilityHint = nil accessibilityLabel = nil accessibilityValue = nil isAccessibilityElement = true + imageItem.image = nil + imageItem.layer.cornerRadius = 6 + imageItem.layer.masksToBounds = true + imageStatus.image = nil + imageFavorite.image = nil + imageFavoriteBackground.isHidden = true + imageLocal.image = nil + labelTitle.text = "" + labelInfo.text = "" + labelSubinfo.text = "" + imageShared.image = nil + imageMore.image = nil + separatorHeightConstraint.constant = 0.5 + tag0.text = "" + tag1.text = "" + titleInfoTrailingDefault() + let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) longPressedGesture.minimumPressDuration = 0.5 longPressedGesture.delegate = self longPressedGesture.delaysTouchesBegan = true self.addGestureRecognizer(longPressedGesture) - - separator.backgroundColor = .separator - separatorHeightConstraint.constant = 0.5 - - labelTitle.text = "" - labelInfo.text = "" - labelSubinfo.text = "" - labelTitle.textColor = NCBrandColor.shared.textColor - labelInfo.textColor = NCBrandColor.shared.textColor2 - labelSubinfo.textColor = NCBrandColor.shared.textColor2 - - imageFavoriteBackground.isHidden = true - } - - override func prepareForReuse() { - super.prepareForReuse() - imageItem.backgroundColor = nil - if fileFavoriteImage?.image != nil { - imageFavoriteBackground.isHidden = false - } else { - imageFavoriteBackground.isHidden = true - } - - accessibilityHint = nil - accessibilityLabel = nil - accessibilityValue = nil } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.xib b/iOSClient/Main/Collection Common/Cell/NCListCell.xib index e7ee83328c..3f9945744a 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.xib @@ -1,9 +1,9 @@ - + - + @@ -63,19 +63,18 @@