Rewrite PasswordViewController
This commit is contained in:
parent
372e897350
commit
68077bf04c
10 changed files with 676 additions and 729 deletions
|
|
@ -110,7 +110,6 @@
|
||||||
9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
||||||
9A58664825AAAB7E006719C2 /* SearchPassword.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A5865EF25AA944B006719C2 /* SearchPassword.storyboard */; };
|
9A58664825AAAB7E006719C2 /* SearchPassword.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A5865EF25AA944B006719C2 /* SearchPassword.storyboard */; };
|
||||||
9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58665025AADB76006719C2 /* CredentialProvider.swift */; };
|
9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58665025AADB76006719C2 /* CredentialProvider.swift */; };
|
||||||
9A58665825AADC49006719C2 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */; };
|
|
||||||
9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
||||||
9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
||||||
9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
||||||
|
|
@ -118,9 +117,15 @@
|
||||||
9A8A8387402FCCCECB1232A4 /* Pods_passKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */; };
|
9A8A8387402FCCCECB1232A4 /* Pods_passKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */; };
|
||||||
9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */; };
|
9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */; };
|
||||||
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
||||||
9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */; };
|
|
||||||
9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */; };
|
9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */; };
|
||||||
9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; };
|
9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; };
|
||||||
|
9AFC87D325B39FF3008D6060 /* PasswordNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */; };
|
||||||
|
9AFC87E225B3B5C6008D6060 /* PasswordNavigationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */; };
|
||||||
|
9AFC87F025B514AD008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; };
|
||||||
|
9AFC87F825B51742008D6060 /* PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87F725B51742008D6060 /* PasswordManager.swift */; };
|
||||||
|
9AFC880025B51EC3008D6060 /* PasswordEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */; };
|
||||||
|
9AFC882725B53BF4008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; };
|
||||||
|
9AFC882E25B53BF5008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; };
|
||||||
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
|
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
|
||||||
A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */; };
|
A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */; };
|
||||||
A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; };
|
A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; };
|
||||||
|
|
@ -166,7 +171,6 @@
|
||||||
DC3E64E61E656F11009A83DE /* CommitLogsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */; };
|
DC3E64E61E656F11009A83DE /* CommitLogsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */; };
|
||||||
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
|
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
|
||||||
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
|
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
|
||||||
DC5734AE1E439AD400D09270 /* PasswordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */; };
|
|
||||||
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */; };
|
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */; };
|
||||||
DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */; };
|
DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */; };
|
||||||
DC917BD71E2E8231000FDF54 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC917BD61E2E8231000FDF54 /* AppDelegate.swift */; };
|
DC917BD71E2E8231000FDF54 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC917BD61E2E8231000FDF54 /* AppDelegate.swift */; };
|
||||||
|
|
@ -386,9 +390,13 @@
|
||||||
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsTableDataSource.swift; sourceTree = "<group>"; };
|
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsTableDataSource.swift; sourceTree = "<group>"; };
|
||||||
9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSelectionDelegate.swift; sourceTree = "<group>"; };
|
9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSelectionDelegate.swift; sourceTree = "<group>"; };
|
||||||
9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = "<group>"; };
|
9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = "<group>"; };
|
|
||||||
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProvider.swift; sourceTree = "<group>"; };
|
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProvider.swift; sourceTree = "<group>"; };
|
||||||
9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = "<group>"; };
|
9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = "<group>"; };
|
||||||
|
9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
|
9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordNavigationDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = "<group>"; };
|
||||||
|
9AFC87F725B51742008D6060 /* PasswordManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManager.swift; sourceTree = "<group>"; };
|
||||||
|
9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEncryptor.swift; sourceTree = "<group>"; };
|
||||||
A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = "<group>"; };
|
A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = "<group>"; };
|
||||||
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
A2367B9F1EF0387000C8FE8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
A2367B9F1EF0387000C8FE8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
|
@ -441,7 +449,6 @@
|
||||||
DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = "<group>"; };
|
DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = "<group>"; };
|
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = "<group>"; };
|
||||||
DC547D572040664E838F3DB3 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = "<group>"; };
|
DC547D572040664E838F3DB3 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordsViewController.swift; sourceTree = "<group>"; };
|
|
||||||
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = "<group>"; };
|
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = "<group>"; };
|
||||||
DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSHKeyUrlImportTableViewController.swift; sourceTree = "<group>"; };
|
DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSHKeyUrlImportTableViewController.swift; sourceTree = "<group>"; };
|
||||||
DC917BD31E2E8231000FDF54 /* Pass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pass.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DC917BD31E2E8231000FDF54 /* Pass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pass.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -725,7 +732,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */,
|
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */,
|
||||||
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */,
|
|
||||||
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */,
|
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */,
|
||||||
);
|
);
|
||||||
path = Services;
|
path = Services;
|
||||||
|
|
@ -739,6 +745,17 @@
|
||||||
path = Protocols;
|
path = Protocols;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9AFC87E025B3B556008D6060 /* Services */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */,
|
||||||
|
9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */,
|
||||||
|
9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */,
|
||||||
|
9AFC87F725B51742008D6060 /* PasswordManager.swift */,
|
||||||
|
);
|
||||||
|
path = Services;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A2168A801EFD431A005EA873 /* Controllers */ = {
|
A2168A801EFD431A005EA873 /* Controllers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -897,7 +914,7 @@
|
||||||
DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */,
|
DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */,
|
||||||
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */,
|
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */,
|
||||||
DCFB77A81E502FF6008DE471 /* PasswordEditorTableViewController.swift */,
|
DCFB77A81E502FF6008DE471 /* PasswordEditorTableViewController.swift */,
|
||||||
DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */,
|
9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */,
|
||||||
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */,
|
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */,
|
||||||
302269B223E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift */,
|
302269B223E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift */,
|
||||||
3066AD6723EE0D6500F65535 /* PGPKeyImporter.swift */,
|
3066AD6723EE0D6500F65535 /* PGPKeyImporter.swift */,
|
||||||
|
|
@ -983,6 +1000,7 @@
|
||||||
DC917BD51E2E8231000FDF54 /* pass */ = {
|
DC917BD51E2E8231000FDF54 /* pass */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9AFC87E025B3B556008D6060 /* Services */,
|
||||||
DC19400C1E4B39400077E0A3 /* Controllers */,
|
DC19400C1E4B39400077E0A3 /* Controllers */,
|
||||||
DC19400E1E4B3A610077E0A3 /* Helpers */,
|
DC19400E1E4B3A610077E0A3 /* Helpers */,
|
||||||
3005F35224B13BF3000519B5 /* Models */,
|
3005F35224B13BF3000519B5 /* Models */,
|
||||||
|
|
@ -1568,13 +1586,13 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */,
|
9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */,
|
||||||
|
9AFC882E25B53BF5008D6060 /* PasswordDecryptor.swift in Sources */,
|
||||||
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */,
|
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */,
|
||||||
30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */,
|
30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */,
|
||||||
9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */,
|
9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */,
|
||||||
9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
||||||
9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */,
|
9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */,
|
||||||
30697C5321F63E0B0064FCAC /* PasscodeExtensionDisplay.swift in Sources */,
|
30697C5321F63E0B0064FCAC /* PasscodeExtensionDisplay.swift in Sources */,
|
||||||
9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -1657,12 +1675,12 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
||||||
9A58665825AADC49006719C2 /* PasswordDecryptor.swift in Sources */,
|
|
||||||
9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */,
|
9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */,
|
||||||
9A58662225AAAA3A006719C2 /* PasswordsViewController.swift in Sources */,
|
9A58662225AAAA3A006719C2 /* PasswordsViewController.swift in Sources */,
|
||||||
9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */,
|
9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */,
|
||||||
30697C5021F63D7F0064FCAC /* ExtensionConstants.swift in Sources */,
|
30697C5021F63D7F0064FCAC /* ExtensionConstants.swift in Sources */,
|
||||||
9A58661425AAA4C1006719C2 /* PasscodeExtensionDisplay.swift in Sources */,
|
9A58661425AAA4C1006719C2 /* PasscodeExtensionDisplay.swift in Sources */,
|
||||||
|
9AFC882725B53BF4008D6060 /* PasswordDecryptor.swift in Sources */,
|
||||||
30697C4B21F63D460064FCAC /* ExtensionViewController.swift in Sources */,
|
30697C4B21F63D460064FCAC /* ExtensionViewController.swift in Sources */,
|
||||||
9A58661B25AAA946006719C2 /* PasswordsTableDataSource.swift in Sources */,
|
9A58661B25AAA946006719C2 /* PasswordsTableDataSource.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
@ -1687,10 +1705,13 @@
|
||||||
306D970E24091CDD006C0E2E /* SwitchTableViewCell.swift in Sources */,
|
306D970E24091CDD006C0E2E /* SwitchTableViewCell.swift in Sources */,
|
||||||
A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */,
|
A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */,
|
||||||
DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */,
|
DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */,
|
||||||
|
9AFC87F025B514AD008D6060 /* PasswordDecryptor.swift in Sources */,
|
||||||
3066AD6823EE0D6500F65535 /* PGPKeyImporter.swift in Sources */,
|
3066AD6823EE0D6500F65535 /* PGPKeyImporter.swift in Sources */,
|
||||||
|
9AFC87E225B3B5C6008D6060 /* PasswordNavigationDataSource.swift in Sources */,
|
||||||
30650E7123F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift in Sources */,
|
30650E7123F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift in Sources */,
|
||||||
DC193FFA1E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift in Sources */,
|
DC193FFA1E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift in Sources */,
|
||||||
DCFB77AB1E503729008DE471 /* ContentProvider.swift in Sources */,
|
DCFB77AB1E503729008DE471 /* ContentProvider.swift in Sources */,
|
||||||
|
9AFC880025B51EC3008D6060 /* PasswordEncryptor.swift in Sources */,
|
||||||
DCA0499C1E3362F400522E8F /* PGPKeyUrlImportTableViewController.swift in Sources */,
|
DCA0499C1E3362F400522E8F /* PGPKeyUrlImportTableViewController.swift in Sources */,
|
||||||
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */,
|
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */,
|
||||||
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */,
|
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */,
|
||||||
|
|
@ -1704,9 +1725,10 @@
|
||||||
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */,
|
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */,
|
||||||
30C25DD821F4834D00BB27BB /* UICodeHighlightingLabel.swift in Sources */,
|
30C25DD821F4834D00BB27BB /* UICodeHighlightingLabel.swift in Sources */,
|
||||||
9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */,
|
||||||
|
9AFC87F825B51742008D6060 /* PasswordManager.swift in Sources */,
|
||||||
DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */,
|
DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */,
|
||||||
30C25DD721F4834D00BB27BB /* UILocalizedLabel.swift in Sources */,
|
30C25DD721F4834D00BB27BB /* UILocalizedLabel.swift in Sources */,
|
||||||
DC5734AE1E439AD400D09270 /* PasswordsViewController.swift in Sources */,
|
9AFC87D325B39FF3008D6060 /* PasswordNavigationViewController.swift in Sources */,
|
||||||
300713C52219D54100F553AC /* AutoCellHeightUITableViewController.swift in Sources */,
|
300713C52219D54100F553AC /* AutoCellHeightUITableViewController.swift in Sources */,
|
||||||
302269B323E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift in Sources */,
|
302269B323E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift in Sources */,
|
||||||
DCD3C65E1EFB9BB400CBE842 /* SettingsSplitViewController.swift in Sources */,
|
DCD3C65E1EFB9BB400CBE842 /* SettingsSplitViewController.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<!--Password Store-->
|
<!--Password Store-->
|
||||||
<scene sceneID="Qd0-Fd-Lct">
|
<scene sceneID="Qd0-Fd-Lct">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController id="cwm-DY-Eli" customClass="PasswordsViewController" customModule="Pass" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="passwordNavigation" id="cwm-DY-Eli" customClass="PasswordNavigationViewController" customModule="Pass" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<layoutGuides>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="zEx-Wa-Uh0"/>
|
<viewControllerLayoutGuide type="top" id="zEx-Wa-Uh0"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="G6Y-6n-xyC"/>
|
<viewControllerLayoutGuide type="bottom" id="G6Y-6n-xyC"/>
|
||||||
|
|
@ -56,11 +56,10 @@
|
||||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||||
</view>
|
</view>
|
||||||
<navigationItem key="navigationItem" title="Password Store" id="Cio-ZG-zCS">
|
<navigationItem key="navigationItem" title="Password Store" id="Cio-ZG-zCS">
|
||||||
<barButtonItem key="leftBarButtonItem" title="Back" id="New-sD-9Z1"/>
|
|
||||||
<barButtonItem key="rightBarButtonItem" systemItem="add" id="qVW-c1-xgh"/>
|
<barButtonItem key="rightBarButtonItem" systemItem="add" id="qVW-c1-xgh"/>
|
||||||
</navigationItem>
|
</navigationItem>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="tableView" destination="Tn1-q5-vaJ" id="UHc-AS-gXh"/>
|
<outlet property="tableView" destination="Tn1-q5-vaJ" id="DBn-bd-fJm"/>
|
||||||
<segue destination="tW4-E9-CGv" kind="show" identifier="showPasswordDetail" id="gXF-zd-527"/>
|
<segue destination="tW4-E9-CGv" kind="show" identifier="showPasswordDetail" id="gXF-zd-527"/>
|
||||||
<segue destination="fyR-Cj-h6o" kind="show" identifier="addPasswordSegue" id="40c-jj-6wh"/>
|
<segue destination="fyR-Cj-h6o" kind="show" identifier="addPasswordSegue" id="40c-jj-6wh"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
|
@ -737,8 +736,8 @@
|
||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="5ZN-vm-3gw" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="5ZN-vm-3gw" sceneMemberID="viewController">
|
||||||
<tabBarItem key="tabBarItem" title="Passwords" image="Lock" selectedImage="Lock" id="gNg-YT-cGW"/>
|
<tabBarItem key="tabBarItem" title="Passwords" image="Lock" selectedImage="Lock" id="gNg-YT-cGW"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="DUk-ZD-z4m">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="DUk-ZD-z4m">
|
||||||
<rect key="frame" x="0.0" y="44" width="390" height="44"/>
|
<rect key="frame" x="0.0" y="44" width="390" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
|
|
@ -1964,7 +1963,7 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
|
||||||
<inferredMetricsTieBreakers>
|
<inferredMetricsTieBreakers>
|
||||||
<segue reference="wCk-aN-a4J"/>
|
<segue reference="wCk-aN-a4J"/>
|
||||||
<segue reference="oxP-I1-Mke"/>
|
<segue reference="oxP-I1-Mke"/>
|
||||||
<segue reference="UfP-k3-XeR"/>
|
<segue reference="yyD-4H-pLE"/>
|
||||||
</inferredMetricsTieBreakers>
|
</inferredMetricsTieBreakers>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Lock" width="25" height="25"/>
|
<image name="Lock" width="25" height="25"/>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ class AddPasswordTableViewController: PasswordEditorTableViewController {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction
|
||||||
|
private func cancel(_ sender: Any) {
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
super.prepare(for: segue, sender: sender)
|
super.prepare(for: segue, sender: sender)
|
||||||
if segue.identifier == "saveAddPasswordSegue" {
|
if segue.identifier == "saveAddPasswordSegue" {
|
||||||
|
|
|
||||||
437
pass/Controllers/PasswordNavigationViewController.swift
Normal file
437
pass/Controllers/PasswordNavigationViewController.swift
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
import UIKit
|
||||||
|
import passKit
|
||||||
|
import SVProgressHUD
|
||||||
|
|
||||||
|
extension UIStoryboard {
|
||||||
|
static var passwordNavigationViewController: PasswordNavigationViewController {
|
||||||
|
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "passwordNavigation") as! PasswordNavigationViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordNavigationViewController: UIViewController {
|
||||||
|
@IBOutlet var tableView: UITableView!
|
||||||
|
|
||||||
|
var dataSource: PasswordNavigationDataSource?
|
||||||
|
var parentPasswordEntity: PasswordEntity?
|
||||||
|
|
||||||
|
var viewingUnsyncedPasswords = false
|
||||||
|
var tapTabBarTime: TimeInterval = 0
|
||||||
|
|
||||||
|
lazy var passwordManager = PasswordManager(viewController: self)
|
||||||
|
|
||||||
|
lazy var searchController: UISearchController = {
|
||||||
|
let uiSearchController = UISearchController(searchResultsController: nil)
|
||||||
|
uiSearchController.searchBar.isTranslucent = true
|
||||||
|
uiSearchController.obscuresBackgroundDuringPresentation = false
|
||||||
|
uiSearchController.searchBar.sizeToFit()
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
uiSearchController.searchBar.searchTextField.clearButtonMode = .whileEditing
|
||||||
|
}
|
||||||
|
return uiSearchController
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var searchBar: UISearchBar = {
|
||||||
|
self.searchController.searchBar
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var refreshControl: UIRefreshControl = {
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
refreshControl.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged)
|
||||||
|
return refreshControl
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var addPasswordUIBarButtonItem: UIBarButtonItem = {
|
||||||
|
var addPasswordUIBarButtonItem = UIBarButtonItem()
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let addPasswordButton = UIButton(type: .system)
|
||||||
|
let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(weight: .regular))
|
||||||
|
addPasswordButton.setImage(plusImage, for: .normal)
|
||||||
|
addPasswordButton.addTarget(self, action: #selector(self.addPasswordAction(_:)), for: .touchDown)
|
||||||
|
addPasswordUIBarButtonItem.customView = addPasswordButton
|
||||||
|
} else {
|
||||||
|
addPasswordUIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addPasswordAction(_:)))
|
||||||
|
}
|
||||||
|
return addPasswordUIBarButtonItem
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var gestureRecognizer: UILongPressGestureRecognizer = {
|
||||||
|
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction))
|
||||||
|
recognizer.minimumPressDuration = 0.6
|
||||||
|
return recognizer
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
searchBar.delegate = self
|
||||||
|
configureTableView(in: parentPasswordEntity)
|
||||||
|
configureNotification()
|
||||||
|
configureSearchBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
configureNavigationItem()
|
||||||
|
configureTabBarItem()
|
||||||
|
configureNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSearchBar() {
|
||||||
|
if Defaults.isShowFolderOn, !isRootViewController() {
|
||||||
|
searchBar.scopeButtonTitles = SearchBarScope.allCases.map(\.localizedName)
|
||||||
|
} else {
|
||||||
|
searchBar.scopeButtonTitles = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureTableView(in dir: PasswordEntity?) {
|
||||||
|
let passwordTableEntries = fetchPasswordTableEntries(in: dir)
|
||||||
|
dataSource = PasswordNavigationDataSource(entries: passwordTableEntries)
|
||||||
|
tableView.addGestureRecognizer(gestureRecognizer)
|
||||||
|
tableView.dataSource = dataSource
|
||||||
|
tableView.delegate = self
|
||||||
|
let atribbutedTitle = "LastSynced".localize() + ": \(PasswordStore.shared.lastSyncedTimeString)"
|
||||||
|
refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureTabBarItem() {
|
||||||
|
guard let tabBarItem = navigationController?.tabBarItem else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let numberOfLocalCommits = PasswordStore.shared.numberOfLocalCommits
|
||||||
|
if numberOfLocalCommits != 0 {
|
||||||
|
tabBarItem.badgeValue = "\(numberOfLocalCommits)"
|
||||||
|
} else {
|
||||||
|
tabBarItem.badgeValue = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchPasswordTableEntries(in dir: PasswordEntity? = nil) -> [PasswordTableEntry] {
|
||||||
|
if Defaults.isShowFolderOn {
|
||||||
|
return PasswordStore.shared.fetchPasswordEntityCoreData(parent: dir).compactMap { PasswordTableEntry($0) }
|
||||||
|
} else {
|
||||||
|
return PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false).compactMap { PasswordTableEntry($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureNavigationItem() {
|
||||||
|
if isRootViewController() {
|
||||||
|
navigationItem.largeTitleDisplayMode = .automatic
|
||||||
|
navigationItem.title = "PasswordStore".localize()
|
||||||
|
} else {
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
navigationItem.title = parentPasswordEntity?.getName()
|
||||||
|
}
|
||||||
|
if viewingUnsyncedPasswords {
|
||||||
|
navigationItem.title = "Unsynced"
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
|
navigationItem.rightBarButtonItem = addPasswordUIBarButtonItem
|
||||||
|
navigationItem.searchController = searchController
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureNavigationBar() {
|
||||||
|
guard let navigationBar = navigationController?.navigationBar else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard PasswordStore.shared.numberOfLocalCommits != 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let tapNavigationBarGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar))
|
||||||
|
tapNavigationBarGestureRecognizer.cancelsTouchesInView = false
|
||||||
|
navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isRootViewController () -> Bool {
|
||||||
|
navigationController?.viewControllers.count == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureNotification() {
|
||||||
|
let notificationCenter = NotificationCenter.default
|
||||||
|
// Reset the data table if some password (maybe another one) has been updated.
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordStoreUpdated, object: nil)
|
||||||
|
// Reset the data table if the disaply settings have been changed.
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordDisplaySettingChanged, object: nil)
|
||||||
|
// Search entrypoint for home screen quick action.
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil)
|
||||||
|
// A Siri shortcut can change the state of the app in the background. Hence, reload when opening the app.
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func addPasswordAction(_: Any?) {
|
||||||
|
if shouldPerformSegue(withIdentifier: "addPasswordSegue", sender: self) {
|
||||||
|
performSegue(withIdentifier: "addPasswordSegue", sender: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func didTapNavigationBar(_ sender: UITapGestureRecognizer) {
|
||||||
|
let location = sender.location(in: navigationController?.navigationBar)
|
||||||
|
let hitView = navigationController?.navigationBar.hitTest(location, with: nil)
|
||||||
|
guard !(hitView is UIControl) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||||
|
let allAction = UIAlertAction(title: "All Passwords", style: .default) { _ in
|
||||||
|
self.configureTableView(in: self.parentPasswordEntity)
|
||||||
|
self.tableView.reloadData()
|
||||||
|
self.viewingUnsyncedPasswords = false
|
||||||
|
self.configureNavigationItem()
|
||||||
|
}
|
||||||
|
let unsyncedAction = UIAlertAction(title: "Unsynced Passwords", style: .default) { _ in
|
||||||
|
self.dataSource?.showUnsyncedTableEntries()
|
||||||
|
self.tableView.reloadData()
|
||||||
|
self.viewingUnsyncedPasswords = true
|
||||||
|
self.configureNavigationItem()
|
||||||
|
}
|
||||||
|
let cancelAction = UIAlertAction.cancel()
|
||||||
|
|
||||||
|
alertController.addAction(allAction)
|
||||||
|
alertController.addAction(unsyncedAction)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
|
||||||
|
present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func longPressAction(_ gesture: UILongPressGestureRecognizer) {
|
||||||
|
if gesture.state == UIGestureRecognizer.State.began {
|
||||||
|
let touchPoint = gesture.location(in: tableView)
|
||||||
|
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
|
||||||
|
guard let dataSource = dataSource else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let passwordTableEntry = dataSource.getPasswordTableEntry(at: indexPath)
|
||||||
|
passwordManager.providePasswordPasteboard(with: passwordTableEntry.passwordEntity.getPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func actOnSearchNotification() {
|
||||||
|
searchBar.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func handleRefreshControl() {
|
||||||
|
syncPasswords()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PasswordNavigationViewController: UITableViewDelegate {
|
||||||
|
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
guard let dataSource = dataSource else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let entry = dataSource.getPasswordTableEntry(at: indexPath)
|
||||||
|
if entry.isDir {
|
||||||
|
showDir(in: entry)
|
||||||
|
} else {
|
||||||
|
showPasswordDetail(at: entry)
|
||||||
|
}
|
||||||
|
searchController.isActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func showDir(in entry: PasswordTableEntry) {
|
||||||
|
let passwordNavigationViewController = UIStoryboard.passwordNavigationViewController
|
||||||
|
passwordNavigationViewController.parentPasswordEntity = entry.passwordEntity
|
||||||
|
navigationController?.pushViewController(passwordNavigationViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showPasswordDetail(at entry: PasswordTableEntry) {
|
||||||
|
let segueIdentifier = "showPasswordDetail"
|
||||||
|
let sender = entry.passwordEntity
|
||||||
|
if shouldPerformSegue(withIdentifier: segueIdentifier, sender: sender) {
|
||||||
|
performSegue(withIdentifier: segueIdentifier, sender: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PasswordNavigationViewController {
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
if segue.identifier == "showPasswordDetail" {
|
||||||
|
if let viewController = segue.destination as? PasswordDetailTableViewController {
|
||||||
|
viewController.passwordEntity = sender as? PasswordEntity
|
||||||
|
}
|
||||||
|
} else if segue.identifier == "addPasswordSegue" {
|
||||||
|
if let navController = segue.destination as? UINavigationController,
|
||||||
|
let viewController = navController.topViewController as? AddPasswordTableViewController,
|
||||||
|
let path = parentPasswordEntity?.getPath() {
|
||||||
|
viewController.defaultDirPrefix = "\(path)/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
|
||||||
|
if identifier == "showPasswordDetail" {
|
||||||
|
guard PGPAgent.shared.isPrepared else {
|
||||||
|
Utils.alert(title: "CannotShowPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if identifier == "addPasswordSegue" {
|
||||||
|
guard PGPAgent.shared.isPrepared && PasswordStore.shared.storeRepository != nil else {
|
||||||
|
Utils.alert(title: "CannotAddPassword".localize(), message: "MakeSurePgpAndGitProperlySet.".localize(), controller: self)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction
|
||||||
|
private func cancelAddPassword(segue _: UIStoryboardSegue) {}
|
||||||
|
|
||||||
|
@IBAction
|
||||||
|
private func saveAddPassword(segue: UIStoryboardSegue) {
|
||||||
|
if let controller = segue.source as? AddPasswordTableViewController {
|
||||||
|
passwordManager.addPassword(with: controller.password!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PasswordNavigationViewController {
|
||||||
|
@objc
|
||||||
|
func actOnReloadTableViewRelatedNotification() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.navigationController?.popToRootViewController(animated: true)
|
||||||
|
self.resetViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetViews() {
|
||||||
|
configureTableView(in: parentPasswordEntity)
|
||||||
|
tableView.reloadData()
|
||||||
|
configureNavigationItem()
|
||||||
|
configureTabBarItem()
|
||||||
|
configureNavigationBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PasswordNavigationViewController: UISearchBarDelegate {
|
||||||
|
func search(matching text: String) {
|
||||||
|
dataSource?.showTableEntries(matching: text)
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateSearch(_ selectedScope: Int?) {
|
||||||
|
if selectedScope == SearchBarScope.all.rawValue {
|
||||||
|
configureTableView(in: nil)
|
||||||
|
} else {
|
||||||
|
configureTableView(in: parentPasswordEntity)
|
||||||
|
}
|
||||||
|
dataSource?.isSearchActive = true
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||||
|
activateSearch(selectedScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||||
|
if Defaults.isShowFolderOn, Defaults.searchDefault == .all {
|
||||||
|
searchBar.selectedScopeButtonIndex = SearchBarScope.all.rawValue
|
||||||
|
} else {
|
||||||
|
searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue
|
||||||
|
}
|
||||||
|
activateSearch(searchBar.selectedScopeButtonIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBar(_: UISearchBar, textDidChange searchText: String) {
|
||||||
|
search(matching: searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||||
|
searchBar.resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBarCancelButtonClicked(_: UISearchBar) {
|
||||||
|
cancelSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||||
|
cancelSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelSearch() {
|
||||||
|
configureTableView(in: parentPasswordEntity)
|
||||||
|
dataSource?.isSearchActive = false
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PasswordNavigationViewController: PasswordAlertPresenter {
|
||||||
|
private func syncPasswords() {
|
||||||
|
guard PasswordStore.shared.repositoryExists() else {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) {
|
||||||
|
Utils.alert(title: "Error".localize(), message: "NoPasswordStore.".localize(), controller: self, completion: nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SVProgressHUD.setDefaultMaskType(.black)
|
||||||
|
SVProgressHUD.setDefaultStyle(.light)
|
||||||
|
SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize())
|
||||||
|
let keychain = AppKeychain.shared
|
||||||
|
var gitCredential: GitCredential {
|
||||||
|
GitCredential.from(
|
||||||
|
authenticationMethod: Defaults.gitAuthenticationMethod,
|
||||||
|
userName: Defaults.gitUsername,
|
||||||
|
keyStore: keychain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
|
do {
|
||||||
|
let pullOptions = gitCredential.getCredentialOptions(passwordProvider: self.present)
|
||||||
|
try PasswordStore.shared.pullRepository(options: pullOptions) { git_transfer_progress, _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects) / Float(git_transfer_progress.pointee.total_objects), status: "PullingFromRemoteRepository".localize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if PasswordStore.shared.numberOfLocalCommits > 0 {
|
||||||
|
let pushOptions = gitCredential.getCredentialOptions(passwordProvider: self.present)
|
||||||
|
try PasswordStore.shared.pushRepository(options: pushOptions) { current, total, _, _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
SVProgressHUD.showProgress(Float(current) / Float(total), status: "PushingToRemoteRepository".localize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
SVProgressHUD.showSuccess(withStatus: "Done".localize())
|
||||||
|
SVProgressHUD.dismiss(withDelay: 1)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
gitCredential.delete()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
SVProgressHUD.dismiss()
|
||||||
|
let error = error as NSError
|
||||||
|
var message = error.localizedDescription
|
||||||
|
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||||
|
message = message | "UnderlyingError".localize(underlyingError.localizedDescription)
|
||||||
|
if underlyingError.localizedDescription.contains("WrongPassphrase".localize()) {
|
||||||
|
message = message | "RecoverySuggestion.".localize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) {
|
||||||
|
Utils.alert(title: "Error".localize(), message: message, controller: self, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,709 +0,0 @@
|
||||||
//
|
|
||||||
// PasswordsViewController.swift
|
|
||||||
// pass
|
|
||||||
//
|
|
||||||
// Created by Mingshen Sun on 3/2/2017.
|
|
||||||
// Copyright © 2017 Bob Sun. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import passKit
|
|
||||||
import SVProgressHUD
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate, UISearchBarDelegate, PasswordAlertPresenter {
|
|
||||||
// Arbitrary threshold to decide whether to show folders or not for only a few entries.
|
|
||||||
private static let hideSectionHeaderThreshold = 6
|
|
||||||
|
|
||||||
private var passwordsTableEntries: [PasswordTableEntry] = []
|
|
||||||
private var passwordsTableAllEntries: [PasswordTableEntry] = []
|
|
||||||
private var parentPasswordEntity: PasswordEntity?
|
|
||||||
private let passwordStore = PasswordStore.shared
|
|
||||||
private let keychain = AppKeychain.shared
|
|
||||||
private var gitCredential: GitCredential {
|
|
||||||
GitCredential.from(
|
|
||||||
authenticationMethod: Defaults.gitAuthenticationMethod,
|
|
||||||
userName: Defaults.gitUsername,
|
|
||||||
keyStore: keychain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var tapTabBarTime: TimeInterval = 0
|
|
||||||
private var tapNavigationBarGestureRecognizer: UITapGestureRecognizer!
|
|
||||||
|
|
||||||
private var sections = [(title: String, entries: [PasswordTableEntry])]()
|
|
||||||
|
|
||||||
private enum PasswordLabel {
|
|
||||||
case all
|
|
||||||
case unsynced
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var searchController: UISearchController = {
|
|
||||||
let uiSearchController = UISearchController(searchResultsController: nil)
|
|
||||||
uiSearchController.searchResultsUpdater = self
|
|
||||||
uiSearchController.dimsBackgroundDuringPresentation = false
|
|
||||||
uiSearchController.searchBar.isTranslucent = false
|
|
||||||
uiSearchController.searchBar.sizeToFit()
|
|
||||||
return uiSearchController
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var syncControl: UIRefreshControl = {
|
|
||||||
let syncControl = UIRefreshControl()
|
|
||||||
syncControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControl.Event.valueChanged)
|
|
||||||
return syncControl
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var searchBarView: UIView? = {
|
|
||||||
guard #available(iOS 11, *) else {
|
|
||||||
let uiView = UIView(frame: CGRect(x: 0, y: 64, width: self.view.bounds.width, height: 44))
|
|
||||||
uiView.addSubview(self.searchController.searchBar)
|
|
||||||
return uiView
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var backUIBarButtonItem: UIBarButtonItem = {
|
|
||||||
let backUIButton = UIButton(type: .system)
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
let leftImage = UIImage(systemName: "chevron.left", withConfiguration: UIImage.SymbolConfiguration(weight: .bold))
|
|
||||||
backUIButton.setImage(leftImage, for: .normal)
|
|
||||||
backUIButton.setTitle("Back".localize(), for: .normal)
|
|
||||||
let padding = CGFloat(integerLiteral: 3)
|
|
||||||
backUIButton.contentEdgeInsets.right += padding
|
|
||||||
backUIButton.titleEdgeInsets.left = padding
|
|
||||||
backUIButton.titleEdgeInsets.right = -padding
|
|
||||||
} else {
|
|
||||||
backUIButton.setTitle("Back".localize(), for: .normal)
|
|
||||||
}
|
|
||||||
backUIButton.addTarget(self, action: #selector(self.backAction(_:)), for: .touchDown)
|
|
||||||
let backUIBarButtonItem = UIBarButtonItem(customView: backUIButton)
|
|
||||||
return backUIBarButtonItem
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var addPasswordUIBarButtonItem: UIBarButtonItem = {
|
|
||||||
var addPasswordUIBarButtonItem = UIBarButtonItem()
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
let addPasswordButton = UIButton(type: .system)
|
|
||||||
let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(weight: .regular))
|
|
||||||
addPasswordButton.setImage(plusImage, for: .normal)
|
|
||||||
addPasswordButton.addTarget(self, action: #selector(self.addPasswordAction(_:)), for: .touchDown)
|
|
||||||
addPasswordUIBarButtonItem.customView = addPasswordButton
|
|
||||||
} else {
|
|
||||||
addPasswordUIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addPasswordAction(_:)))
|
|
||||||
}
|
|
||||||
return addPasswordUIBarButtonItem
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var transitionFromRight: CATransition = {
|
|
||||||
let transition = CATransition()
|
|
||||||
transition.type = CATransitionType.push
|
|
||||||
transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
||||||
transition.fillMode = CAMediaTimingFillMode.forwards
|
|
||||||
transition.duration = 0.25
|
|
||||||
transition.subtype = CATransitionSubtype.fromRight
|
|
||||||
transition.delegate = self
|
|
||||||
return transition
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var transitionFromLeft: CATransition = {
|
|
||||||
let transition = CATransition()
|
|
||||||
transition.type = CATransitionType.push
|
|
||||||
transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
||||||
transition.fillMode = CAMediaTimingFillMode.forwards
|
|
||||||
transition.duration = 0.25
|
|
||||||
transition.subtype = CATransitionSubtype.fromLeft
|
|
||||||
transition.delegate = self
|
|
||||||
return transition
|
|
||||||
}()
|
|
||||||
|
|
||||||
@IBOutlet var tableView: UITableView!
|
|
||||||
|
|
||||||
private func initPasswordsTableEntries(parent: PasswordEntity?) {
|
|
||||||
let passwordAllEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false)
|
|
||||||
passwordsTableAllEntries = passwordAllEntities.compactMap {
|
|
||||||
PasswordTableEntry($0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let passwordEntities = Defaults.isShowFolderOn ?
|
|
||||||
passwordStore.fetchPasswordEntityCoreData(parent: parent) :
|
|
||||||
passwordAllEntities
|
|
||||||
passwordsTableEntries = passwordEntities.compactMap {
|
|
||||||
PasswordTableEntry($0)
|
|
||||||
}
|
|
||||||
|
|
||||||
parentPasswordEntity = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction
|
|
||||||
private func cancelAddPassword(segue _: UIStoryboardSegue) {}
|
|
||||||
|
|
||||||
@IBAction
|
|
||||||
private func saveAddPassword(segue: UIStoryboardSegue) {
|
|
||||||
if let controller = segue.source as? AddPasswordTableViewController {
|
|
||||||
addPassword(password: controller.password!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addPassword(password: Password, keyID: String? = nil) {
|
|
||||||
SVProgressHUD.setDefaultMaskType(.black)
|
|
||||||
SVProgressHUD.setDefaultStyle(.light)
|
|
||||||
SVProgressHUD.show(withStatus: "Saving".localize())
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
do {
|
|
||||||
_ = try self.passwordStore.add(password: password, keyID: keyID)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// will trigger reloadTableView() by a notification
|
|
||||||
SVProgressHUD.showSuccess(withStatus: "Done".localize())
|
|
||||||
SVProgressHUD.dismiss(withDelay: 1)
|
|
||||||
}
|
|
||||||
} catch let AppError.pgpPublicKeyNotFound(keyID: key) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// alert: cancel or select keys
|
|
||||||
SVProgressHUD.dismiss()
|
|
||||||
let alert = UIAlertController(title: "Cannot Encrypt Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
|
||||||
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
|
|
||||||
let selectKey = UIAlertAction.selectKey(controller: self) { action in
|
|
||||||
self.addPassword(password: password, keyID: action.title)
|
|
||||||
}
|
|
||||||
alert.addAction(selectKey)
|
|
||||||
|
|
||||||
self.present(alert, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} catch {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func syncPasswords() {
|
|
||||||
guard passwordStore.repositoryExists() else {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) {
|
|
||||||
Utils.alert(title: "Error".localize(), message: "NoPasswordStore.".localize(), controller: self, completion: nil)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SVProgressHUD.setDefaultMaskType(.black)
|
|
||||||
SVProgressHUD.setDefaultStyle(.light)
|
|
||||||
SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize())
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
|
||||||
do {
|
|
||||||
let pullOptions = self.gitCredential.getCredentialOptions(passwordProvider: self.present)
|
|
||||||
try self.passwordStore.pullRepository(options: pullOptions) { git_transfer_progress, _ in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects) / Float(git_transfer_progress.pointee.total_objects), status: "PullingFromRemoteRepository".localize())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.passwordStore.numberOfLocalCommits > 0 {
|
|
||||||
let pushOptions = self.gitCredential.getCredentialOptions(passwordProvider: self.present)
|
|
||||||
try self.passwordStore.pushRepository(options: pushOptions) { current, total, _, _ in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
SVProgressHUD.showProgress(Float(current) / Float(total), status: "PushingToRemoteRepository".localize())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.reloadTableView(parent: nil)
|
|
||||||
SVProgressHUD.showSuccess(withStatus: "Done".localize())
|
|
||||||
SVProgressHUD.dismiss(withDelay: 1)
|
|
||||||
self.syncControl.endRefreshing()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
self.gitCredential.delete()
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
SVProgressHUD.dismiss()
|
|
||||||
self.syncControl.endRefreshing()
|
|
||||||
let error = error as NSError
|
|
||||||
var message = error.localizedDescription
|
|
||||||
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
||||||
message = message | "UnderlyingError".localize(underlyingError.localizedDescription)
|
|
||||||
if underlyingError.localizedDescription.contains("WrongPassphrase".localize()) {
|
|
||||||
message = message | "RecoverySuggestion.".localize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) {
|
|
||||||
Utils.alert(title: "Error".localize(), message: message, controller: self, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
if Defaults.isShowFolderOn {
|
|
||||||
searchController.searchBar.scopeButtonTitles = SearchBarScope.allCases.map(\.localizedName)
|
|
||||||
} else {
|
|
||||||
searchController.searchBar.scopeButtonTitles = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
searchController.searchBar.delegate = self
|
|
||||||
tableView.delegate = self
|
|
||||||
tableView.dataSource = self
|
|
||||||
definesPresentationContext = true
|
|
||||||
if #available(iOS 11.0, *) {
|
|
||||||
navigationItem.searchController = searchController
|
|
||||||
navigationController?.navigationBar.prefersLargeTitles = true
|
|
||||||
navigationItem.largeTitleDisplayMode = .automatic
|
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
tableView.contentInset = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0)
|
|
||||||
view.addSubview(searchBarView!)
|
|
||||||
}
|
|
||||||
navigationItem.title = "PasswordStore".localize()
|
|
||||||
tapNavigationBarGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar))
|
|
||||||
|
|
||||||
SVProgressHUD.setDefaultMaskType(.black)
|
|
||||||
tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell")
|
|
||||||
|
|
||||||
// initialize the password table
|
|
||||||
reloadTableView(parent: nil)
|
|
||||||
|
|
||||||
// reset the data table if some password (maybe another one) has been updated
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordStoreUpdated, object: nil)
|
|
||||||
// reset the data table if the disaply settings have been changed
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordDisplaySettingChanged, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil)
|
|
||||||
// A Siri shortcut can change the state of the app in the background. Hence, reload when opening the app.
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
||||||
|
|
||||||
// listen to the swipe back guesture
|
|
||||||
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture))
|
|
||||||
swipeRight.direction = UISwipeGestureRecognizer.Direction.right
|
|
||||||
view.addGestureRecognizer(swipeRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func didTapNavigationBar(_ sender: UITapGestureRecognizer) {
|
|
||||||
let location = sender.location(in: navigationController?.navigationBar)
|
|
||||||
let hitView = navigationController?.navigationBar.hitTest(location, with: nil)
|
|
||||||
guard !(hitView is UIControl) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard passwordStore.numberOfLocalCommits != 0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
|
||||||
let allAction = UIAlertAction(title: "All Passwords", style: .default) { _ in
|
|
||||||
self.reloadTableView(parent: nil, label: .all)
|
|
||||||
}
|
|
||||||
let unsyncedAction = UIAlertAction(title: "Unsynced Passwords", style: .default) { _ in
|
|
||||||
let filteredPasswordsTableEntries = self.passwordsTableEntries.filter { entry in
|
|
||||||
!entry.synced
|
|
||||||
}
|
|
||||||
self.reloadTableView(data: filteredPasswordsTableEntries, label: .unsynced)
|
|
||||||
}
|
|
||||||
let cancelAction = UIAlertAction.cancel()
|
|
||||||
|
|
||||||
alertController.addAction(allAction)
|
|
||||||
alertController.addAction(unsyncedAction)
|
|
||||||
alertController.addAction(cancelAction)
|
|
||||||
|
|
||||||
present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
tabBarController!.delegate = self
|
|
||||||
if let path = tableView.indexPathForSelectedRow {
|
|
||||||
tableView.deselectRow(at: path, animated: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add gesture recognizer to the navigation bar when the view is about to appear
|
|
||||||
navigationController?.navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer)
|
|
||||||
|
|
||||||
// This allows controlls in the navigation bar to continue receiving touches
|
|
||||||
tapNavigationBarGestureRecognizer.cancelsTouchesInView = false
|
|
||||||
|
|
||||||
tableView.refreshControl = passwordStore.repositoryExists() ? syncControl : nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
// Remove gesture recognizer from navigation bar when view is about to disappear
|
|
||||||
navigationController?.navigationBar.removeGestureRecognizer(tapNavigationBarGestureRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
|
||||||
super.viewWillLayoutSubviews()
|
|
||||||
guard #available(iOS 11, *) else {
|
|
||||||
searchBarView?.frame = CGRect(x: 0, y: navigationController!.navigationBar.bounds.size.height + UIApplication.shared.statusBarFrame.height, width: UIScreen.main.bounds.width, height: 44)
|
|
||||||
searchController.searchBar.sizeToFit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func numberOfSections(in _: UITableView) -> Int {
|
|
||||||
sections.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
sections[section].entries.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let longPressGestureRecognizer: UILongPressGestureRecognizer = {
|
|
||||||
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction))
|
|
||||||
recognizer.minimumPressDuration = 0.6
|
|
||||||
return recognizer
|
|
||||||
}()
|
|
||||||
let entry = getPasswordEntry(by: indexPath)
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell
|
|
||||||
cell.configure(with: entry)
|
|
||||||
if !entry.isDir {
|
|
||||||
cell.addGestureRecognizer(longPressGestureRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
UITableView.automaticDimension
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
UITableView.automaticDimension
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry {
|
|
||||||
sections[indexPath.section].entries[indexPath.row]
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
let entry = getPasswordEntry(by: indexPath)
|
|
||||||
if !entry.isDir {
|
|
||||||
let segueIdentifier = "showPasswordDetail"
|
|
||||||
let sender = tableView.cellForRow(at: indexPath)
|
|
||||||
if shouldPerformSegue(withIdentifier: segueIdentifier, sender: sender) {
|
|
||||||
performSegue(withIdentifier: segueIdentifier, sender: sender)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
|
||||||
searchController.isActive = false
|
|
||||||
reloadTableView(parent: entry.passwordEntity, anim: transitionFromRight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func respondToSwipeGesture(gesture: UIGestureRecognizer) {
|
|
||||||
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
|
|
||||||
// swipe right -> swipe back
|
|
||||||
if swipeGesture.direction == .right, parentPasswordEntity != nil {
|
|
||||||
backAction(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func backAction(_: Any?) {
|
|
||||||
guard Defaults.isShowFolderOn else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var anim: CATransition? = transitionFromLeft
|
|
||||||
if parentPasswordEntity == nil {
|
|
||||||
anim = nil
|
|
||||||
}
|
|
||||||
reloadTableView(parent: parentPasswordEntity?.parent, anim: anim)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func addPasswordAction(_: Any?) {
|
|
||||||
if shouldPerformSegue(withIdentifier: "addPasswordSegue", sender: self) {
|
|
||||||
performSegue(withIdentifier: "addPasswordSegue", sender: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func longPressAction(_ gesture: UILongPressGestureRecognizer) {
|
|
||||||
if gesture.state == UIGestureRecognizer.State.began {
|
|
||||||
let touchPoint = gesture.location(in: tableView)
|
|
||||||
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
|
|
||||||
decryptThenCopyPassword(from: indexPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hideSectionHeader() -> Bool {
|
|
||||||
passwordsTableEntries.count < Self.hideSectionHeaderThreshold || searchController.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
||||||
if hideSectionHeader() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return sections[section].title
|
|
||||||
}
|
|
||||||
|
|
||||||
func sectionIndexTitles(for _: UITableView) -> [String]? {
|
|
||||||
if hideSectionHeader() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return sections.map(\.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_: UITableView, sectionForSectionIndexTitle _: String, at index: Int) -> Int {
|
|
||||||
index
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
|
|
||||||
decryptThenCopyPassword(from: indexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func decryptThenCopyPassword(from indexPath: IndexPath) {
|
|
||||||
guard PGPAgent.shared.isPrepared else {
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity
|
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
|
||||||
SVProgressHUD.dismiss()
|
|
||||||
decryptPassword(passwordEntity: passwordEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) {
|
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
|
||||||
do {
|
|
||||||
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
|
|
||||||
let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
SecurePasteboard.shared.copy(textToCopy: decryptedPassword.password)
|
|
||||||
SVProgressHUD.setDefaultMaskType(.black)
|
|
||||||
SVProgressHUD.setDefaultStyle(.dark)
|
|
||||||
SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize())
|
|
||||||
SVProgressHUD.dismiss(withDelay: 0.6)
|
|
||||||
}
|
|
||||||
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// alert: cancel or try again
|
|
||||||
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
|
||||||
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
|
|
||||||
let selectKey = UIAlertAction.selectKey(controller: self) { action in
|
|
||||||
self.decryptPassword(passwordEntity: passwordEntity, keyID: action.title)
|
|
||||||
}
|
|
||||||
alert.addAction(selectKey)
|
|
||||||
|
|
||||||
self.present(alert, animated: true)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateSections(item: [PasswordTableEntry]) {
|
|
||||||
let collation = UILocalizedIndexedCollation.current()
|
|
||||||
let sectionTitles = collation.sectionIndexTitles
|
|
||||||
var newSections = [(title: String, entries: [PasswordTableEntry])]()
|
|
||||||
|
|
||||||
// initialize all sections
|
|
||||||
for titleNumber in 0 ..< sectionTitles.count {
|
|
||||||
newSections.append((title: sectionTitles[titleNumber], entries: [PasswordTableEntry]()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// put entries into sections
|
|
||||||
for entry in item {
|
|
||||||
let sectionNumber = collation.section(for: entry, collationStringSelector: #selector(getter: PasswordTableEntry.title))
|
|
||||||
newSections[sectionNumber].entries.append(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort each list and set sectionTitles
|
|
||||||
for titleNumber in 0 ..< sectionTitles.count {
|
|
||||||
let entriesToSort = newSections[titleNumber].entries
|
|
||||||
let sortedEntries = collation.sortedArray(from: entriesToSort, collationStringSelector: #selector(getter: PasswordTableEntry.title))
|
|
||||||
newSections[titleNumber].entries = sortedEntries as! [PasswordTableEntry]
|
|
||||||
}
|
|
||||||
|
|
||||||
// only keep non-empty sections
|
|
||||||
sections = newSections.filter { !$0.entries.isEmpty }
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func actOnSearchNotification() {
|
|
||||||
searchController.searchBar.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
|
|
||||||
if identifier == "showPasswordDetail" {
|
|
||||||
guard PGPAgent.shared.isPrepared else {
|
|
||||||
Utils.alert(title: "CannotShowPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil)
|
|
||||||
if let sender = sender as? UITableViewCell {
|
|
||||||
let selectedIndexPath = tableView.indexPath(for: sender)!
|
|
||||||
tableView.deselectRow(at: selectedIndexPath, animated: true)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if identifier == "addPasswordSegue" {
|
|
||||||
guard PGPAgent.shared.isPrepared && passwordStore.storeRepository != nil else {
|
|
||||||
Utils.alert(title: "CannotAddPassword".localize(), message: "MakeSurePgpAndGitProperlySet.".localize(), controller: self, completion: nil)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
|
||||||
if segue.identifier == "showPasswordDetail" {
|
|
||||||
if let viewController = segue.destination as? PasswordDetailTableViewController {
|
|
||||||
let selectedIndexPath = tableView.indexPath(for: sender as! UITableViewCell)!
|
|
||||||
let passwordEntity = getPasswordEntry(by: selectedIndexPath).passwordEntity
|
|
||||||
viewController.passwordEntity = passwordEntity
|
|
||||||
}
|
|
||||||
} else if segue.identifier == "addPasswordSegue" {
|
|
||||||
if let navController = segue.destination as? UINavigationController {
|
|
||||||
if let viewController = navController.topViewController as? AddPasswordTableViewController {
|
|
||||||
if let path = parentPasswordEntity?.getPath() {
|
|
||||||
viewController.defaultDirPrefix = "\(path)/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterContentForSearchText(searchText: String, scope: SearchBarScope = .all) {
|
|
||||||
var entries: [PasswordTableEntry] = scope == .all ? passwordsTableAllEntries : passwordsTableEntries
|
|
||||||
if searchController.isActive, let searchBarText = searchController.searchBar.text, !searchBarText.isEmpty {
|
|
||||||
entries = entries.filter { $0.match(searchText) }
|
|
||||||
}
|
|
||||||
reloadTableView(data: entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reloadTableView(data: [PasswordTableEntry], label: PasswordLabel = .all, anim: CAAnimation? = nil) {
|
|
||||||
// set navigation item
|
|
||||||
if passwordStore.numberOfLocalCommits != 0 {
|
|
||||||
navigationController?.tabBarItem.badgeValue = "\(passwordStore.numberOfLocalCommits)"
|
|
||||||
} else {
|
|
||||||
navigationController?.tabBarItem.badgeValue = nil
|
|
||||||
}
|
|
||||||
if parentPasswordEntity != nil {
|
|
||||||
navigationItem.leftBarButtonItem = backUIBarButtonItem
|
|
||||||
navigationItem.title = parentPasswordEntity?.getName()
|
|
||||||
if #available(iOS 11, *) {
|
|
||||||
navigationController?.navigationBar.prefersLargeTitles = false
|
|
||||||
}
|
|
||||||
navigationController?.navigationBar.removeGestureRecognizer(tapNavigationBarGestureRecognizer)
|
|
||||||
} else {
|
|
||||||
navigationItem.leftBarButtonItem = nil
|
|
||||||
switch label {
|
|
||||||
case .all:
|
|
||||||
navigationItem.title = "PasswordStore".localize()
|
|
||||||
case .unsynced:
|
|
||||||
navigationItem.title = "Unsynced"
|
|
||||||
}
|
|
||||||
if #available(iOS 11, *) {
|
|
||||||
navigationController?.navigationBar.prefersLargeTitles = true
|
|
||||||
}
|
|
||||||
navigationController?.navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer)
|
|
||||||
}
|
|
||||||
navigationItem.rightBarButtonItem = addPasswordUIBarButtonItem
|
|
||||||
|
|
||||||
// set the password table
|
|
||||||
generateSections(item: data)
|
|
||||||
if anim != nil {
|
|
||||||
tableView.layer.add(anim!, forKey: "UITableViewReloadDataAnimationKey")
|
|
||||||
}
|
|
||||||
tableView.reloadData()
|
|
||||||
tableView.layer.removeAnimation(forKey: "UITableViewReloadDataAnimationKey")
|
|
||||||
|
|
||||||
// set the sync control title
|
|
||||||
let atribbutedTitle = "LastSynced".localize() + ": \(lastSyncedTimeString())"
|
|
||||||
syncControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func lastSyncedTimeString() -> String {
|
|
||||||
guard let date = passwordStore.lastSyncedTime else {
|
|
||||||
return "SyncAgain?".localize()
|
|
||||||
}
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
formatter.timeStyle = .short
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reloadTableView(parent: PasswordEntity?, label: PasswordLabel = .all, anim: CAAnimation? = nil) {
|
|
||||||
initPasswordsTableEntries(parent: parent)
|
|
||||||
reloadTableView(data: passwordsTableEntries, label: label, anim: anim)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func actOnReloadTableViewRelatedNotification() {
|
|
||||||
DispatchQueue.main.async { [weak weakSelf = self] in
|
|
||||||
guard let strongSelf = weakSelf else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Reset selectedScopeButtonIndex to make sure the correct reloadTableView
|
|
||||||
strongSelf.searchController.searchBar.selectedScopeButtonIndex = 0
|
|
||||||
strongSelf.initPasswordsTableEntries(parent: nil)
|
|
||||||
strongSelf.reloadTableView(data: strongSelf.passwordsTableEntries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func handleRefresh(_: UIRefreshControl) {
|
|
||||||
syncPasswords()
|
|
||||||
}
|
|
||||||
|
|
||||||
func tabBarController(_: UITabBarController, didSelect viewController: UIViewController) {
|
|
||||||
if viewController == navigationController {
|
|
||||||
let currentTime = Date().timeIntervalSince1970
|
|
||||||
let duration = currentTime - tapTabBarTime
|
|
||||||
tapTabBarTime = currentTime
|
|
||||||
if duration < 0.35 {
|
|
||||||
let topIndexPath = IndexPath(row: 0, section: 0)
|
|
||||||
if tableView.numberOfSections > 0 {
|
|
||||||
tableView.scrollToRow(at: topIndexPath, at: .bottom, animated: true)
|
|
||||||
}
|
|
||||||
tapTabBarTime = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
backAction(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBar(_: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
|
||||||
// update the default search scope
|
|
||||||
Defaults.searchDefault = SearchBarScope(rawValue: selectedScope)
|
|
||||||
updateSearchResults(for: searchController)
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBarShouldBeginEditing(_: UISearchBar) -> Bool {
|
|
||||||
// set the default search scope to "all"
|
|
||||||
if Defaults.isShowFolderOn, Defaults.searchDefault == .all {
|
|
||||||
searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.all.rawValue
|
|
||||||
} else {
|
|
||||||
searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBarShouldEndEditing(_: UISearchBar) -> Bool {
|
|
||||||
// set the default search scope to "current"
|
|
||||||
searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue
|
|
||||||
updateSearchResults(for: searchController)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PasswordsViewController: UISearchResultsUpdating {
|
|
||||||
func updateSearchResults(for searchController: UISearchController) {
|
|
||||||
let scope = SearchBarScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .all
|
|
||||||
filterContentForSearchText(searchText: searchController.searchBar.text!, scope: scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PasswordsViewController: CAAnimationDelegate {
|
|
||||||
func animationDidStart(_: CAAnimation) {
|
|
||||||
view.window?.backgroundColor = Colors.systemBackground
|
|
||||||
view.layer.backgroundColor = Colors.systemBackground.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
//
|
//
|
||||||
// PasswordDecryptor.swift
|
// PasswordDecryptor.swift
|
||||||
// passAutoFillExtension
|
// pass
|
||||||
//
|
//
|
||||||
// Created by Sun, Mingshen on 12/31/20.
|
// Created by Sun, Mingshen on 1/17/21.
|
||||||
// Copyright © 2020 Bob Sun. All rights reserved.
|
// Copyright © 2021 Bob Sun. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import passKit
|
import passKit
|
||||||
|
import SVProgressHUD
|
||||||
|
import UIKit
|
||||||
|
|
||||||
func decryptPassword(in controller: UIViewController, with passwordPath: String, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) {
|
func decryptPassword(in controller: UIViewController, with passwordPath: String, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) {
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
DispatchQueue.global(qos: .userInteractive).async {
|
||||||
28
pass/Services/PasswordEncryptor.swift
Normal file
28
pass/Services/PasswordEncryptor.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import passKit
|
||||||
|
|
||||||
|
func encryptPassword(in controller: UIViewController, with password: Password, keyID: String? = nil, completion: @escaping (() -> Void)) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
do {
|
||||||
|
_ = try PasswordStore.shared.add(password: password, keyID: keyID)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
} catch let AppError.pgpPublicKeyNotFound(keyID: key) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = UIAlertController(title: "Cannot Encrypt Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction.cancelAndPopView(controller: controller))
|
||||||
|
let selectKey = UIAlertAction.selectKey(controller: controller) { action in
|
||||||
|
encryptPassword(in: controller, with: password, keyID: action.title, completion: completion)
|
||||||
|
}
|
||||||
|
alert.addAction(selectKey)
|
||||||
|
|
||||||
|
controller.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: controller, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
pass/Services/PasswordManager.swift
Normal file
37
pass/Services/PasswordManager.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import UIKit
|
||||||
|
import passKit
|
||||||
|
import SVProgressHUD
|
||||||
|
|
||||||
|
class PasswordManager {
|
||||||
|
weak var viewController: UIViewController?
|
||||||
|
|
||||||
|
init(viewController: UIViewController) {
|
||||||
|
self.viewController = viewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func providePasswordPasteboard(with passwordPath: String) {
|
||||||
|
guard let viewController = viewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decryptPassword(in: viewController, with: passwordPath) { password in
|
||||||
|
SecurePasteboard.shared.copy(textToCopy: password.password)
|
||||||
|
SVProgressHUD.setDefaultMaskType(.black)
|
||||||
|
SVProgressHUD.setDefaultStyle(.dark)
|
||||||
|
SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize())
|
||||||
|
SVProgressHUD.dismiss(withDelay: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPassword(with password: Password) {
|
||||||
|
guard let viewController = viewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptPassword(in: viewController, with: password) {
|
||||||
|
SVProgressHUD.setDefaultMaskType(.black)
|
||||||
|
SVProgressHUD.setDefaultStyle(.light)
|
||||||
|
SVProgressHUD.showSuccess(withStatus: "Done".localize())
|
||||||
|
SVProgressHUD.dismiss(withDelay: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
pass/Services/PasswordNavigationDataSource.swift
Normal file
117
pass/Services/PasswordNavigationDataSource.swift
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
//
|
||||||
|
// PasswordNavigationDataSource.swift
|
||||||
|
// pass
|
||||||
|
//
|
||||||
|
// Created by Sun, Mingshen on 1/16/21.
|
||||||
|
// Copyright © 2021 Bob Sun. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import passKit
|
||||||
|
|
||||||
|
struct Section {
|
||||||
|
var title: String
|
||||||
|
var entries: [PasswordTableEntry]
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordNavigationDataSource: NSObject, UITableViewDataSource {
|
||||||
|
var sections: [Section]
|
||||||
|
var filteredSections: [Section]
|
||||||
|
|
||||||
|
var isSearchActive = false
|
||||||
|
var cellGestureRecognizer: UIGestureRecognizer?
|
||||||
|
|
||||||
|
let hideSectionHeaderThreshold = 6
|
||||||
|
|
||||||
|
init(entries: [PasswordTableEntry] = []) {
|
||||||
|
sections = buildSections(from: entries)
|
||||||
|
filteredSections = sections
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
filteredSections[section].entries.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
|
showSectionTitles() ? filteredSections[section].title : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell
|
||||||
|
let entry = getPasswordTableEntry(at: indexPath)
|
||||||
|
cell.configure(with: entry)
|
||||||
|
|
||||||
|
if let gestureRecognizer = cellGestureRecognizer, !entry.isDir {
|
||||||
|
cell.addGestureRecognizer(gestureRecognizer)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
filteredSections.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionIndexTitles(for _: UITableView) -> [String]? {
|
||||||
|
showSectionTitles() ? filteredSections.map(\.title) : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSectionTitles() -> Bool {
|
||||||
|
!isSearchActive && filteredSections.count > hideSectionHeaderThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPasswordTableEntry(at indexPath: IndexPath) -> PasswordTableEntry {
|
||||||
|
filteredSections[indexPath.section].entries[indexPath.row]
|
||||||
|
}
|
||||||
|
|
||||||
|
func showTableEntries(matching text: String) {
|
||||||
|
guard !text.isEmpty else {
|
||||||
|
filteredSections = sections
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredSections = sections.map { section in
|
||||||
|
let entries = section.entries.filter { $0.match(text) }
|
||||||
|
return Section(title: section.title, entries: entries)
|
||||||
|
}
|
||||||
|
.filter { !$0.entries.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
|
func showUnsyncedTableEntries() {
|
||||||
|
filteredSections = sections.map { section in
|
||||||
|
let entries = section.entries.filter { !$0.synced }
|
||||||
|
return Section(title: section.title, entries: entries)
|
||||||
|
}
|
||||||
|
.filter { !$0.entries.isEmpty }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildSections(from entries: [PasswordTableEntry]) -> [Section] {
|
||||||
|
let collation = UILocalizedIndexedCollation.current()
|
||||||
|
let sectionTitles = collation.sectionIndexTitles
|
||||||
|
var sections = [Section]()
|
||||||
|
|
||||||
|
// initialize all sections
|
||||||
|
for titleNumber in 0 ..< sectionTitles.count {
|
||||||
|
sections.append(Section(title: sectionTitles[titleNumber], entries: [PasswordTableEntry]()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// put entries into sections
|
||||||
|
for entry in entries {
|
||||||
|
let sectionNumber = collation.section(for: entry, collationStringSelector: #selector(getter: PasswordTableEntry.title))
|
||||||
|
sections[sectionNumber].entries.append(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort each list and set sectionTitles
|
||||||
|
for titleNumber in 0 ..< sectionTitles.count {
|
||||||
|
let entriesToSort = sections[titleNumber].entries
|
||||||
|
let sortedEntries = collation.sortedArray(from: entriesToSort, collationStringSelector: #selector(getter: PasswordTableEntry.title))
|
||||||
|
sections[titleNumber].entries = sortedEntries as! [PasswordTableEntry]
|
||||||
|
}
|
||||||
|
|
||||||
|
// only keep non-empty sections
|
||||||
|
return sections.filter { !$0.entries.isEmpty }
|
||||||
|
}
|
||||||
|
|
@ -97,6 +97,16 @@ public class PasswordStore {
|
||||||
Defaults.lastSyncedTime
|
Defaults.lastSyncedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var lastSyncedTimeString: String {
|
||||||
|
guard let date = lastSyncedTime else {
|
||||||
|
return "SyncAgain?".localize()
|
||||||
|
}
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
public var numberOfCommits: UInt? {
|
public var numberOfCommits: UInt? {
|
||||||
storeRepository?.numberOfCommits(inCurrentBranch: nil)
|
storeRepository?.numberOfCommits(inCurrentBranch: nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue