Update UI to support more customizable password generator

This commit is contained in:
Danny Moesch 2020-02-28 19:05:23 +01:00 committed by Mingshen Sun
parent ff014a5699
commit b84f2dce13
8 changed files with 252 additions and 71 deletions

View file

@ -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 = "<group>"; };
30697C5221F63E0B0064FCAC /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = "<group>"; };
30697C5E21F674800064FCAC /* String+UtilitiesTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+UtilitiesTest.swift"; sourceTree = "<group>"; };
306D970D24091CDD006C0E2E /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = "<group>"; };
306D971124091EE7006C0E2E /* SwitchTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchTableViewCell.xib; sourceTree = "<group>"; };
3087574E2343E42A00B971A2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
30A1D29B21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordGeneratorFlavorTest.swift; sourceTree = "<group>"; };
30A1D2A121B2BC6F00E2D1F7 /* TokenBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBuilder.swift; sourceTree = "<group>"; };
@ -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 */,

View file

@ -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<PasswordEditorCellKey, Any>]]()
@ -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
@ -207,17 +245,26 @@ class PasswordEditorTableViewController: UITableViewController {
}
}
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<PasswordEditorCellKey, Any>) -> 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

View file

@ -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 {
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)
return self
}
// "not editable"
if minimumValue == maximumValue {
titleLabel.textColor = UIColor.gray
valueLabel.textColor = UIColor.gray
slider.isUserInteractionEnabled = false
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?) {}
}

View file

@ -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
}
}

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="71" id="ZfB-GH-J5H" customClass="SwitchTableViewCell" customModule="pass" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="74"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZfB-GH-J5H" id="yOK-hg-p5T">
<rect key="frame" x="0.0" y="0.0" width="320" height="74"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZcB-yD-ZwW" userLabel="Title">
<rect key="frame" x="15" y="11" width="26.5" height="52"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="38" id="KbB-1u-gvz"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uza-Tn-Tvb">
<rect key="frame" x="263" y="20" width="51" height="34"/>
<connections>
<action selector="switchValueChanged:" destination="ZfB-GH-J5H" eventType="valueChanged" id="7M4-rU-jkI"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="uza-Tn-Tvb" firstAttribute="trailing" secondItem="yOK-hg-p5T" secondAttribute="trailingMargin" constant="7" id="AI4-cq-01M"/>
<constraint firstAttribute="bottom" secondItem="uza-Tn-Tvb" secondAttribute="bottom" constant="20" symbolic="YES" id="JHm-gr-hAx"/>
<constraint firstItem="uza-Tn-Tvb" firstAttribute="top" secondItem="yOK-hg-p5T" secondAttribute="top" constant="20" symbolic="YES" id="YPW-8k-up9"/>
<constraint firstItem="ZcB-yD-ZwW" firstAttribute="leading" secondItem="yOK-hg-p5T" secondAttribute="leadingMargin" id="lFa-jJ-Jc7"/>
<constraint firstAttribute="bottomMargin" secondItem="ZcB-yD-ZwW" secondAttribute="bottom" id="nMt-uF-ECF"/>
<constraint firstItem="ZcB-yD-ZwW" firstAttribute="top" secondItem="yOK-hg-p5T" secondAttribute="topMargin" id="wJh-hO-b3q"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="controlSwitch" destination="uza-Tn-Tvb" id="XcC-iD-dpH"/>
<outlet property="titleLabel" destination="ZcB-yD-ZwW" id="be8-Ec-Iu5"/>
</connections>
<point key="canvasLocation" x="-106" y="64"/>
</tableViewCell>
</objects>
</document>

View file

@ -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";

View file

@ -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";

View file

@ -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
}