From 050a960167e830192e3457b648b7904c6edc67a7 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Thu, 2 Mar 2017 14:51:40 +0800 Subject: [PATCH] Support folder in password view - change core data - change data struct to store table view entry - delete unnecessary functions --- pass/Base.lproj/Main.storyboard | 6 +- .../AboutRepositoryTableViewController.swift | 2 +- .../PasswordDetailTableViewController.swift | 17 ++- .../Controllers/PasswordsViewController.swift | 137 ++++++++++++------ pass/Models/PasswordEntity.swift | 2 +- pass/Models/PasswordStore.swift | 85 +++++++---- .../pass.xcdatamodel/contents | 17 +-- 7 files changed, 177 insertions(+), 89 deletions(-) diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index 9040cfa..313c9a7 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -43,9 +43,6 @@ - - - @@ -63,6 +60,7 @@ + diff --git a/pass/Controllers/AboutRepositoryTableViewController.swift b/pass/Controllers/AboutRepositoryTableViewController.swift index 42ca754..6a4d7d0 100644 --- a/pass/Controllers/AboutRepositoryTableViewController.swift +++ b/pass/Controllers/AboutRepositoryTableViewController.swift @@ -31,7 +31,7 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController { numberFormatter.numberStyle = NumberFormatter.Style.decimal let fm = FileManager.default - let passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData() + let passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false) let numberOfPasswords = numberFormatter.string(from: NSNumber(value: passwordEntities.count))! var size = UInt64(0) diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 6f01fa4..a3f543a 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -13,7 +13,6 @@ import SVProgressHUD class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate { var passwordEntity: PasswordEntity? - var passwordCategoryEntities: [PasswordCategoryEntity]? var passwordCategoryText = "" var password: Password? var passwordImage: UIImage? @@ -62,13 +61,23 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni var tableData = Array() + private func generateCategoryText() -> String { + var passwordCategoryArray: [String] = [] + var parent = passwordEntity?.parent + while parent != nil { + passwordCategoryArray.append(parent!.name!) + parent = parent!.parent + } + passwordCategoryArray.reverse() + return passwordCategoryArray.joined(separator: " > ") + } + override func viewDidLoad() { super.viewDidLoad() tableView.register(UINib(nibName: "LabelTableViewCell", bundle: nil), forCellReuseIdentifier: "labelCell") tableView.register(UINib(nibName: "PasswordDetailTitleTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordDetailTitleTableViewCell") - let passwordCategoryArray = passwordCategoryEntities?.map { $0.category! } - passwordCategoryText = (passwordCategoryArray?.joined(separator: " > "))! + passwordCategoryText = generateCategoryText() let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PasswordDetailTableViewController.tapMenu(recognizer:))) tableView.addGestureRecognizer(tapGesture) @@ -300,7 +309,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni footerLabel.numberOfLines = 0 footerLabel.font = UIFont.preferredFont(forTextStyle: .footnote) footerLabel.textColor = UIColor.gray - let dateString = PasswordStore.shared.getLatestCommitDate(filename: (passwordEntity?.rawPath)!) + let dateString = PasswordStore.shared.getLatestCommitDate(filename: (passwordEntity?.path)!) footerLabel.text = "Last Updated: \(dateString ?? "Unknown")" view.addSubview(footerLabel) return view diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index 1e3dba4..02e6636 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -11,9 +11,29 @@ import SVProgressHUD import SwiftyUserDefaults import PasscodeLock +enum PasswordsTableEntryType { + case password, dir +} + +struct PasswordsTableEntry { + var title: String + var isDir: Bool + var passwordEntity: PasswordEntity? +} + class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - private var passwordEntities: [PasswordEntity]? - var filteredPasswordEntities = [PasswordEntity]() + private var passwordsTableEntries: [PasswordsTableEntry] = [] + private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + private var parentPasswordEntity: PasswordEntity? = nil + + private func initPasswordsTableEntries() { + passwordsTableEntries.removeAll() + filteredPasswordsTableEntries.removeAll() + passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(parent: parentPasswordEntity).map { + PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0) + } + } + var sections : [(index: Int, length :Int, title: String)] = Array() var searchActive : Bool = false let searchController = UISearchController(searchResultsController: nil) @@ -23,6 +43,10 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return refreshControl }() let searchBarView = UIView(frame: CGRect(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 44)) + lazy var backUIBarButtonItem: UIBarButtonItem = { + let backUIBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(self.backAction(_:))) + return backUIBarButtonItem + }() @IBOutlet weak var tableView: UITableView! @@ -48,6 +72,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } } } + func syncPasswords() { SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultStyle(.light) @@ -69,8 +94,9 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } DispatchQueue.main.async { PasswordStore.shared.updatePasswordEntityCoreData() - self.passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData() - self.reloadTableView(data: self.passwordEntities!) + self.parentPasswordEntity = nil + self.initPasswordsTableEntries() + self.reloadTableView(data: self.passwordsTableEntries) PasswordStore.shared.setAllSynced() self.setNavigationItemTitle() Defaults[.lastUpdatedTime] = Date() @@ -90,12 +116,12 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV override func viewDidLoad() { super.viewDidLoad() setNavigationItemTitle() - passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData() + initPasswordsTableEntries() NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordUpdatedNotification), name: NSNotification.Name(rawValue: "passwordUpdated"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordStoreErasedNotification), name: NSNotification.Name(rawValue: "passwordStoreErased"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnSearchNotification), name: NSNotification.Name(rawValue: "search"), object: nil) - generateSections(item: passwordEntities!) + generateSections(item: passwordsTableEntries) tableView.delegate = self tableView.dataSource = self searchController.searchResultsUpdater = self @@ -134,17 +160,15 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) - var password: PasswordEntity - let index = sections[indexPath.section].index + indexPath.row - if searchController.isActive && searchController.searchBar.text != "" { - password = filteredPasswordEntities[index] + let entry = getPasswordEntry(by: indexPath) + if !entry.isDir { + if entry.passwordEntity!.synced { + cell.textLabel?.text = entry.title + } else { + cell.textLabel?.text = "↻ \(entry.title)" + } } else { - password = passwordEntities![index] - } - if password.synced { - cell.textLabel?.text = password.name! - } else { - cell.textLabel?.text = "↻ \(password.name!)" + cell.textLabel?.text = "\(entry.title)/" } let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:))) longPressGestureRecognizer.minimumPressDuration = 0.6 @@ -152,6 +176,35 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return cell } + private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry{ + var entry: PasswordsTableEntry + let index = sections[indexPath.section].index + indexPath.row + if searchController.isActive && searchController.searchBar.text != "" { + entry = filteredPasswordsTableEntries[index] + } else { + entry = passwordsTableEntries[index] + } + return entry + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let entry = getPasswordEntry(by: indexPath) + if !entry.isDir { + performSegue(withIdentifier: "showPasswordDetail", sender: tableView.cellForRow(at: indexPath)) + } else { + tableView.deselectRow(at: indexPath, animated: true) + parentPasswordEntity = entry.passwordEntity + initPasswordsTableEntries() + reloadTableView(data: passwordsTableEntries) + } + } + + func backAction(_ sender: Any?) { + parentPasswordEntity = parentPasswordEntity?.parent + initPasswordsTableEntries() + reloadTableView(data: passwordsTableEntries) + } + func longPressAction(_ gesture: UILongPressGestureRecognizer) { if gesture.state == UIGestureRecognizerState.began { let touchPoint = gesture.location(in: tableView) @@ -185,9 +238,9 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV let index = sections[indexPath.section].index + indexPath.row let password: PasswordEntity if searchController.isActive && searchController.searchBar.text != "" { - password = filteredPasswordEntities[index] + password = passwordsTableEntries[index].passwordEntity! } else { - password = passwordEntities![index] + password = filteredPasswordsTableEntries[index].passwordEntity! } UIImpactFeedbackGenerator(style: .medium).impactOccurred() var passphrase = "" @@ -232,33 +285,34 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } } - func generateSections(item: [PasswordEntity]) { + func generateSections(item: [PasswordsTableEntry]) { sections.removeAll() - if item.count == 0 { + guard item.count != 0 else { return } var index = 0 for i in 0 ..< item.count { - let name = item[index].name!.uppercased() - let commonPrefix = item[i].name!.commonPrefix(with: name, options: .caseInsensitive) + let title = item[index].title.uppercased() + let commonPrefix = item[i].title.commonPrefix(with: title, options: .caseInsensitive) if commonPrefix.characters.count == 0 { - let firstCharacter = name[name.startIndex] + let firstCharacter = title[title.startIndex] let newSection = (index: index, length: i - index, title: "\(firstCharacter)") sections.append(newSection) index = i } } - let name = item[index].name!.uppercased() - let firstCharacter = name[name.startIndex] + let title = item[index].title.uppercased() + let firstCharacter = title[title.startIndex] let newSection = (index: index, length: item.count - index, title: "\(firstCharacter)") sections.append(newSection) } func actOnPasswordUpdatedNotification() { - passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData() - reloadTableView(data: passwordEntities!) + initPasswordsTableEntries() + reloadTableView(data: passwordsTableEntries) setNavigationItemTitle() } + private func setNavigationItemTitle() { let numberOfUnsynced = PasswordStore.shared.getNumberOfUnsyncedPasswords() if numberOfUnsynced == 0 { @@ -269,8 +323,8 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } func actOnPasswordStoreErasedNotification() { - passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData() - reloadTableView(data: passwordEntities!) + initPasswordsTableEntries() + reloadTableView(data: passwordsTableEntries) setNavigationItemTitle() } @@ -297,28 +351,20 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV if segue.identifier == "showPasswordDetail" { if let viewController = segue.destination as? PasswordDetailTableViewController { let selectedIndexPath = self.tableView.indexPath(for: sender as! UITableViewCell)! - let index = sections[selectedIndexPath.section].index + selectedIndexPath.row - let passwordEntity: PasswordEntity - if searchController.isActive && searchController.searchBar.text != "" { - passwordEntity = filteredPasswordEntities[index] - } else { - passwordEntity = passwordEntities![index] - } + let passwordEntity = getPasswordEntry(by: selectedIndexPath).passwordEntity! viewController.passwordEntity = passwordEntity - let passwordCategoryEntities = PasswordStore.shared.fetchPasswordCategoryEntityCoreData(password: passwordEntity) - viewController.passwordCategoryEntities = passwordCategoryEntities } } } func filterContentForSearchText(searchText: String, scope: String = "All") { - filteredPasswordEntities = passwordEntities!.filter { password in - return password.name!.lowercased().contains(searchText.lowercased()) + filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in + return entry.title.lowercased().contains(searchText.lowercased()) } if searchController.isActive && searchController.searchBar.text != "" { - reloadTableView(data: filteredPasswordEntities) + reloadTableView(data: filteredPasswordsTableEntries) } else { - reloadTableView(data: passwordEntities!) + reloadTableView(data: passwordsTableEntries) } } @@ -328,7 +374,12 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle) } - func reloadTableView (data: [PasswordEntity]) { + func reloadTableView(data: [PasswordsTableEntry]) { + if parentPasswordEntity != nil { + navigationItem.leftBarButtonItem = backUIBarButtonItem + } else { + navigationItem.leftBarButtonItem = nil + } generateSections(item: data) tableView.reloadData() updateRefreshControlTitle() diff --git a/pass/Models/PasswordEntity.swift b/pass/Models/PasswordEntity.swift index d293383..f881279 100644 --- a/pass/Models/PasswordEntity.swift +++ b/pass/Models/PasswordEntity.swift @@ -12,7 +12,7 @@ import SwiftyUserDefaults extension PasswordEntity { func decrypt(passphrase: String) throws -> Password? { var password: Password? - let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(rawPath!)") + let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(path!)") let encryptedData = try Data(contentsOf: encryptedDataPath) let decryptedData = try PasswordStore.shared.pgp.decryptData(encryptedData, passphrase: passphrase) let plainText = String(data: decryptedData, encoding: .utf8) ?? "" diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index 915c11a..7607233 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -237,27 +237,56 @@ class PasswordStore { try storeRepository?.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock) } + + func updatePasswordEntityCoreData() { deleteCoreData(entityName: "PasswordEntity") - deleteCoreData(entityName: "PasswordCategoryEntity") let fm = FileManager.default - fm.enumerator(atPath: self.storeURL.path)?.forEach({ (e) in - if let e = e as? String, let url = URL(string: e) { - if url.pathExtension == "gpg" { - let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity - let endIndex = url.lastPathComponent.index(url.lastPathComponent.endIndex, offsetBy: -4) - passwordEntity.name = url.lastPathComponent.substring(to: endIndex) - passwordEntity.rawPath = "\(url.path)" - let items = url.path.characters.split(separator: "/").map(String.init) - for i in 0 ..< items.count - 1 { - let passwordCategoryEntity = PasswordCategoryEntity(context: context) - passwordCategoryEntity.category = items[i] - passwordCategoryEntity.level = Int16(i) - passwordCategoryEntity.password = passwordEntity + do { + var q = try fm.contentsOfDirectory(atPath: self.storeURL.path).filter{ + !$0.hasPrefix(".") + }.map { (filename) -> PasswordEntity in + let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity + if filename.hasSuffix(".gpg") { + passwordEntity.name = filename.substring(to: filename.index(filename.endIndex, offsetBy: -4)) + } else { + passwordEntity.name = filename + } + passwordEntity.path = filename + passwordEntity.parent = nil + return passwordEntity + } + while q.count > 0 { + let e = q.first! + q.remove(at: 0) + guard !e.name!.hasPrefix(".") else { + continue + } + var isDirectory: ObjCBool = false + let filePath = storeURL.appendingPathComponent(e.path!).path + if fm.fileExists(atPath: filePath, isDirectory: &isDirectory) { + if isDirectory.boolValue { + e.isDir = true + let files = try fm.contentsOfDirectory(atPath: filePath).map { (filename) -> PasswordEntity in + let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity + if filename.hasSuffix(".gpg") { + passwordEntity.name = filename.substring(to: filename.index(filename.endIndex, offsetBy: -4)) + } else { + passwordEntity.name = filename + } + passwordEntity.path = "\(e.path!)/\(filename)" + passwordEntity.parent = e + return passwordEntity + } + q += files + } else { + e.isDir = false } } } - }) + } catch { + print(error) + } do { try context.save() } catch { @@ -281,9 +310,10 @@ class PasswordStore { return commits } - func fetchPasswordEntityCoreData() -> [PasswordEntity] { + func fetchPasswordEntityCoreData(parent: PasswordEntity?) -> [PasswordEntity] { let passwordEntityFetch = NSFetchRequest(entityName: "PasswordEntity") do { + passwordEntityFetch.predicate = NSPredicate(format: "parent = %@", parent ?? 0) let fetchedPasswordEntities = try context.fetch(passwordEntityFetch) as! [PasswordEntity] return fetchedPasswordEntities.sorted { $0.name!.caseInsensitiveCompare($1.name!) == .orderedAscending } } catch { @@ -291,18 +321,21 @@ class PasswordStore { } } - func fetchPasswordCategoryEntityCoreData(password: PasswordEntity) -> [PasswordCategoryEntity] { - let passwordCategoryEntityFetchRequest = NSFetchRequest(entityName: "PasswordCategoryEntity") - passwordCategoryEntityFetchRequest.predicate = NSPredicate(format: "password = %@", password) - passwordCategoryEntityFetchRequest.sortDescriptors = [NSSortDescriptor(key: "level", ascending: true)] + func fetchPasswordEntityCoreData(withDir: Bool) -> [PasswordEntity] { + let passwordEntityFetch = NSFetchRequest(entityName: "PasswordEntity") do { - let passwordCategoryEntities = try context.fetch(passwordCategoryEntityFetchRequest) as! [PasswordCategoryEntity] - return passwordCategoryEntities + if !withDir { + passwordEntityFetch.predicate = NSPredicate(format: "isDir = false") + + } + let fetchedPasswordEntities = try context.fetch(passwordEntityFetch) as! [PasswordEntity] + return fetchedPasswordEntities.sorted { $0.name!.caseInsensitiveCompare($1.name!) == .orderedAscending } } catch { - fatalError("Failed to fetch password categories: \(error)") + fatalError("Failed to fetch passwords: \(error)") } } + func fetchUnsyncedPasswords() -> [PasswordEntity] { let passwordEntityFetchRequest = NSFetchRequest(entityName: "PasswordEntity") passwordEntityFetchRequest.predicate = NSPredicate(format: "synced = %i", 0) @@ -452,7 +485,9 @@ class PasswordStore { progressBlock(0.3) let saveURL = storeURL.appendingPathComponent("\(password.name).gpg") try encryptedData.write(to: saveURL) - passwordEntity.rawPath = "\(password.name).gpg" + passwordEntity.name = password.name + passwordEntity.path = "\(password.name).gpg" + passwordEntity.parent = nil passwordEntity.synced = false try context.save() print(saveURL.path) @@ -466,7 +501,7 @@ class PasswordStore { func update(passwordEntity: PasswordEntity, password: Password, progressBlock: (_ progress: Float) -> Void) { do { let encryptedData = try passwordEntity.encrypt(password: password) - let saveURL = storeURL.appendingPathComponent(passwordEntity.rawPath!) + let saveURL = storeURL.appendingPathComponent(passwordEntity.path!) try encryptedData.write(to: saveURL) progressBlock(0.3) let _ = createAddCommitInRepository(message: "Update password by pass for iOS", fileData: encryptedData, filename: saveURL.lastPathComponent, progressBlock: progressBlock) diff --git a/pass/pass.xcdatamodeld/pass.xcdatamodel/contents b/pass/pass.xcdatamodeld/pass.xcdatamodel/contents index ede2475..00d0473 100644 --- a/pass/pass.xcdatamodeld/pass.xcdatamodel/contents +++ b/pass/pass.xcdatamodeld/pass.xcdatamodel/contents @@ -1,20 +1,15 @@ - - - - - - + + - - + - + + - - + \ No newline at end of file