From b84f2dce1325b5647d2fb0379900a07b80c63b0b Mon Sep 17 00:00:00 2001 From: Danny Moesch Date: Fri, 28 Feb 2020 19:05:23 +0100 Subject: [PATCH] Update UI to support more customizable password generator --- pass.xcodeproj/project.pbxproj | 8 ++ .../PasswordEditorTableViewController.swift | 119 +++++++++++++----- pass/Views/SliderTableViewCell.swift | 82 ++++++------ pass/Views/SwitchTableViewCell.swift | 45 +++++++ pass/Views/SwitchTableViewCell.xib | 51 ++++++++ pass/de.lproj/Localizable.strings | 5 +- pass/en.lproj/Localizable.strings | 5 +- passKit/Helpers/Colors.swift | 8 +- 8 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 pass/Views/SwitchTableViewCell.swift create mode 100644 pass/Views/SwitchTableViewCell.xib diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index ec79d41..9d4969f 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ 30697C5321F63E0B0064FCAC /* PasscodeExtensionDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30697C5121F63E0B0064FCAC /* PasscodeExtensionDisplay.swift */; }; 30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30697C5221F63E0B0064FCAC /* CredentialProviderViewController.swift */; }; 30697C5F21F674800064FCAC /* String+UtilitiesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30697C5E21F674800064FCAC /* String+UtilitiesTest.swift */; }; + 306D970E24091CDD006C0E2E /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306D970D24091CDD006C0E2E /* SwitchTableViewCell.swift */; }; + 306D971224091EE7006C0E2E /* SwitchTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 306D971124091EE7006C0E2E /* SwitchTableViewCell.xib */; }; 3087574F2343E42A00B971A2 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3087574E2343E42A00B971A2 /* Colors.swift */; }; 308C273A2279F9CB0016D0E2 /* SearchBarScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302202EE222F14E400555236 /* SearchBarScope.swift */; }; 30A1D29C21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1D29B21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift */; }; @@ -278,6 +280,8 @@ 30697C5121F63E0B0064FCAC /* PasscodeExtensionDisplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeExtensionDisplay.swift; sourceTree = ""; }; 30697C5221F63E0B0064FCAC /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = ""; }; 30697C5E21F674800064FCAC /* String+UtilitiesTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+UtilitiesTest.swift"; sourceTree = ""; }; + 306D970D24091CDD006C0E2E /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; + 306D971124091EE7006C0E2E /* SwitchTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchTableViewCell.xib; sourceTree = ""; }; 3087574E2343E42A00B971A2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 30A1D29B21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordGeneratorFlavorTest.swift; sourceTree = ""; }; 30A1D2A121B2BC6F00E2D1F7 /* TokenBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBuilder.swift; sourceTree = ""; }; @@ -786,6 +790,8 @@ DCFB77A21E500D9C008DE471 /* PasswordDetailTitleTableViewCell.xib */, A2802BF71E70813A00879216 /* SliderTableViewCell.swift */, A2802BF81E70813A00879216 /* SliderTableViewCell.xib */, + 306D970D24091CDD006C0E2E /* SwitchTableViewCell.swift */, + 306D971124091EE7006C0E2E /* SwitchTableViewCell.xib */, DC037CB91E4DD47B00609409 /* TextFieldTableViewCell.swift */, DC037CBA1E4DD47B00609409 /* TextFieldTableViewCell.xib */, DC037CBD1E4ED4E100609409 /* TextViewTableViewCell.swift */, @@ -1160,6 +1166,7 @@ A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */, DCFB779F1E4F40C7008DE471 /* FillPasswordTableViewCell.xib in Resources */, DC037CC01E4ED4E100609409 /* TextViewTableViewCell.xib in Resources */, + 306D971224091EE7006C0E2E /* SwitchTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1423,6 +1430,7 @@ files = ( DC037CBF1E4ED4E100609409 /* TextViewTableViewCell.swift in Sources */, DCC441541E916382008A90C4 /* SSHKeyArmorImportTableViewController.swift in Sources */, + 306D970E24091CDD006C0E2E /* SwitchTableViewCell.swift in Sources */, A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */, DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */, 3066AD6823EE0D6500F65535 /* PGPKeyImporter.swift in Sources */, diff --git a/pass/Controllers/PasswordEditorTableViewController.swift b/pass/Controllers/PasswordEditorTableViewController.swift index 32aae56..2084e5f 100644 --- a/pass/Controllers/PasswordEditorTableViewController.swift +++ b/pass/Controllers/PasswordEditorTableViewController.swift @@ -11,14 +11,29 @@ import SafariServices import OneTimePassword import passKit -enum PasswordEditorCellType { - case nameCell, fillPasswordCell, passwordLengthCell, additionsCell, deletePasswordCell, scanQRCodeCell, passwordFlavorCell +enum PasswordEditorCellType: Equatable { + case nameCell + case fillPasswordCell + case passwordLengthCell + case passwordUseDigitsCell + case passwordVaryCasesCell + case passwordUseSpecialSymbols + case passwordGroupsCell + case additionsCell + case deletePasswordCell + case scanQRCodeCell + case passwordFlavorCell } enum PasswordEditorCellKey { case type, title, content, placeholders } +protocol PasswordSettingSliderTableViewCellDelegate { + + func generateAndCopyPassword() +} + class PasswordEditorTableViewController: UITableViewController { var tableData = [[Dictionary]]() @@ -33,9 +48,10 @@ class PasswordEditorTableViewController: UITableViewController { private let additionsSection = 2 private var hidePasswordSettings = true + private var passwordGenerator: PasswordGenerator = Defaults.passwordGenerator + var nameCell: TextFieldTableViewCell? var fillPasswordCell: FillPasswordTableViewCell? - private var passwordLengthCell: SliderTableViewCell? var additionsCell: TextViewTableViewCell? private var deletePasswordCell: UITableViewCell? private var scanQRCodeCell: UITableViewCell? @@ -72,7 +88,7 @@ class PasswordEditorTableViewController: UITableViewController { passwordFlavorCell?.textLabel?.textColor = Colors.systemBlue passwordFlavorCell?.selectionStyle = .none passwordFlavorCell?.accessoryType = .disclosureIndicator - passwordFlavorCell?.detailTextLabel?.text = Defaults.passwordGeneratorFlavor.localized + passwordFlavorCell?.detailTextLabel?.text = passwordGenerator.flavor.localized } override func viewDidLoad() { @@ -84,7 +100,8 @@ class PasswordEditorTableViewController: UITableViewController { tableView.register(UINib(nibName: "TextFieldTableViewCell", bundle: nil), forCellReuseIdentifier: "textFieldCell") tableView.register(UINib(nibName: "TextViewTableViewCell", bundle: nil), forCellReuseIdentifier: "textViewCell") tableView.register(UINib(nibName: "FillPasswordTableViewCell", bundle: nil), forCellReuseIdentifier: "fillPasswordCell") - tableView.register(UINib(nibName: "SliderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordLengthCell") + tableView.register(UINib(nibName: "SliderTableViewCell", bundle: nil), forCellReuseIdentifier: "sliderCell") + tableView.register(UINib(nibName: "SwitchTableViewCell", bundle: nil), forCellReuseIdentifier: "switchCell") tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 48 @@ -97,7 +114,10 @@ class PasswordEditorTableViewController: UITableViewController { ], [ [.type: PasswordEditorCellType.fillPasswordCell, .title: "Password".localize(), .content: password?.password ?? ""], - [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"], + [.type: PasswordEditorCellType.passwordLengthCell], + [.type: PasswordEditorCellType.passwordUseDigitsCell], + [.type: PasswordEditorCellType.passwordVaryCasesCell], + [.type: PasswordEditorCellType.passwordUseSpecialSymbols], [.type: PasswordEditorCellType.passwordFlavorCell], ], [ @@ -108,6 +128,7 @@ class PasswordEditorTableViewController: UITableViewController { [.type: PasswordEditorCellType.deletePasswordCell], ] ] + updateTableData(withRespectTo: passwordGenerator.flavor) } override func viewDidLayoutSubviews() { @@ -133,26 +154,43 @@ class PasswordEditorTableViewController: UITableViewController { } return fillPasswordCell! case .passwordLengthCell: - passwordLengthCell = tableView.dequeueReusableCell(withIdentifier: "passwordLengthCell", for: indexPath) as? SliderTableViewCell - let lengthSetting = Defaults.passwordGeneratorFlavor.defaultLength - let minimumLength = lengthSetting.min - let maximumLength = lengthSetting.max - var defaultLength = lengthSetting.def - if let currentPasswordLength = (tableData[passwordSection][0][PasswordEditorCellKey.content] as? String)?.count, - currentPasswordLength >= minimumLength, - currentPasswordLength <= maximumLength { - defaultLength = currentPasswordLength - } - passwordLengthCell?.reset(title: "Length".localize(), - minimumValue: minimumLength, - maximumValue: maximumLength, - defaultValue: defaultLength) - passwordLengthCell?.delegate = self - return passwordLengthCell! + return (tableView.dequeueReusableCell(withIdentifier: "sliderCell", for: indexPath) as! SliderTableViewCell) + .set(title: "Length".localize()) + .configureSlider(with: passwordGenerator.flavor.lengthLimits) + .set(initialValue: passwordGenerator.limitedLength) + .checkNewValue { $0 != self.passwordGenerator.length } + .updateNewValue { self.passwordGenerator.length = $0 } + .delegate(to: self) + case .passwordUseDigitsCell: + return (tableView.dequeueReusableCell(withIdentifier: "switchCell", for: indexPath) as! SwitchTableViewCell) + .set(title: "Digits".localize()) + .set(initialValue: passwordGenerator.useDigits) + .updateNewValue { self.passwordGenerator.useDigits = $0 } + .delegate(to: self) + case .passwordVaryCasesCell: + return (tableView.dequeueReusableCell(withIdentifier: "switchCell", for: indexPath) as! SwitchTableViewCell) + .set(title: "VaryCases".localize()) + .set(initialValue: passwordGenerator.varyCases) + .updateNewValue { self.passwordGenerator.varyCases = $0 } + .delegate(to: self) + case .passwordUseSpecialSymbols: + return (tableView.dequeueReusableCell(withIdentifier: "switchCell", for: indexPath) as! SwitchTableViewCell) + .set(title: "SpecialSymbols".localize()) + .set(initialValue: passwordGenerator.useSpecialSymbols) + .updateNewValue { self.passwordGenerator.useSpecialSymbols = $0 } + .delegate(to: self) + case .passwordGroupsCell: + return (tableView.dequeueReusableCell(withIdentifier: "sliderCell", for: indexPath) as! SliderTableViewCell) + .set(title: "Groups".localize()) + .configureSlider(with: (min: 0, max: 6)) + .set(initialValue: passwordGenerator.groups) + .checkNewValue { $0 != self.passwordGenerator.groups && self.passwordGenerator.isAcceptable(groups: $0) } + .updateNewValue { self.passwordGenerator.groups = $0 } + .delegate(to: self) case .passwordFlavorCell: return passwordFlavorCell! case .additionsCell: - additionsCell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as?TextViewTableViewCell + additionsCell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as? TextViewTableViewCell additionsCell?.contentTextView.delegate = self additionsCell?.setContent(content: cellData[PasswordEditorCellKey.content] as? String) additionsCell?.contentTextView.textColor = Colors.label @@ -206,18 +244,27 @@ class PasswordEditorTableViewController: UITableViewController { showPasswordGeneratorFlavorActionSheet(sourceCell: selectedCell!, tableView: tableView) } } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + Defaults.passwordGenerator = passwordGenerator + } private func showPasswordGeneratorFlavorActionSheet(sourceCell: UITableViewCell, tableView: UITableView) { let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) PasswordGeneratorFlavor.allCases.forEach { flavor in var actionTitle = flavor.longNameLocalized - if Defaults.passwordGeneratorFlavor == flavor { + if passwordGenerator.flavor == flavor { actionTitle = "✓ " + actionTitle } let action = UIAlertAction(title: actionTitle, style: .default) { _ in - Defaults.passwordGeneratorFlavor = flavor - sourceCell.detailTextLabel?.text = Defaults.passwordGeneratorFlavor.localized + guard self.passwordGenerator.flavor != flavor else { + return + } + self.passwordGenerator.flavor = flavor + sourceCell.detailTextLabel?.text = self.passwordGenerator.flavor.localized + self.updateTableData(withRespectTo: flavor) tableView.reloadSections([self.passwordSection], with: .none) } optionMenu.addAction(action) @@ -230,13 +277,29 @@ class PasswordEditorTableViewController: UITableViewController { self.present(optionMenu, animated: true, completion: nil) } + private func updateTableData(withRespectTo flavor: PasswordGeneratorFlavor) { + // Remove delimiter configuration for XKCD style passwords. Re-add it for random ones. + switch flavor { + case .random: + guard tableData[1].first(where: isPasswordDelimiterCellData) == nil else { + return + } + tableData[1].insert([.type: PasswordEditorCellType.passwordGroupsCell], at: tableData[1].endIndex - 1) + case .xkcd: + tableData[1].removeAll(where: isPasswordDelimiterCellData) + } + } + + private func isPasswordDelimiterCellData(data: Dictionary) -> Bool { + return (data[.type] as? PasswordEditorCellType) == .some(.passwordGroupsCell) + } + // generate the password, don't care whether the original line is otp private func generateAndCopyPasswordNoOtpCheck() { // show password settings (e.g., the length slider) showPasswordSettings() - let length = passwordLengthCell?.roundedValue ?? 0 - let plainPassword = Defaults.passwordGeneratorFlavor.generate(length: length) + let plainPassword = passwordGenerator.generate() // update tableData so to make sure reloadData() works correctly tableData[passwordSection][0][PasswordEditorCellKey.content] = plainPassword diff --git a/pass/Views/SliderTableViewCell.swift b/pass/Views/SliderTableViewCell.swift index 8de7e4c..a541615 100644 --- a/pass/Views/SliderTableViewCell.swift +++ b/pass/Views/SliderTableViewCell.swift @@ -6,59 +6,71 @@ // Copyright © 2017 Yishi Lin. All rights reserved. // - +import passKit import UIKit -protocol PasswordSettingSliderTableViewCellDelegate { - func generateAndCopyPassword() -} +class SliderTableViewCell: UITableViewCell { -class SliderTableViewCell: UITableViewCell, ContentProvider { + @IBOutlet var titleLabel: UILabel! + @IBOutlet var valueLabel: UILabel! + @IBOutlet var slider: UISlider! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var valueLabel: UILabel! - @IBOutlet weak var slider: UISlider! + private var checker: ((Int) -> Bool)! + private var updater: ((Int) -> Void)! - var delegate: UITableViewController? - - var roundedValue: Int { - get { - return Int(valueLabel.text!)! - } - } + private var delegate: PasswordSettingSliderTableViewCellDelegate! @IBAction func handleSliderValueChange(_ sender: UISlider) { - let oldRoundedValue = self.roundedValue let newRoundedValue = Int(sender.value) - // proceed only when the rounded value gets updated - guard newRoundedValue != oldRoundedValue else { - return; + // Proceed only if the rounded value gets updated. + guard checker(newRoundedValue) else { + return } sender.value = Float(newRoundedValue) valueLabel.text = "\(newRoundedValue)" - if let delegate: PasswordSettingSliderTableViewCellDelegate = self.delegate as? PasswordSettingSliderTableViewCellDelegate { - delegate.generateAndCopyPassword() - } + + updater(newRoundedValue) + delegate.generateAndCopyPassword() } - func reset(title: String, minimumValue: Int, maximumValue: Int, defaultValue: Int) { + func set(title: String) -> SliderTableViewCell { titleLabel.text = title - slider.minimumValue = Float(minimumValue) - slider.maximumValue = Float(maximumValue) - slider.value = Float(defaultValue) - valueLabel.text = String(defaultValue) - - // "not editable" - if minimumValue == maximumValue { - titleLabel.textColor = UIColor.gray - valueLabel.textColor = UIColor.gray - slider.isUserInteractionEnabled = false - } + return self } + func configureSlider(with configuration: LengthLimits) -> SliderTableViewCell { + slider.minimumValue = Float(configuration.min) + slider.maximumValue = Float(configuration.max) + return self + } + + func set(initialValue: Int) -> SliderTableViewCell { + slider.value = Float(initialValue) + valueLabel.text = String(initialValue) + return self + } + + func checkNewValue(with checker: @escaping (Int) -> Bool) -> SliderTableViewCell { + self.checker = checker + return self + } + + func updateNewValue(using updater: @escaping (Int) -> Void) -> SliderTableViewCell { + self.updater = updater + return self + } + + func delegate(to delegate: PasswordSettingSliderTableViewCellDelegate) -> SliderTableViewCell { + self.delegate = delegate + return self + } +} + +extension SliderTableViewCell: ContentProvider { + func getContent() -> String? { return nil } - func setContent(content: String?) {} + func setContent(content _: String?) {} } diff --git a/pass/Views/SwitchTableViewCell.swift b/pass/Views/SwitchTableViewCell.swift new file mode 100644 index 0000000..c515a22 --- /dev/null +++ b/pass/Views/SwitchTableViewCell.swift @@ -0,0 +1,45 @@ +// +// SwitchTableViewCell.swift +// pass +// +// Created by Danny Moesch on 28.02.20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import passKit +import UIKit + +class SwitchTableViewCell: UITableViewCell { + + @IBOutlet var titleLabel: UILabel! + @IBOutlet var controlSwitch: UISwitch! + + private var updater: ((Bool) -> Void)! + + private var delegate: PasswordSettingSliderTableViewCellDelegate! + + @IBAction func switchValueChanged(_: Any) { + updater(controlSwitch.isOn) + delegate.generateAndCopyPassword() + } + + func set(title: String) -> SwitchTableViewCell { + titleLabel.text = title + return self + } + + func set(initialValue: Bool) -> SwitchTableViewCell { + controlSwitch.isOn = initialValue + return self + } + + func updateNewValue(using updater: @escaping (Bool) -> Void) -> SwitchTableViewCell { + self.updater = updater + return self + } + + func delegate(to delegate: PasswordSettingSliderTableViewCellDelegate) -> SwitchTableViewCell { + self.delegate = delegate + return self + } +} diff --git a/pass/Views/SwitchTableViewCell.xib b/pass/Views/SwitchTableViewCell.xib new file mode 100644 index 0000000..45fb0e6 --- /dev/null +++ b/pass/Views/SwitchTableViewCell.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pass/de.lproj/Localizable.strings b/pass/de.lproj/Localizable.strings index 83eaa9b..9d7c722 100644 --- a/pass/de.lproj/Localizable.strings +++ b/pass/de.lproj/Localizable.strings @@ -255,8 +255,11 @@ "UseKeyValueFormat." = "Verwende das Format \"Name: Inhalt\" für zusätzliche Felder."; "DeletePassword" = "Passwort löschen"; "AddOneTimePassword" = "Einmalkennwort hinzufügen"; -"GetMemorableOne" = "Einprägsames Passwort: xkpasswd"; "Length" = "Länge"; +"VaryCases" = "Groß- und Kleinbuchstaben"; +"Digits" = "Ziffern"; +"SpecialSymbols" = "Sonderzeichen"; +"Groups" = "Bilde Gruppen"; "DeletePassword?" = "Passwort löschen?"; "OverwriteOtpConfiguration?" = "Konfiguration des Einmalkennwortes überschreiben?"; "ValidTokenUrl" = "Gültiges URL-Token"; diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 029eb8f..ee75bf1 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -255,8 +255,11 @@ "UseKeyValueFormat." = "Use \"key: value\" format for additional fields."; "DeletePassword" = "Delete Password"; "AddOneTimePassword" = "Add One-Time Password"; -"GetMemorableOne" = "Get a Memorable One: xkpasswd"; "Length" = "Length"; +"VaryCases" = "Random Capitalization"; +"Digits" = "Digits"; +"SpecialSymbols" = "Special Symbols"; +"Groups" = "Arrange Into Groups"; "DeletePassword?" = "Delete Password?"; "OverwriteOtpConfiguration?" = "Overwrite the one-time password configuration?"; "ValidTokenUrl" = "Valid token URL"; diff --git a/passKit/Helpers/Colors.swift b/passKit/Helpers/Colors.swift index 0273350..e56fdae 100644 --- a/passKit/Helpers/Colors.swift +++ b/passKit/Helpers/Colors.swift @@ -35,11 +35,7 @@ public struct Colors { return .init(red: 242.0, green: 242.0, blue: 247.0, alpha: 1.0) }() - public static let systemRed: UIColor = { - return .systemRed - }() + public static let systemRed = UIColor.systemRed - public static let systemBlue: UIColor = { - return .systemBlue - }() + public static let systemBlue = UIColor.systemBlue }