diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 72bf6029e..67ad40441 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -11,9 +11,16 @@ 4A03255E25A368BF00E63D7A /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */; }; 4A03257825A36A6900E63D7A /* VaultListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03257725A36A6900E63D7A /* VaultListViewController.swift */; }; 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */; }; + 4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */; }; 4A0C07E225AC80C100B83211 /* UIView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */; }; 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */; }; 4A123EA824BEF5F0001D1CF7 /* CloudProviderPaginationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A123EA724BEF5F0001D1CF7 /* CloudProviderPaginationMock.swift */; }; + 4A1EB0CA2689C373006D072B /* LocalVaultAdding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0C92689C373006D072B /* LocalVaultAdding.swift */; }; + 4A1EB0CC2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0CB2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift */; }; + 4A1EB0CE2689C7A3006D072B /* DetectedVaultFailureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0CD2689C7A3006D072B /* DetectedVaultFailureViewController.swift */; }; + 4A1EB0D02689C7F8006D072B /* DetectedVaultFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0CF2689C7F8006D072B /* DetectedVaultFailureView.swift */; }; + 4A1EB0D5268A5AA3006D072B /* VaultDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0D4268A5AA3006D072B /* VaultDetector.swift */; }; + 4A1EB0D8268A6DE1006D072B /* AddLocalVaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1EB0D7268A6DE1006D072B /* AddLocalVaultViewController.swift */; }; 4A2245DC24A5E1C600DBA437 /* MetadataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2245DB24A5E1C600DBA437 /* MetadataManagerTests.swift */; }; 4A248221266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A248220266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift */; }; 4A248223266E266E002D9F59 /* FolderCreationTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A248222266E266E002D9F59 /* FolderCreationTask.swift */; }; @@ -34,6 +41,11 @@ 4A2FD08B25B5E437008565C8 /* OpenExistingVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2FD08A25B5E437008565C8 /* OpenExistingVaultCoordinator.swift */; }; 4A3D6554267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D6553267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift */; }; 4A3D655F268099F9000DA764 /* VaultCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D655E268099F9000DA764 /* VaultCoordinatorError.swift */; }; + 4A3D65612680A3CB000DA764 /* LocalFileSystemAuthenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D65602680A3CB000DA764 /* LocalFileSystemAuthenticating.swift */; }; + 4A3D65642680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D65632680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift */; }; + 4A3D65662680A842000DA764 /* LocalFileSystemAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D65652680A842000DA764 /* LocalFileSystemAuthenticationViewModel.swift */; }; + 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D658126838991000DA764 /* OpenExistingLocalVaultViewModel.swift */; }; + 4A3D658626847B11000DA764 /* CreateNewLocalVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D658526847B11000DA764 /* CreateNewLocalVaultViewModel.swift */; }; 4A447DB325BEF68100D9520D /* CloudChoosing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A447DB225BEF68100D9520D /* CloudChoosing.swift */; }; 4A447DBC25BF003400D9520D /* ChooseCloudViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A447DBB25BF003400D9520D /* ChooseCloudViewModel.swift */; }; 4A447E0425BF0B0F00D9520D /* SingleSectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A447E0325BF0B0F00D9520D /* SingleSectionTableViewController.swift */; }; @@ -77,6 +89,19 @@ 4A66F57925C47BB2001BE15E /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A66F57825C47BB2001BE15E /* TextFieldCell.swift */; }; 4A66F58225C487C9001BE15E /* PasswordFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A66F58125C487C9001BE15E /* PasswordFieldCell.swift */; }; 4A66F58B25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A66F58A25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift */; }; + 4A6A51FF268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A51FE268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift */; }; + 4A6A5204268B2915006F7368 /* OpenExistingLocalVaultViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A5203268B2915006F7368 /* OpenExistingLocalVaultViewModelTests.swift */; }; + 4A6A5206268B2B24006F7368 /* MockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A5205268B2B24006F7368 /* MockError.swift */; }; + 4A6A5208268B2B75006F7368 /* AddLocalVaultViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A5207268B2B75006F7368 /* AddLocalVaultViewModelTestCase.swift */; }; + 4A6A520B268B3710006F7368 /* CreateNewLocalVaultViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A520A268B3710006F7368 /* CreateNewLocalVaultViewModelTests.swift */; }; + 4A6A520D268B5EF7006F7368 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A520C268B5EF7006F7368 /* RootViewController.swift */; }; + 4A6A520F268B5FA1006F7368 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A520E268B5FA1006F7368 /* OnboardingView.swift */; }; + 4A6A5212268B6697006F7368 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AE97DB324572E4A00452814 /* Assets.xcassets */; }; + 4A6A5213268B66AC006F7368 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AF91CC625A6437000ACF01E /* Colors.xcassets */; }; + 4A6A5214268B6937006F7368 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4A6A5216268B6937006F7368 /* Localizable.strings */; }; + 4A6A5219268B6D32006F7368 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A5218268B6D31006F7368 /* OnboardingViewController.swift */; }; + 4A6A521B268B7147006F7368 /* FileProviderCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A521A268B7147006F7368 /* FileProviderCoordinator.swift */; }; + 4A6A521D268B7C8F006F7368 /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A521C268B7C8F006F7368 /* BaseNavigationController.swift */; }; 4A717CD924C835740048E08F /* ReparentTaskManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A717CD824C835740048E08F /* ReparentTaskManagerTests.swift */; }; 4A753DB92678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */; }; 4A797F8F24AC6731007DDBE1 /* FileProviderItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A797F8E24AC6731007DDBE1 /* FileProviderItemTests.swift */; }; @@ -109,8 +134,6 @@ 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */; }; 4AA621D9249A6A8400A0BCBD /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */; }; 4AA621DD249A6A8400A0BCBD /* FileProviderEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621DC249A6A8400A0BCBD /* FileProviderEnumerator.swift */; }; - 4AA621E7249A6A8400A0BCBD /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621E6249A6A8400A0BCBD /* DocumentActionViewController.swift */; }; - 4AA621EA249A6A8400A0BCBD /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AA621E8249A6A8400A0BCBD /* MainInterface.storyboard */; }; 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */; }; 4AA8615125C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */; }; @@ -192,6 +215,7 @@ 747F2F3B2587BC4B0072FB30 /* FileSystemLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740375FF2587AEB60023FF53 /* FileSystemLock.swift */; }; 747F2F3C2587BC4B0072FB30 /* RWLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740375FE2587AEB60023FF53 /* RWLock.swift */; }; 747F2F3D2587BC4B0072FB30 /* LockNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740376002587AEB60023FF53 /* LockNode.swift */; }; + 74D365BA268B5DB0005ECD69 /* FilesAppUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74D365B9268B5DB0005ECD69 /* FilesAppUtil.swift */; }; 74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; }; /* End PBXBuildFile section */ @@ -280,9 +304,16 @@ 4A03257725A36A6900E63D7A /* VaultListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewController.swift; sourceTree = ""; }; 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Preview.swift"; sourceTree = ""; }; 4A0698692619EF9C00A67F30 /* CryptomatorCommon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CryptomatorCommon; sourceTree = ""; }; + 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetailItem.swift; sourceTree = ""; }; 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Preview.swift"; sourceTree = ""; }; 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListPosition.swift; sourceTree = ""; }; 4A123EA724BEF5F0001D1CF7 /* CloudProviderPaginationMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudProviderPaginationMock.swift; sourceTree = ""; }; + 4A1EB0C92689C373006D072B /* LocalVaultAdding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalVaultAdding.swift; sourceTree = ""; }; + 4A1EB0CB2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewLocalVaultCoordinator.swift; sourceTree = ""; }; + 4A1EB0CD2689C7A3006D072B /* DetectedVaultFailureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedVaultFailureViewController.swift; sourceTree = ""; }; + 4A1EB0CF2689C7F8006D072B /* DetectedVaultFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedVaultFailureView.swift; sourceTree = ""; }; + 4A1EB0D4268A5AA3006D072B /* VaultDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetector.swift; sourceTree = ""; }; + 4A1EB0D7268A6DE1006D072B /* AddLocalVaultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalVaultViewController.swift; sourceTree = ""; }; 4A2245D024A5E16300DBA437 /* CryptomatorFileProviderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CryptomatorFileProviderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4A2245D424A5E16300DBA437 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4A2245DB24A5E1C600DBA437 /* MetadataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataManagerTests.swift; sourceTree = ""; }; @@ -305,6 +336,11 @@ 4A2FD08A25B5E437008565C8 /* OpenExistingVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultCoordinator.swift; sourceTree = ""; }; 4A3D6553267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewVaultPasswordViewModelTests.swift; sourceTree = ""; }; 4A3D655E268099F9000DA764 /* VaultCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCoordinatorError.swift; sourceTree = ""; }; + 4A3D65602680A3CB000DA764 /* LocalFileSystemAuthenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileSystemAuthenticating.swift; sourceTree = ""; }; + 4A3D65632680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileSystemAuthenticationViewController.swift; sourceTree = ""; }; + 4A3D65652680A842000DA764 /* LocalFileSystemAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileSystemAuthenticationViewModel.swift; sourceTree = ""; }; + 4A3D658126838991000DA764 /* OpenExistingLocalVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingLocalVaultViewModel.swift; sourceTree = ""; }; + 4A3D658526847B11000DA764 /* CreateNewLocalVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewLocalVaultViewModel.swift; sourceTree = ""; }; 4A447DB225BEF68100D9520D /* CloudChoosing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudChoosing.swift; sourceTree = ""; }; 4A447DBB25BF003400D9520D /* ChooseCloudViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseCloudViewModel.swift; sourceTree = ""; }; 4A447E0325BF0B0F00D9520D /* SingleSectionTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleSectionTableViewController.swift; sourceTree = ""; }; @@ -350,6 +386,18 @@ 4A66F57825C47BB2001BE15E /* TextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCell.swift; sourceTree = ""; }; 4A66F58125C487C9001BE15E /* PasswordFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordFieldCell.swift; sourceTree = ""; }; 4A66F58A25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultPasswordViewModel.swift; sourceTree = ""; }; + 4A6A51FE268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingLocalVaultCoordinator.swift; sourceTree = ""; }; + 4A6A5203268B2915006F7368 /* OpenExistingLocalVaultViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingLocalVaultViewModelTests.swift; sourceTree = ""; }; + 4A6A5205268B2B24006F7368 /* MockError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockError.swift; sourceTree = ""; }; + 4A6A5207268B2B75006F7368 /* AddLocalVaultViewModelTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalVaultViewModelTestCase.swift; sourceTree = ""; }; + 4A6A520A268B3710006F7368 /* CreateNewLocalVaultViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewLocalVaultViewModelTests.swift; sourceTree = ""; }; + 4A6A520C268B5EF7006F7368 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; + 4A6A520E268B5FA1006F7368 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 4A6A5215268B6937006F7368 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 4A6A5217268B693E006F7368 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 4A6A5218268B6D31006F7368 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + 4A6A521A268B7147006F7368 /* FileProviderCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinator.swift; sourceTree = ""; }; + 4A6A521C268B7C8F006F7368 /* BaseNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; 4A717CD824C835740048E08F /* ReparentTaskManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReparentTaskManagerTests.swift; sourceTree = ""; }; 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingLegacyVaultPasswordViewModel.swift; sourceTree = ""; }; 4A797F8E24AC6731007DDBE1 /* FileProviderItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderItemTests.swift; sourceTree = ""; }; @@ -386,8 +434,6 @@ 4AA621DE249A6A8400A0BCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4AA621DF249A6A8400A0BCBD /* FileProviderExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderExtension.entitlements; sourceTree = ""; }; 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExtensionUI.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AA621E6249A6A8400A0BCBD /* DocumentActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentActionViewController.swift; sourceTree = ""; }; - 4AA621E9249A6A8400A0BCBD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 4AA621EB249A6A8400A0BCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedMasterkeyViewModel.swift; sourceTree = ""; }; 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultChooseFolderViewController.swift; sourceTree = ""; }; @@ -475,6 +521,7 @@ 7439031225E0008D00BB3B81 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 7439031B25E0009D00BB3B81 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; + 74D365B9268B5DB0005ECD69 /* FilesAppUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesAppUtil.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -535,6 +582,27 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4A1EB0D3268A5A44006D072B /* LocalVault */ = { + isa = PBXGroup; + children = ( + 4A1EB0CB2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift */, + 4A3D658526847B11000DA764 /* CreateNewLocalVaultViewModel.swift */, + ); + path = LocalVault; + sourceTree = ""; + }; + 4A1EB0D6268A6CF5006D072B /* LocalVault */ = { + isa = PBXGroup; + children = ( + 4A1EB0D7268A6DE1006D072B /* AddLocalVaultViewController.swift */, + 4A3D65602680A3CB000DA764 /* LocalFileSystemAuthenticating.swift */, + 4A3D65632680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift */, + 4A3D65652680A842000DA764 /* LocalFileSystemAuthenticationViewModel.swift */, + 4A1EB0C92689C373006D072B /* LocalVaultAdding.swift */, + ); + path = LocalVault; + sourceTree = ""; + }; 4A2245D124A5E16300DBA437 /* CryptomatorFileProviderTests */ = { isa = PBXGroup; children = ( @@ -637,6 +705,7 @@ 4AA621E5249A6A8400A0BCBD /* FileProviderExtensionUI */, 4A828287252617D600A4EAD4 /* Frameworks */, 4A5E5B2A2453119100BD6298 /* Products */, + 74D365BC268B965B005ECD69 /* SharedResources */, ); sourceTree = ""; }; @@ -658,17 +727,39 @@ isa = PBXGroup; children = ( 4A644B4A267B4C08008CBB9A /* CreateNewVaultChooseFolderViewController.swift */, + 4A53CC16267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift */, 4A644B4C267B55E4008CBB9A /* CreateNewVaultCoordinator.swift */, + 4A53CC12267CC1C100853BB3 /* CreateNewVaultPasswordViewController.swift */, + 4A53CC14267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift */, + 4A1EB0CF2689C7F8006D072B /* DetectedVaultFailureView.swift */, + 4A1EB0CD2689C7A3006D072B /* DetectedVaultFailureViewController.swift */, 4A644B4E267B9E6A008CBB9A /* SetVaultNameCoordinator.swift */, 4A644B43267A3BEC008CBB9A /* SetVaultNameViewController.swift */, 4A644B46267A3D43008CBB9A /* SetVaultNameViewModel.swift */, - 4A53CC12267CC1C100853BB3 /* CreateNewVaultPasswordViewController.swift */, - 4A53CC14267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift */, - 4A53CC16267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift */, + 4A1EB0D3268A5A44006D072B /* LocalVault */, ); path = CreateNewVault; sourceTree = ""; }; + 4A6A5200268B1BEF006F7368 /* LocalVault */ = { + isa = PBXGroup; + children = ( + 4A6A51FE268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift */, + 4A3D658126838991000DA764 /* OpenExistingLocalVaultViewModel.swift */, + ); + path = LocalVault; + sourceTree = ""; + }; + 4A6A5209268B2B7B006F7368 /* AddLocalVault */ = { + isa = PBXGroup; + children = ( + 4A6A5207268B2B75006F7368 /* AddLocalVaultViewModelTestCase.swift */, + 4A6A520A268B3710006F7368 /* CreateNewLocalVaultViewModelTests.swift */, + 4A6A5203268B2915006F7368 /* OpenExistingLocalVaultViewModelTests.swift */, + ); + path = AddLocalVault; + sourceTree = ""; + }; 4A7B97CA25B6F7340044B7FB /* CloudAccountList */ = { isa = PBXGroup; children = ( @@ -700,6 +791,7 @@ 4A3D655E268099F9000DA764 /* VaultCoordinatorError.swift */, 4A2FD08125B5E2BA008565C8 /* VaultInstalling.swift */, 4A644B45267A3D21008CBB9A /* CreateNewVault */, + 4A1EB0D6268A6CF5006D072B /* LocalVault */, 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */, ); path = AddVault; @@ -721,17 +813,21 @@ 4A8195E325ADB92600F7DDA1 /* Common */ = { isa = PBXGroup; children = ( + 4A6A521C268B7C8F006F7368 /* BaseNavigationController.swift */, 4A644B56267C958F008CBB9A /* ChildCoordinator.swift */, 4AFCE53925B9D6A60069C4FC /* CloudAuthenticator.swift */, 4AFCE51E25B89CD80069C4FC /* CloudProviderType+Localization.swift */, 4A03255425A3685500E63D7A /* Coordinator.swift */, 4AF91CE125A7234500ACF01E /* DatabaseManager.swift */, 4AFCE4DC25B8514F0069C4FC /* EditableTableViewHeader.swift */, + 74D365B9268B5DB0005ECD69 /* FilesAppUtil.swift */, 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */, 4A8D061B25C84C450082C5F7 /* SingleSectionHeaderTableViewController.swift */, 4A447E0325BF0B0F00D9520D /* SingleSectionTableViewController.swift */, 4AF91CCF25A71C5800ACF01E /* UIImage+CloudProviderType.swift */, 7469AD99266E26B0000DCD45 /* URL+Zip.swift */, + 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */, + 4A1EB0D4268A5AA3006D072B /* VaultDetector.swift */, 4AA22C25261CA96600A17486 /* Cells */, 4A447DEB25BF064300D9520D /* ChooseFolder */, 4A7B97CA25B6F7340044B7FB /* CloudAccountList */, @@ -816,8 +912,11 @@ children = ( 4A9FCB0B251A02A3002A8B41 /* FileProviderExtensionUI.entitlements */, 4AA621EB249A6A8400A0BCBD /* Info.plist */, - 4AA621E6249A6A8400A0BCBD /* DocumentActionViewController.swift */, - 4AA621E8249A6A8400A0BCBD /* MainInterface.storyboard */, + 4A6A521A268B7147006F7368 /* FileProviderCoordinator.swift */, + 4A6A520E268B5FA1006F7368 /* OnboardingView.swift */, + 4A6A5218268B6D31006F7368 /* OnboardingViewController.swift */, + 4A6A520C268B5EF7006F7368 /* RootViewController.swift */, + 4A6A5216268B6937006F7368 /* Localizable.strings */, ); path = FileProviderExtensionUI; sourceTree = ""; @@ -826,11 +925,12 @@ isa = PBXGroup; children = ( 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */, + 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */, 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */, 4A2FD08A25B5E437008565C8 /* OpenExistingVaultCoordinator.swift */, 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */, 4A66F58A25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift */, - 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */, + 4A6A5200268B1BEF006F7368 /* LocalVault */, ); path = OpenExistingVault; sourceTree = ""; @@ -866,8 +966,6 @@ 4AE97DB824572E4A00452814 /* Info.plist */, 4AE97DAA24572E4900452814 /* AppDelegate.swift */, 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */, - 4AE97DB324572E4A00452814 /* Assets.xcassets */, - 4AF91CC625A6437000ACF01E /* Colors.xcassets */, 4A7BC0E825ADF13100F007B3 /* AddVault */, 4A8195E325ADB92600F7DDA1 /* Common */, 4AE97DB524572E4A00452814 /* LaunchScreen.storyboard */, @@ -885,11 +983,13 @@ children = ( 4AE97DC324572E4A00452814 /* Info.plist */, 4AFCE56925BAEE890069C4FC /* AccountListViewModelTests.swift */, + 4A644B58267CA3AD008CBB9A /* CreateNewFolderViewModelTests.swift */, + 4A3D6553267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift */, 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */, + 4A6A5205268B2B24006F7368 /* MockError.swift */, 4A644B48267B40C3008CBB9A /* SetVaultNameViewModelTests.swift */, 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */, - 4A644B58267CA3AD008CBB9A /* CreateNewFolderViewModelTests.swift */, - 4A3D6553267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift */, + 4A6A5209268B2B7B006F7368 /* AddLocalVault */, ); path = CryptomatorTests; sourceTree = ""; @@ -1013,6 +1113,15 @@ path = Settings; sourceTree = ""; }; + 74D365BC268B965B005ECD69 /* SharedResources */ = { + isa = PBXGroup; + children = ( + 4AE97DB324572E4A00452814 /* Assets.xcassets */, + 4AF91CC625A6437000ACF01E /* Colors.xcassets */, + ); + path = SharedResources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1243,7 +1352,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4AA621EA249A6A8400A0BCBD /* MainInterface.storyboard in Resources */, + 4A6A5213268B66AC006F7368 /* Colors.xcassets in Resources */, + 4A6A5212268B6697006F7368 /* Assets.xcassets in Resources */, + 4A6A5214268B6937006F7368 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1288,7 +1399,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f ./.cloud-access-secrets.sh ]; then\n source ./.cloud-access-secrets.sh\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:0:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:0:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; + shellScript = "if [ -f ./.cloud-access-secrets.sh ]; then\n source ./.cloud-access-secrets.sh\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; }; 742595D72552EE0000A8A008 /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; @@ -1409,7 +1520,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4AA621E7249A6A8400A0BCBD /* DocumentActionViewController.swift in Sources */, + 4A6A520F268B5FA1006F7368 /* OnboardingView.swift in Sources */, + 4A6A520D268B5EF7006F7368 /* RootViewController.swift in Sources */, + 4A6A521B268B7147006F7368 /* FileProviderCoordinator.swift in Sources */, + 4A6A5219268B6D32006F7368 /* OnboardingViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1425,13 +1539,16 @@ 4A7B97C225B6F7200044B7FB /* AccountListViewController.swift in Sources */, 4A66F57925C47BB2001BE15E /* TextFieldCell.swift in Sources */, 4A53CC17267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift in Sources */, + 4A6A521D268B7C8F006F7368 /* BaseNavigationController.swift in Sources */, 4ADD2342267383BE00374E4E /* AddVaultSuccessViewModel.swift in Sources */, 4AF91CD925A722A600ACF01E /* VaultInfo.swift in Sources */, + 74D365BA268B5DB0005ECD69 /* FilesAppUtil.swift in Sources */, 4A644B4D267B55E4008CBB9A /* CreateNewVaultCoordinator.swift in Sources */, 740D3682266A19150058744D /* SettingsViewModel.swift in Sources */, 4A7B97D325B6F7520044B7FB /* AccountListViewModel.swift in Sources */, 4A7BC0F125ADFAD600F007B3 /* AddVaultCoordinator.swift in Sources */, 4A3D655F268099F9000DA764 /* VaultCoordinatorError.swift in Sources */, + 4A1EB0CC2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift in Sources */, 4A644B57267C958F008CBB9A /* ChildCoordinator.swift in Sources */, 4A53CC15267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift in Sources */, 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */, @@ -1439,6 +1556,7 @@ 4A447E4D25BF1E8B00D9520D /* FileCell.swift in Sources */, 4A644B44267A3BEC008CBB9A /* SetVaultNameViewController.swift in Sources */, 4A2FD04425B1C3BB008565C8 /* EmptyListMessage.swift in Sources */, + 4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */, 4A66F58B25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift in Sources */, 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */, 4A7BC0E025ADF12D00F007B3 /* AddVaultViewController.swift in Sources */, @@ -1448,31 +1566,38 @@ 4AFCE53A25B9D6A60069C4FC /* CloudAuthenticator.swift in Sources */, 4A9D1247261E227600A670E2 /* WebDAVAuthenticationCoordinator.swift in Sources */, 4A644B55267C926A008CBB9A /* FolderCreating.swift in Sources */, + 4A3D658626847B11000DA764 /* CreateNewLocalVaultViewModel.swift in Sources */, 4AE97DAB24572E4900452814 /* AppDelegate.swift in Sources */, 4AA22C16261CA8D800A17486 /* URLFieldCell.swift in Sources */, 4A2FD07925B5D98B008565C8 /* CloudCell.swift in Sources */, 4A447DBC25BF003400D9520D /* ChooseCloudViewModel.swift in Sources */, 740D3684266A1B180058744D /* SettingsCoordinator.swift in Sources */, 7408E6C326778D9B00D7FAEA /* AboutCoordinator.swift in Sources */, + 4A1EB0D5268A5AA3006D072B /* VaultDetector.swift in Sources */, 4AFCE4D425B842830069C4FC /* AccountListing.swift in Sources */, 4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */, 4AA8615125C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift in Sources */, 4AFCE4DD25B8514F0069C4FC /* EditableTableViewHeader.swift in Sources */, 4A2FD07025B5D5FB008565C8 /* ChooseCloudViewController.swift in Sources */, + 4A1EB0CA2689C373006D072B /* LocalVaultAdding.swift in Sources */, 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */, 4A8D060525C82F1F0082C5F7 /* AddVaultSuccesing.swift in Sources */, 740D367E266A18DF0058744D /* SettingsViewController.swift in Sources */, 4AFCE51625B880C90069C4FC /* AccountCell.swift in Sources */, 4A8D061C25C84C450082C5F7 /* SingleSectionHeaderTableViewController.swift in Sources */, + 4A3D65662680A842000DA764 /* LocalFileSystemAuthenticationViewModel.swift in Sources */, 4A447E2425BF0E3A00D9520D /* ChooseFolderViewModel.swift in Sources */, 4AA22BFB261CA69F00A17486 /* WebDAVAuthenticationViewController.swift in Sources */, 4A9D123F261E1DD400A670E2 /* WebDAVAuthenticating.swift in Sources */, 4A53CC13267CC1C100853BB3 /* CreateNewVaultPasswordViewController.swift in Sources */, + 4A6A51FF268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift in Sources */, 4A03255525A3685500E63D7A /* Coordinator.swift in Sources */, 4A644B47267A3D43008CBB9A /* SetVaultNameViewModel.swift in Sources */, + 4A1EB0D02689C7F8006D072B /* DetectedVaultFailureView.swift in Sources */, 4A53CC11267CBFA100853BB3 /* AddVaultSuccessCoordinator.swift in Sources */, 4A8D05D625C5CBE10082C5F7 /* AddVaultSuccessViewController.swift in Sources */, 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */, + 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */, 4A2FD08225B5E2BA008565C8 /* VaultInstalling.swift in Sources */, 4A8D05DF25C5CD210082C5F7 /* ButtonCell.swift in Sources */, 7408E6CD26779BCC00D7FAEA /* AboutViewModel.swift in Sources */, @@ -1482,10 +1607,12 @@ 4AFCE4FF25B871500069C4FC /* AccountCellContent.swift in Sources */, 4A2FD08B25B5E437008565C8 /* OpenExistingVaultCoordinator.swift in Sources */, 7469AD9A266E26B0000DCD45 /* URL+Zip.swift in Sources */, + 4A3D65642680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift in Sources */, 4A7B97DC25B6F80A0044B7FB /* AccountInfo.swift in Sources */, 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */, 4A447E5625BF1F6A00D9520D /* CloudItemCell.swift in Sources */, 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */, + 4A1EB0D8268A6DE1006D072B /* AddLocalVaultViewController.swift in Sources */, 4A7B97E525B6F86E0044B7FB /* AccountListPosition.swift in Sources */, 4A447E3D25BF1AD400D9520D /* FolderCell.swift in Sources */, 4A644B4B267B4C08008CBB9A /* CreateNewVaultChooseFolderViewController.swift in Sources */, @@ -1493,9 +1620,11 @@ 4AF91CE225A7234500ACF01E /* DatabaseManager.swift in Sources */, 4AFCE51F25B89CD80069C4FC /* CloudProviderType+Localization.swift in Sources */, 4A447E0425BF0B0F00D9520D /* SingleSectionTableViewController.swift in Sources */, + 4A3D65612680A3CB000DA764 /* LocalFileSystemAuthenticating.swift in Sources */, 4A9D1237261DAC5D00A670E2 /* WebDAVAuthenticationViewModel.swift in Sources */, 4A753DB92678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift in Sources */, 7408E6C126778C7A00D7FAEA /* LocalWebViewModel.swift in Sources */, + 4A1EB0CE2689C7A3006D072B /* DetectedVaultFailureViewController.swift in Sources */, 4A644B51267BAAF4008CBB9A /* CreateNewFolderViewController.swift in Sources */, 4A644B5B267CA972008CBB9A /* DetectedVaultView.swift in Sources */, ); @@ -1505,12 +1634,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A6A520B268B3710006F7368 /* CreateNewLocalVaultViewModelTests.swift in Sources */, + 4A6A5208268B2B75006F7368 /* AddLocalVaultViewModelTestCase.swift in Sources */, 4AF91CF425A8BB0D00ACF01E /* VaultListViewModelTests.swift in Sources */, 4A3D6554267CF17B000DA764 /* CreateNewVaultPasswordViewModelTests.swift in Sources */, 4AF91CEB25A7306E00ACF01E /* DatabaseManagerTests.swift in Sources */, 4A644B49267B40C3008CBB9A /* SetVaultNameViewModelTests.swift in Sources */, 4AFCE56A25BAEE890069C4FC /* AccountListViewModelTests.swift in Sources */, 4A644B59267CA3AD008CBB9A /* CreateNewFolderViewModelTests.swift in Sources */, + 4A6A5206268B2B24006F7368 /* MockError.swift in Sources */, + 4A6A5204268B2915006F7368 /* OpenExistingLocalVaultViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1611,12 +1744,13 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 4AA621E8249A6A8400A0BCBD /* MainInterface.storyboard */ = { + 4A6A5216268B6937006F7368 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - 4AA621E9249A6A8400A0BCBD /* Base */, + 4A6A5215268B6937006F7368 /* en */, + 4A6A5217268B693E006F7368 /* de */, ); - name = MainInterface.storyboard; + name = Localizable.strings; sourceTree = ""; }; 4AE97DB524572E4A00452814 /* LaunchScreen.storyboard */ = { diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5f9ee1cf3..d7b677d6b 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/cryptomator/cloud-access-swift.git", "state": { "branch": null, - "revision": "fb5090d76f1f8861f57d2186d9109449c7867918", - "version": "0.12.2" + "revision": "785aba8f7d8b0c4762667b5b12c70870ca56b224", + "version": "0.12.4" } }, { diff --git a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift index aa28563a8..1d34d62f5 100644 --- a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift +++ b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift @@ -43,22 +43,7 @@ class AddVaultSuccessCoordinator: AddVaultSuccesing, Coordinator { } func showFilesApp(forVaultUID vaultUID: String) { - guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) else { - DDLogDebug("containerURL is nil") - return - } - let url = containerURL.appendingPathComponent("File Provider Storage").appendingPathComponent(vaultUID) - guard let sharedDocumentsURL = changeSchemeToSharedDocuments(for: url) else { - DDLogDebug("Conversion to shared documents url failed") - return - } - UIApplication.shared.open(sharedDocumentsURL) + FilesAppUtil.showFilesApp(forVaultUID: vaultUID) done() } - - private func changeSchemeToSharedDocuments(for url: URL) -> URL? { - var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) - comps?.scheme = "shareddocuments" - return comps?.url - } } diff --git a/Cryptomator/AddVault/AddVaultSuccessViewController.swift b/Cryptomator/AddVault/AddVaultSuccessViewController.swift index 6c3f363cb..199a4e637 100644 --- a/Cryptomator/AddVault/AddVaultSuccessViewController.swift +++ b/Cryptomator/AddVault/AddVaultSuccessViewController.swift @@ -87,10 +87,10 @@ private class VaultSuccessHeaderView: UIView { addSubview(stack) NSLayoutConstraint.activate([ - stack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - stack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - stack.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 12), - stack.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -12) + stack.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor, constant: 20), + stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20) ]) } } diff --git a/Cryptomator/AddVault/AddVaultSuccessViewModel.swift b/Cryptomator/AddVault/AddVaultSuccessViewModel.swift index 5030aee59..f97aa5eff 100644 --- a/Cryptomator/AddVault/AddVaultSuccessViewModel.swift +++ b/Cryptomator/AddVault/AddVaultSuccessViewModel.swift @@ -7,6 +7,7 @@ // import Foundation + struct AddVaultSuccessViewModel { let vaultName: String let vaultUID: String diff --git a/Cryptomator/AddVault/AddVaultViewController.swift b/Cryptomator/AddVault/AddVaultViewController.swift index 4de87cc2b..eb3c3c78e 100644 --- a/Cryptomator/AddVault/AddVaultViewController.swift +++ b/Cryptomator/AddVault/AddVaultViewController.swift @@ -71,10 +71,10 @@ class AddVaultViewController: UITableViewController { imageView.contentMode = .scaleAspectFit imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: headerView.layoutMarginsGuide.leadingAnchor), - imageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -20), + imageView.leadingAnchor.constraint(equalTo: headerView.readableContentGuide.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: headerView.readableContentGuide.trailingAnchor), imageView.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 20), - imageView.trailingAnchor.constraint(equalTo: headerView.layoutMarginsGuide.trailingAnchor) + imageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -20) ]) return headerView @@ -93,7 +93,6 @@ class AddVaultViewController: UITableViewController { } #if DEBUG -import CryptomatorCloudAccess import SwiftUI struct VaultAddVCPreview: PreviewProvider { diff --git a/Cryptomator/AddVault/ChooseCloudViewController.swift b/Cryptomator/AddVault/ChooseCloudViewController.swift index 1e008152a..a138af59c 100644 --- a/Cryptomator/AddVault/ChooseCloudViewController.swift +++ b/Cryptomator/AddVault/ChooseCloudViewController.swift @@ -57,7 +57,6 @@ class ChooseCloudViewController: SingleSectionHeaderTableViewController { } #if DEBUG -import CryptomatorCloudAccess import SwiftUI struct ChooseCloudVCPreview: PreviewProvider { diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift index b0d2ae0de..78ab37162 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift @@ -7,11 +7,8 @@ // import UIKit -class CreateNewVaultChooseFolderViewController: ChooseFolderViewController { - init(viewModel: CreateNewVaultChooseFolderViewModelProtocol) { - super.init(with: viewModel) - } +class CreateNewVaultChooseFolderViewController: ChooseFolderViewController { override func viewDidLoad() { super.viewDidLoad() title = NSLocalizedString("addVault.createNewVault.title", comment: "") @@ -25,16 +22,16 @@ class CreateNewVaultChooseFolderViewController: ChooseFolderViewController { toolbarItems?.append(createFolderButton) } - override func showDetectedVault(_ vault: Item) { - let failureView = FailureView() + override func showDetectedVault(_ vault: VaultDetailItem) { + let failureView = DetectedVaultFailureView() let containerView = UIView() failureView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(failureView) NSLayoutConstraint.activate([ - failureView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), failureView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - failureView.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor), - failureView.trailingAnchor.constraint(equalTo: containerView.readableContentGuide.trailingAnchor) + failureView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + failureView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + failureView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor) ]) // Prevents the view from being placed under the navigation bar @@ -58,23 +55,36 @@ class CreateNewVaultChooseFolderViewController: ChooseFolderViewController { } } -private class FailureView: DetectedVaultView { - init() { - let configuration = UIImage.SymbolConfiguration(pointSize: 120) - let warningSymbol = UIImage(systemName: "exclamationmark.triangle.fill", withConfiguration: configuration) - let imageView = UIImageView(image: warningSymbol) - imageView.tintColor = UIColor(named: "yellow") - super.init(imageView: imageView, text: NSLocalizedString("addVault.createNewVault.detectedMasterkey.text", comment: "")) - } -} - #if DEBUG import CryptomatorCloudAccessCore import SwiftUI -struct FailureView_Preview: PreviewProvider { - static var previews: some View { - FailureView().toPreview() + +private class CreateNewVaultChooseFolderViewModelMock: ChooseFolderViewModelProtocol { + var headerTitle: String = "/Vault" + + let foundMasterkey = true + let canCreateFolder: Bool + let cloudPath: CloudPath + let items: [CloudItemMetadata] = [] + + init(cloudPath: CloudPath, canCreateFolder: Bool) { + self.canCreateFolder = canCreateFolder + self.cloudPath = cloudPath + } + + func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) { + onChange() } + + func refreshItems() {} } +struct CreateNewVaultChooseFolderVCPreview: PreviewProvider { + static var previews: some View { + let viewController = CreateNewVaultChooseFolderViewController(with: CreateNewVaultChooseFolderViewModelMock(cloudPath: CloudPath("/Vault"), canCreateFolder: false)) + let item = VaultDetailItem(name: "Vault", vaultPath: CloudPath("/Vault/masterkey.cryptomator"), isLegacyVault: false) + viewController.showDetectedVault(item) + return viewController.toPreview() + } +} #endif diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewModel.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewModel.swift index 19b10eeb3..9c008d28b 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewModel.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewModel.swift @@ -8,9 +8,10 @@ import CryptomatorCloudAccessCore import Foundation + protocol CreateNewVaultChooseFolderViewModelProtocol: ChooseFolderViewModelProtocol { var vaultName: String { get } - func chooseCurrentFolder() throws -> Item + func chooseCurrentFolder() throws -> Folder } class CreateNewVaultChooseFolderViewModel: ChooseFolderViewModel, CreateNewVaultChooseFolderViewModelProtocol { @@ -21,12 +22,12 @@ class CreateNewVaultChooseFolderViewModel: ChooseFolderViewModel, CreateNewVault super.init(canCreateFolder: true, cloudPath: cloudPath, provider: provider) } - func chooseCurrentFolder() throws -> Item { + func chooseCurrentFolder() throws -> Folder { guard !items.contains(where: { $0.name == vaultName }) else { throw CreateNewVaultChooseFolderViewModelError.vaultNameCollision } let vaultPath = cloudPath.appendingPathComponent(vaultName) - return Item(type: .folder, path: vaultPath) + return Folder(path: vaultPath) } } diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index aaaf02ec8..114602c35 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import UIKit @@ -32,16 +33,13 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, Coordinator { func showAccountList(for cloudProviderType: CloudProviderType) { if cloudProviderType == .localFileSystem { - let alertController = UIAlertController(title: "Info", message: NSLocalizedString("testFlight.otherFileProviders.alert.text", comment: ""), preferredStyle: .alert) - let okAction = UIAlertAction(title: NSLocalizedString("common.button.ok", comment: ""), style: .default) - alertController.addAction(okAction) - navigationController.present(alertController, animated: true, completion: nil) - return + startLocalFileSystemAuthenticationFlow() + } else { + let viewModel = AccountListViewModel(with: cloudProviderType) + let accountListVC = AccountListViewController(with: viewModel) + accountListVC.coordinator = self + navigationController.pushViewController(accountListVC, animated: true) } - let viewModel = AccountListViewModel(with: cloudProviderType) - let accountListVC = AccountListViewController(with: viewModel) - accountListVC.coordinator = self - navigationController.pushViewController(accountListVC, animated: true) } func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { @@ -70,6 +68,23 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, Coordinator { navigationController.dismiss(animated: true) parentCoordinator?.childDidFinish(self) } + + // MARK: - LocalFileSystemProvider Flow + + private func startLocalFileSystemAuthenticationFlow() { + let child = CreateNewLocalVaultCoordinator(vaultName: vaultName, navigationController: navigationController) + childCoordinators.append(child) + child.parentCoordinator = self + child.start() + } + + func startAuthenticatedLocalFileSystemCreateNewVaultFlow(credential: LocalFileSystemCredential, account: CloudProviderAccount, item: Item) { + let provider = LocalFileSystemProvider(rootURL: credential.rootURL) + let child = AuthenticatedCreateNewVaultCoordinator(navigationController: navigationController, provider: provider, account: account, vaultName: vaultName) + childCoordinators.append(child) + child.parentCoordinator = self + child.chooseItem(item) + } } private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInstalling, Coordinator { @@ -96,7 +111,7 @@ private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInsta func showItems(for path: CloudPath) { let viewModel = CreateNewVaultChooseFolderViewModel(vaultName: vaultName, cloudPath: path, provider: provider) - let chooseFolderVC = CreateNewVaultChooseFolderViewController(viewModel: viewModel) + let chooseFolderVC = CreateNewVaultChooseFolderViewController(with: viewModel) chooseFolderVC.coordinator = self navigationController.pushViewController(chooseFolderVC, animated: true) } @@ -106,19 +121,18 @@ private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInsta } func chooseItem(_ item: Item) { - switch item.type { - case .folder: - let viewModel = CreateNewVaultPasswordViewModel(vaultPath: item.path, account: account, vaultUID: UUID().uuidString) - let passwordVC = CreateNewVaultPasswordViewController(viewModel: viewModel) - passwordVC.coordinator = self - navigationController.pushViewController(passwordVC, animated: true) - default: + guard let vaultFolder = item as? Folder else { handleError(VaultCoordinatorError.wrongItemType, for: navigationController) + return } + let viewModel = CreateNewVaultPasswordViewModel(vaultPath: vaultFolder.path, account: account, vaultUID: UUID().uuidString) + let passwordVC = CreateNewVaultPasswordViewController(viewModel: viewModel) + passwordVC.coordinator = self + navigationController.pushViewController(passwordVC, animated: true) } func showCreateNewFolder(parentPath: CloudPath) { - let modalNavigationController = UINavigationController() + let modalNavigationController = BaseNavigationController() let child = AuthenticatedFolderCreationCoordinator(navigationController: modalNavigationController, provider: provider, parentPath: parentPath) child.parentCoordinator = self childCoordinators.append(child) @@ -126,6 +140,13 @@ private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInsta child.start() } + func handleError(error: Error) { + navigationController.popViewController(animated: true) + if let topViewController = navigationController.topViewController { + handleError(error, for: topViewController) + } + } + // MARK: - VaultInstalling func showSuccessfullyAddedVault(withName name: String, vaultUID: String) { diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewController.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewController.swift index f826c4981..58a1e7712 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewController.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewController.swift @@ -7,6 +7,7 @@ // import UIKit + class CreateNewVaultPasswordViewController: UITableViewController { weak var coordinator: (Coordinator & VaultInstalling)? private var viewModel: CreateNewVaultPasswordViewModelProtocol @@ -121,6 +122,7 @@ class CreateNewVaultPasswordViewController: UITableViewController { #if DEBUG import Promises import SwiftUI + private class CreateNewVaultPasswordViewModelMock: CreateNewVaultPasswordViewModelProtocol { let vaultUID = "" let vaultName = "" diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewModel.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewModel.swift index 6598f8b2b..7ba5072cd 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultPasswordViewModel.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation import Promises + protocol CreateNewVaultPasswordViewModelProtocol { var headerTitles: [String] { get } var vaultUID: String { get } @@ -26,7 +27,10 @@ class CreateNewVaultPasswordViewModel: CreateNewVaultPasswordViewModelProtocol { } let vaultUID: String - let headerTitles = ["Enter a new password.", "Confirm the new password."] + let headerTitles = [ + NSLocalizedString("addVault.createNewVault.enterPassword.header", comment: ""), + NSLocalizedString("addVault.createNewVault.confirmPassword.header", comment: "") + ] var password: String? var confirmingPassword: String? diff --git a/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureView.swift b/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureView.swift new file mode 100644 index 000000000..2a6f94472 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureView.swift @@ -0,0 +1,19 @@ +// +// DetectedVaultFailureView.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class DetectedVaultFailureView: DetectedVaultView { + init() { + let configuration = UIImage.SymbolConfiguration(pointSize: 120) + let warningSymbol = UIImage(systemName: "exclamationmark.triangle.fill", withConfiguration: configuration) + let imageView = UIImageView(image: warningSymbol) + imageView.tintColor = UIColor(named: "yellow") + super.init(imageView: imageView, text: NSLocalizedString("addVault.createNewVault.detectedMasterkey.text", comment: "")) + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureViewController.swift b/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureViewController.swift new file mode 100644 index 000000000..7fce631c1 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/DetectedVaultFailureViewController.swift @@ -0,0 +1,42 @@ +// +// DetectedVaultFailureViewController.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class DetectedVaultFailureViewController: UIViewController { + override func viewDidLoad() { + let failureView = DetectedVaultFailureView() + let containerView = UIView() + containerView.backgroundColor = .systemGroupedBackground + view.addSubview(containerView) + failureView.translatesAutoresizingMaskIntoConstraints = false + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(failureView) + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + + failureView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + failureView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + failureView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + failureView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor) + ]) + } +} + +#if DEBUG +import SwiftUI + +struct DetectedVaultFailureVC_Preview: PreviewProvider { + static var previews: some View { + DetectedVaultFailureViewController().toPreview() + } +} +#endif diff --git a/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultCoordinator.swift new file mode 100644 index 000000000..b96c758ae --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultCoordinator.swift @@ -0,0 +1,51 @@ +// +// CreateNewLocalVaultCoordinator.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class CreateNewLocalVaultCoordinator: LocalVaultAdding, LocalFileSystemAuthenticating, Coordinator { + var childCoordinators = [Coordinator]() + var navigationController: UINavigationController + weak var parentCoordinator: CreateNewVaultCoordinator? + private let vaultName: String + + init(vaultName: String, navigationController: UINavigationController) { + self.vaultName = vaultName + self.navigationController = navigationController + } + + func start() { + let viewModel = CreateNewLocalVaultViewModel(vaultName: vaultName) + let localFSAuthVC = AddLocalVaultViewController(viewModel: viewModel) + localFSAuthVC.coordinator = self + navigationController.pushViewController(localFSAuthVC, animated: true) + } + + // MARK: - LocalFileSystemAuthenticating + + func authenticated(credential: LocalFileSystemCredential) { + // TODO: Show Progress-HUD + } + + // MARK: - LocalVaultAdding + + func validationFailed(with error: Error, at viewController: UIViewController) { + // TODO: Disable Progress-HUD + if case CreateNewLocalVaultViewModelError.detectedExistingVault = error { + let failureVC = DetectedVaultFailureViewController() + failureVC.title = NSLocalizedString("addVault.createNewVault.title", comment: "") + navigationController.pushViewController(failureVC, animated: true) + } else { + handleError(error, for: viewController) + } + } + + func showPasswordScreen(for result: LocalFileSystemAuthenticationResult) { + parentCoordinator?.startAuthenticatedLocalFileSystemCreateNewVaultFlow(credential: result.credential, account: result.account, item: result.item) + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultViewModel.swift b/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultViewModel.swift new file mode 100644 index 000000000..34a4c03b0 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/LocalVault/CreateNewLocalVaultViewModel.swift @@ -0,0 +1,50 @@ +// +// CreateNewLocalVaultViewModel.swift +// Cryptomator +// +// Created by Philipp Schmid on 24.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import Promises + +class CreateNewLocalVaultViewModel: LocalFileSystemAuthenticationViewModel, LocalFileSystemVaultInstallingViewModelProtocol { + private let vaultName: String + + init(vaultName: String, accountManager: CloudProviderAccountManager = CloudProviderAccountDBManager.shared) { + let documentPickerButtonText = NSLocalizedString("localFileSystemAuthentication.createNewVault.button", comment: "") + let headerText = NSLocalizedString("localFileSystemAuthentication.createNewVault.header", comment: "") + self.vaultName = vaultName + super.init(documentPickerButtonText: documentPickerButtonText, headerText: headerText, validationLogic: CreateNewLocalVaultValidationLogic(vaultName: vaultName), accountManager: accountManager) + } + + func addVault(for credential: LocalFileSystemCredential) -> Promise { + return validateAndSave(credential: credential).then { account -> LocalFileSystemAuthenticationResult in + let vault = Folder(path: CloudPath("/\(self.vaultName)")) + return LocalFileSystemAuthenticationResult(credential: credential, account: account, item: vault) + } + } +} + +private class CreateNewLocalVaultValidationLogic: LocalFileSystemAuthenticationValidationLogic { + private let vaultName: String + init(vaultName: String) { + self.vaultName = vaultName + } + + func validate(items: [CloudItemMetadata]) throws { + guard VaultDetector.getVaultItem(items: items, parentCloudPath: CloudPath("/")) == nil else { + throw CreateNewLocalVaultViewModelError.detectedExistingVault + } + guard !items.contains(where: { $0.name == vaultName }) else { + throw CreateNewVaultChooseFolderViewModelError.vaultNameCollision + } + } +} + +enum CreateNewLocalVaultViewModelError: Error { + case detectedExistingVault +} diff --git a/Cryptomator/AddVault/CreateNewVault/SetVaultNameCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/SetVaultNameCoordinator.swift index 64f52fb80..1326cd34d 100644 --- a/Cryptomator/AddVault/CreateNewVault/SetVaultNameCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/SetVaultNameCoordinator.swift @@ -7,6 +7,7 @@ // import UIKit + protocol VaultNaming: AnyObject { func setVaultName(_ name: String) } diff --git a/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewController.swift b/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewController.swift index 765952e4f..c9a74d341 100644 --- a/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewController.swift +++ b/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewController.swift @@ -7,6 +7,7 @@ // import UIKit + class SetVaultNameViewController: SingleSectionHeaderTableViewController { weak var coordinator: (VaultNaming & Coordinator)? private var viewModel: SetVaultNameViewModelProtocol diff --git a/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewModel.swift b/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewModel.swift index 44b928c2a..c2a4b5df4 100644 --- a/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewModel.swift +++ b/Cryptomator/AddVault/CreateNewVault/SetVaultNameViewModel.swift @@ -7,6 +7,7 @@ // import Foundation + protocol SetVaultNameViewModelProtocol: SingleSectionHeaderTableViewModelProtocol { var vaultName: String? { get set } func getValidatedVaultName() throws -> String diff --git a/Cryptomator/AddVault/DetectedVaultView.swift b/Cryptomator/AddVault/DetectedVaultView.swift index d7b1075a9..d2e15a513 100644 --- a/Cryptomator/AddVault/DetectedVaultView.swift +++ b/Cryptomator/AddVault/DetectedVaultView.swift @@ -7,6 +7,7 @@ // import UIKit + class DetectedVaultView: UIView { private lazy var label: UILabel = { let label = UILabel() @@ -27,12 +28,12 @@ class DetectedVaultView: UIView { addSubview(label) NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: topAnchor), imageView.centerXAnchor.constraint(equalTo: centerXAnchor), label.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + label.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 20), label.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20), - label.bottomAnchor.constraint(equalTo: bottomAnchor), - label.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor) + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20) ]) } diff --git a/Cryptomator/AddVault/LocalVault/AddLocalVaultViewController.swift b/Cryptomator/AddVault/LocalVault/AddLocalVaultViewController.swift new file mode 100644 index 000000000..f9ed73a66 --- /dev/null +++ b/Cryptomator/AddVault/LocalVault/AddLocalVaultViewController.swift @@ -0,0 +1,35 @@ +// +// AddLocalVaultViewController.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class AddLocalVaultViewController: LocalFileSystemAuthenticationViewController { + typealias AddLocalVaultViewModel = LocalFileSystemAuthenticationViewModelProtocol & LocalFileSystemVaultInstallingViewModelProtocol + let viewModel: AddLocalVaultViewModel + + init(viewModel: AddLocalVaultViewModel) { + self.viewModel = viewModel + super.init(viewModel: viewModel) + } + + override func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + let credential: LocalFileSystemCredential + do { + credential = try viewModel.userPicked(urls: urls) + } catch { + coordinator?.handleError(error, for: self) + return + } + coordinator?.authenticated(credential: credential) + viewModel.addVault(for: credential).then { result in + self.coordinator?.showPasswordScreen(for: result) + }.catch { error in + self.coordinator?.validationFailed(with: error, at: self) + } + } +} diff --git a/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticating.swift b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticating.swift new file mode 100644 index 000000000..66f632bbf --- /dev/null +++ b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticating.swift @@ -0,0 +1,15 @@ +// +// LocalFileSystemAuthenticating.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +protocol LocalFileSystemAuthenticating { + func authenticated(credential: LocalFileSystemCredential) +} diff --git a/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewController.swift b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewController.swift new file mode 100644 index 000000000..e7d4f15df --- /dev/null +++ b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewController.swift @@ -0,0 +1,125 @@ +// +// LocalFileSystemAuthenticationViewController.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import MobileCoreServices +import UIKit +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers +#endif + +class LocalFileSystemAuthenticationViewController: SingleSectionTableViewController, UIDocumentPickerDelegate { + weak var coordinator: (LocalFileSystemAuthenticating & LocalVaultAdding & Coordinator)? + private let viewModel: LocalFileSystemAuthenticationViewModelProtocol + private lazy var openDocumentPickerCell: ButtonCell = { + let cell = ButtonCell() + cell.button.setTitle(viewModel.documentPickerButtonText, for: .normal) + cell.button.addTarget(self, action: #selector(openDocumentPicker), for: .touchUpInside) + return cell + }() + + init(viewModel: LocalFileSystemAuthenticationViewModelProtocol) { + self.viewModel = viewModel + super.init() + } + + @objc func openDocumentPicker() { + let documentPicker: UIDocumentPickerViewController + if #available(iOS 14, *) { + documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) + } else { + documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], in: .open) + } + documentPicker.allowsMultipleSelection = false + documentPicker.delegate = self + present(documentPicker, animated: true) + } + + // MARK: - UIDocumentPickerDelegate + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + do { + let credential = try viewModel.userPicked(urls: urls) + coordinator?.authenticated(credential: credential) + } catch { + coordinator?.handleError(error, for: self) + } + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return openDocumentPickerCell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return LocalFileSystemAuthenticationHeaderView(text: viewModel.headerText) + } +} + +private class LocalFileSystemAuthenticationHeaderView: UIView { + private lazy var image: UIImageView = { + let image = UIImage(named: "bot-vault") + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var infoText: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + init(text: String) { + super.init(frame: .zero) + infoText.text = text + let stack = UIStackView(arrangedSubviews: [image, infoText]) + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.spacing = 20 + addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + stack.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 12), + stack.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -12) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#if DEBUG +import CryptomatorCommonCore +import SwiftUI + +struct LocalFileSystemViewModelMock: LocalFileSystemAuthenticationViewModelProtocol { + let documentPickerButtonText = "Select Storage Location" + let headerText = "In the next screen, choose the storage location for your new vault." + func userPicked(urls: [URL]) throws -> LocalFileSystemCredential { + fatalError("Not mocked") + } +} + +struct LocalFileSystemAuthenticationVCPreview: PreviewProvider { + static var previews: some View { + LocalFileSystemAuthenticationViewController(viewModel: LocalFileSystemViewModelMock()).toPreview() + } +} +#endif diff --git a/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift new file mode 100644 index 000000000..3c6f38d44 --- /dev/null +++ b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift @@ -0,0 +1,83 @@ +// +// LocalFileSystemAuthenticationViewModel.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import Promises + +protocol LocalFileSystemAuthenticationViewModelProtocol { + var documentPickerButtonText: String { get } + var headerText: String { get } + func userPicked(urls: [URL]) throws -> LocalFileSystemCredential +} + +protocol LocalFileSystemVaultInstallingViewModelProtocol { + func addVault(for credential: LocalFileSystemCredential) -> Promise +} + +protocol LocalFileSystemAuthenticationValidationLogic { + func validate(items: [CloudItemMetadata]) throws +} + +class LocalFileSystemAuthenticationViewModel: LocalFileSystemAuthenticationViewModelProtocol { + let documentPickerButtonText: String + let headerText: String + private let validationLogic: LocalFileSystemAuthenticationValidationLogic + private let accountManager: CloudProviderAccountManager + + init(documentPickerButtonText: String, headerText: String, validationLogic: LocalFileSystemAuthenticationValidationLogic, accountManager: CloudProviderAccountManager) { + self.documentPickerButtonText = documentPickerButtonText + self.headerText = headerText + self.validationLogic = validationLogic + self.accountManager = accountManager + } + + func userPicked(urls: [URL]) throws -> LocalFileSystemCredential { + guard let rootURL = urls.first else { + throw LocalFileSystemAuthenticationViewModelError.invalidURL + } + let credential = LocalFileSystemCredential(rootURL: rootURL, identifier: UUID().uuidString) + return credential + } + + func validateAndSave(credential: LocalFileSystemCredential) -> Promise { + return validate(credential: credential).then { + try self.save(credential: credential) + } + } + + private func validate(credential: LocalFileSystemCredential) -> Promise { + let provider = LocalFileSystemProvider(rootURL: credential.rootURL) + return provider.fetchItemListExhaustively(forFolderAt: CloudPath("/")).then { itemList in + try self.validationLogic.validate(items: itemList.items) + } + } + + private func save(credential: LocalFileSystemCredential) throws -> CloudProviderAccount { + try LocalFileSystemBookmarkManager.saveBookmarkForRootURL(credential.rootURL, for: credential.identifier) + let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .localFileSystem) + try accountManager.saveNewAccount(account) + return account + } +} + +struct LocalFileSystemCredential { + let rootURL: URL + let identifier: String +} + +struct LocalFileSystemAuthenticationResult { + let credential: LocalFileSystemCredential + let account: CloudProviderAccount + let item: Item +} + +enum LocalFileSystemAuthenticationViewModelError: Error { + case invalidURL +} diff --git a/Cryptomator/AddVault/LocalVault/LocalVaultAdding.swift b/Cryptomator/AddVault/LocalVault/LocalVaultAdding.swift new file mode 100644 index 000000000..58ba1030d --- /dev/null +++ b/Cryptomator/AddVault/LocalVault/LocalVaultAdding.swift @@ -0,0 +1,14 @@ +// +// LocalVaultAdding.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +protocol LocalVaultAdding { + func validationFailed(with error: Error, at viewController: UIViewController) + func showPasswordScreen(for result: LocalFileSystemAuthenticationResult) +} diff --git a/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultCoordinator.swift new file mode 100644 index 000000000..130670cbe --- /dev/null +++ b/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultCoordinator.swift @@ -0,0 +1,43 @@ +// +// OpenExistingLocalVaultCoordinator.swift +// Cryptomator +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class OpenExistingLocalVaultCoordinator: LocalVaultAdding, LocalFileSystemAuthenticating, Coordinator { + var childCoordinators = [Coordinator]() + var navigationController: UINavigationController + weak var parentCoordinator: OpenExistingVaultCoordinator? + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start() { + let viewModel = OpenExistingLocalVaultViewModel() + let localFSAuthVC = AddLocalVaultViewController(viewModel: viewModel) + localFSAuthVC.coordinator = self + navigationController.pushViewController(localFSAuthVC, animated: true) + } + + // MARK: - LocalFileSystemAuthenticating + + func authenticated(credential: LocalFileSystemCredential) { + // TODO: Show Progress-HUD + } + + // MARK: - LocalVaultAdding + + func validationFailed(with error: Error, at viewController: UIViewController) { + // TODO: Disable Progress-HUD + handleError(error, for: viewController) + } + + func showPasswordScreen(for result: LocalFileSystemAuthenticationResult) { + parentCoordinator?.startAuthenticatedLocalFileSystemOpenExistingVaultFlow(credential: result.credential, account: result.account, item: result.item) + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultViewModel.swift new file mode 100644 index 000000000..5036df854 --- /dev/null +++ b/Cryptomator/AddVault/OpenExistingVault/LocalVault/OpenExistingLocalVaultViewModel.swift @@ -0,0 +1,46 @@ +// +// OpenExistingLocalVaultViewModel.swift +// Cryptomator +// +// Created by Philipp Schmid on 23.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import Promises + +class OpenExistingLocalVaultViewModel: LocalFileSystemAuthenticationViewModel, LocalFileSystemVaultInstallingViewModelProtocol { + private let validator: OpenExistingLocalVaultValidationLogic + init(accountManager: CloudProviderAccountManager = CloudProviderAccountDBManager.shared) { + let documentPickerButtonText = NSLocalizedString("localFileSystemAuthentication.openExistingVault.button", comment: "") + let headerText = NSLocalizedString("localFileSystemAuthentication.openExistingVault.header", comment: "") + self.validator = OpenExistingLocalVaultValidationLogic() + super.init(documentPickerButtonText: documentPickerButtonText, headerText: headerText, validationLogic: validator, accountManager: accountManager) + } + + func addVault(for credential: LocalFileSystemCredential) -> Promise { + return validateAndSave(credential: credential).then { account -> LocalFileSystemAuthenticationResult in + guard let detectedVaultItem = self.validator.vaultItem else { + throw OpenExistingLocalVaultViewModelError.noVaultFound + } + let vaultItem = VaultDetailItem(name: credential.rootURL.lastPathComponent, vaultPath: detectedVaultItem.vaultPath, isLegacyVault: detectedVaultItem.isLegacyVault) + return LocalFileSystemAuthenticationResult(credential: credential, account: account, item: vaultItem) + } + } +} + +private class OpenExistingLocalVaultValidationLogic: LocalFileSystemAuthenticationValidationLogic { + var vaultItem: VaultDetailItem? + func validate(items: [CloudItemMetadata]) throws { + guard let vaultItem = VaultDetector.getVaultItem(items: items, parentCloudPath: CloudPath("/")) else { + throw OpenExistingLocalVaultViewModelError.noVaultFound + } + self.vaultItem = vaultItem + } +} + +enum OpenExistingLocalVaultViewModelError: Error { + case noVaultFound +} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift index 62fcb4ffa..a8c2659f9 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift @@ -10,16 +10,15 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation import Promises + class OpenExistingLegacyVaultPasswordViewModel: OpenExistingVaultPasswordViewModelProtocol { var password: String? let provider: CloudProvider let account: CloudProviderAccount - // later: localMasterkeyURL: URL instead of masterkeyPath: CloudPath - let masterkeyPath: CloudPath + let vault: VaultItem var vaultName: String { - let masterkeyParentPath = masterkeyPath.deletingLastPathComponent() - return masterkeyParentPath.lastPathComponent + return vault.name } var footerTitle: String { @@ -29,10 +28,10 @@ class OpenExistingLegacyVaultPasswordViewModel: OpenExistingVaultPasswordViewMod private let localMasterkeyURL: URL let vaultUID: String - init(provider: CloudProvider, account: CloudProviderAccount, masterkeyPath: CloudPath, vaultID: String) { + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultID: String) { self.provider = provider self.account = account - self.masterkeyPath = masterkeyPath + self.vault = vault let tmpDirURL = FileManager.default.temporaryDirectory self.localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) self.vaultUID = vaultID @@ -42,6 +41,6 @@ class OpenExistingLegacyVaultPasswordViewModel: OpenExistingVaultPasswordViewMod guard let password = password else { return Promise(MasterkeyProcessingViewModelError.noPasswordSet) } - return VaultDBManager.shared.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, masterkeyPath: masterkeyPath, password: password, storePasswordInKeychain: true) + return VaultDBManager.shared.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: true) } } diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift index 9d4707e1c..f15c95519 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift @@ -7,36 +7,22 @@ // import CryptomatorCloudAccessCore +import CryptomatorCommonCore import UIKit class OpenExistingVaultChooseFolderViewController: ChooseFolderViewController { - private var vault: Item? + private var vault: VaultDetailItem? override func viewDidLoad() { super.viewDidLoad() title = NSLocalizedString("addVault.openExistingVault.title", comment: "") + tableView.register(ButtonCell.self, forCellReuseIdentifier: "ButtonCell") } - override func showDetectedVault(_ vault: Item) { + override func showDetectedVault(_ vault: VaultDetailItem) { + tableView.reloadData() self.vault = vault - let successView = SuccessView(viewModel: DetectedMasterkeyViewModel(masterkeyPath: vault.path)) - let containerView = UIView() - successView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(successView) - NSLayoutConstraint.activate([ - successView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), - successView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - successView.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor), - successView.trailingAnchor.constraint(equalTo: containerView.readableContentGuide.trailingAnchor) - ]) - - // Prevents the view from being placed under the navigation bar - tableView.backgroundView = containerView - tableView.contentInsetAdjustmentBehavior = .never - tableView.separatorStyle = .none - - let addVaultButton = UIBarButtonItem(title: NSLocalizedString("common.button.add", comment: ""), style: .done, target: self, action: #selector(addVault)) - navigationItem.rightBarButtonItem = addVaultButton + refreshControl = nil navigationController?.setToolbarHidden(true, animated: true) } @@ -46,12 +32,86 @@ class OpenExistingVaultChooseFolderViewController: ChooseFolderViewController { } coordinator?.chooseItem(vault) } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewModel.foundMasterkey { + return 1 + } else { + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if viewModel.foundMasterkey { + // swiftlint:disable:next force_cast + let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCell", for: indexPath) as! ButtonCell + cell.button.setTitle(NSLocalizedString("addVault.openExistingVault.detectedMasterkey.add", comment: ""), for: .normal) + cell.button.addTarget(self, action: #selector(addVault), for: .touchUpInside) + return cell + } else { + return super.tableView(tableView, cellForRowAt: indexPath) + } + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + if viewModel.foundMasterkey, let vault = vault { + return SuccessView(vaultName: vault.name) + } else { + return super.tableView(tableView, viewForHeaderInSection: section) + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if viewModel.foundMasterkey { + // do nothing + } else { + super.tableView(tableView, didSelectRowAt: indexPath) + } + } } private class SuccessView: DetectedVaultView { - init(viewModel: DetectedMasterkeyViewModel) { + init(vaultName: String) { let botVaultImage = UIImage(named: "bot-vault") let imageView = UIImageView(image: botVaultImage) - super.init(imageView: imageView, text: viewModel.text) + let text = String(format: NSLocalizedString("addVault.openExistingVault.detectedMasterkey.text", comment: ""), vaultName) + super.init(imageView: imageView, text: text) + } +} + +#if DEBUG +import SwiftUI + +private class OpenExistingVaultChooseFolderViewModelMock: ChooseFolderViewModelProtocol { + var headerTitle: String = "/Vault" + + let foundMasterkey = true + let canCreateFolder: Bool + let cloudPath: CloudPath + let items: [CloudItemMetadata] = [] + + init(cloudPath: CloudPath, canCreateFolder: Bool) { + self.canCreateFolder = canCreateFolder + self.cloudPath = cloudPath + } + + func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) { + onChange() + } + + func refreshItems() {} +} + +struct OpenExistingVaultChooseFolderVCPreview: PreviewProvider { + static var previews: some View { + let viewController = OpenExistingVaultChooseFolderViewController(with: OpenExistingVaultChooseFolderViewModelMock(cloudPath: CloudPath("/Vault"), canCreateFolder: false)) + let item = VaultDetailItem(name: "vault", vaultPath: CloudPath("/Vault/masterkey.cryptomator"), isLegacyVault: false) + viewController.showDetectedVault(item) + return viewController.toPreview() } } +#endif diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index 3526d7ca2..d7eb8f229 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation @@ -30,16 +31,13 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, Coordinator { func showAccountList(for cloudProviderType: CloudProviderType) { if cloudProviderType == .localFileSystem { - let alertController = UIAlertController(title: "Info", message: NSLocalizedString("testFlight.otherFileProviders.alert.text", comment: ""), preferredStyle: .alert) - let okAction = UIAlertAction(title: NSLocalizedString("common.button.ok", comment: ""), style: .default) - alertController.addAction(okAction) - navigationController.present(alertController, animated: true, completion: nil) - return + startLocalFileSystemAuthenticationFlow() + } else { + let viewModel = AccountListViewModel(with: cloudProviderType) + let accountListVC = AccountListViewController(with: viewModel) + accountListVC.coordinator = self + navigationController.pushViewController(accountListVC, animated: true) } - let viewModel = AccountListViewModel(with: cloudProviderType) - let accountListVC = AccountListViewController(with: viewModel) - accountListVC.coordinator = self - navigationController.pushViewController(accountListVC, animated: true) } func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { @@ -68,6 +66,23 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, Coordinator { navigationController.dismiss(animated: true) parentCoordinator?.close() } + + // MARK: - LocalFileSystemProvider Flow + + private func startLocalFileSystemAuthenticationFlow() { + let child = OpenExistingLocalVaultCoordinator(navigationController: navigationController) + childCoordinators.append(child) + child.parentCoordinator = self + child.start() + } + + func startAuthenticatedLocalFileSystemOpenExistingVaultFlow(credential: LocalFileSystemCredential, account: CloudProviderAccount, item: Item) { + let provider = LocalFileSystemProvider(rootURL: credential.rootURL) + let child = AuthenticatedOpenExistingVaultCoordinator(navigationController: navigationController, provider: provider, account: account) + childCoordinators.append(child) + child.parentCoordinator = self + child.chooseItem(item) + } } private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, FolderChoosing, Coordinator { @@ -103,15 +118,16 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder func chooseItem(_ item: Item) { let viewModel: OpenExistingVaultPasswordViewModelProtocol - switch item.type { - case .vaultConfig: - viewModel = OpenExistingVaultPasswordViewModel(provider: provider, account: account, vaultConfigPath: item.path, vaultUID: UUID().uuidString) - case .legacyMasterkey: - viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, account: account, masterkeyPath: item.path, vaultID: UUID().uuidString) - default: + guard let vaultItem = item as? VaultDetailItem else { handleError(VaultCoordinatorError.wrongItemType, for: navigationController) return } + if vaultItem.isLegacyVault { + viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultID: UUID().uuidString) + } else { + viewModel = OpenExistingVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultUID: UUID().uuidString) + } + let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) passwordVC.coordinator = self navigationController.pushViewController(passwordVC, animated: true) @@ -119,6 +135,13 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder func showCreateNewFolder(parentPath: CloudPath) {} + func handleError(error: Error) { + navigationController.popViewController(animated: true) + if let topViewController = navigationController.topViewController { + handleError(error, for: topViewController) + } + } + // MARK: - VaultInstalling func showSuccessfullyAddedVault(withName name: String, vaultUID: String) { diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift index d7182f90d..ac970f312 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift @@ -25,11 +25,9 @@ class OpenExistingVaultPasswordViewModel: OpenExistingVaultPasswordViewModelProt let provider: CloudProvider let account: CloudProviderAccount - // later: localMasterkeyURL: URL instead of masterkeyPath: CloudPath - let vaultConfigPath: CloudPath + let vault: VaultItem var vaultName: String { - let vaultConfigParentPath = vaultConfigPath.deletingLastPathComponent() - return vaultConfigParentPath.lastPathComponent + return vault.name } var footerTitle: String { @@ -38,10 +36,10 @@ class OpenExistingVaultPasswordViewModel: OpenExistingVaultPasswordViewModelProt let vaultUID: String - init(provider: CloudProvider, account: CloudProviderAccount, vaultConfigPath: CloudPath, vaultUID: String) { + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String) { self.provider = provider self.account = account - self.vaultConfigPath = vaultConfigPath + self.vault = vault self.vaultUID = vaultUID } @@ -49,7 +47,7 @@ class OpenExistingVaultPasswordViewModel: OpenExistingVaultPasswordViewModelProt guard let password = password else { return Promise(MasterkeyProcessingViewModelError.noPasswordSet) } - return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultConfigPath: vaultConfigPath, password: password, storePasswordInKeychain: true) + return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: true) } } diff --git a/Cryptomator/AddVault/VaultCoordinatorError.swift b/Cryptomator/AddVault/VaultCoordinatorError.swift index 0799874f6..2b523042d 100644 --- a/Cryptomator/AddVault/VaultCoordinatorError.swift +++ b/Cryptomator/AddVault/VaultCoordinatorError.swift @@ -7,6 +7,7 @@ // import Foundation + enum VaultCoordinatorError: Error { case wrongItemType } diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 5eb5857d6..24b2f04ec 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -62,13 +62,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DDLogError("Error while setting up OneDrive: \(error)") } - // Application-wide styling - UINavigationBar.appearance().barTintColor = UIColor(named: "primary") - UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white] - UIBarButtonItem.appearance().tintColor = UIColor.white - // Create window - let navigationController = UINavigationController() + let navigationController = BaseNavigationController() coordinator = MainCoordinator(navigationController: navigationController) coordinator?.start() window = UIWindow(frame: UIScreen.main.bounds) diff --git a/Cryptomator/Common/BaseNavigationController.swift b/Cryptomator/Common/BaseNavigationController.swift new file mode 100644 index 000000000..291f694c8 --- /dev/null +++ b/Cryptomator/Common/BaseNavigationController.swift @@ -0,0 +1,18 @@ +// +// BaseNavigationController.swift +// Cryptomator +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class BaseNavigationController: UINavigationController { + override func viewDidLoad() { + super.viewDidLoad() + navigationBar.tintColor = .white + navigationBar.barTintColor = UIColor(named: "primary") + navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] + } +} diff --git a/Cryptomator/Common/ChildCoordinator.swift b/Cryptomator/Common/ChildCoordinator.swift index 78c28761b..174634cac 100644 --- a/Cryptomator/Common/ChildCoordinator.swift +++ b/Cryptomator/Common/ChildCoordinator.swift @@ -7,6 +7,7 @@ // import Foundation + protocol ChildCoordinator: Coordinator { func stop() var parentCoordinator: Coordinator? { get set } diff --git a/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift b/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift index ef07c729b..6d8938e42 100644 --- a/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift +++ b/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import CryptomatorCommonCore import UIKit class ChooseFolderViewController: SingleSectionTableViewController { @@ -18,7 +19,7 @@ class ChooseFolderViewController: SingleSectionTableViewController { }() private lazy var searchController: UISearchController = { - return UISearchController(searchResultsController: self) + return UISearchController() }() init(with viewModel: ChooseFolderViewModelProtocol) { @@ -40,9 +41,7 @@ class ChooseFolderViewController: SingleSectionTableViewController { guard let self = self else { return } self.coordinator?.handleError(error, for: self) } onChange: { [weak self] in - guard let self = self else { return } - self.refreshControl?.endRefreshing() - self.tableView.reloadData() + self?.onItemsChange() } onVaultDetection: { [weak self] vault in guard let self = self else { return } self.tableView.reloadData() @@ -52,7 +51,7 @@ class ChooseFolderViewController: SingleSectionTableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let isToolbarHidden = navigationController?.isToolbarHidden, isToolbarHidden { + if let isToolbarHidden = navigationController?.isToolbarHidden, isToolbarHidden, !viewModel.foundMasterkey { navigationController?.setToolbarHidden(false, animated: animated) } } @@ -64,10 +63,15 @@ class ChooseFolderViewController: SingleSectionTableViewController { } } - func showDetectedVault(_ vault: Item) { + func showDetectedVault(_ vault: VaultDetailItem) { fatalError("not implemented") } + func onItemsChange() { + refreshControl?.endRefreshing() + tableView.reloadData() + } + @objc func cancel() { coordinator?.close() } @@ -114,13 +118,37 @@ class ChooseFolderViewController: SingleSectionTableViewController { return cell } + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + let itemsLoading = refreshControl?.isRefreshing ?? true + if !itemsLoading, viewModel.items.isEmpty { + return NSLocalizedString("chooseFolder.emptyFolder.footer", comment: "") + } else { + return nil + } + } + // MARK: - UITableViewDelegate - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + /* override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + if viewModel.foundMasterkey { + return nil + } + return header + } */ + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { if viewModel.foundMasterkey { return nil } - return header + return viewModel.headerTitle + } + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + // Prevents the header title from being displayed in uppercase + guard let headerView = view as? UITableViewHeaderFooterView else { + return + } + headerView.textLabel?.text = viewModel.headerTitle } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -151,22 +179,25 @@ private class HeaderWithSearchbar: UITableViewHeaderFooterView { NSLayoutConstraint.activate([ searchBar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - searchBar.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), searchBar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + searchBar.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), self.title.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - self.title.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), + self.title.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), self.title.topAnchor.constraint(equalTo: searchBar.bottomAnchor), - self.title.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor) + self.title.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor) ]) } } #if DEBUG -import CryptomatorCloudAccess import SwiftUI private class ChooseFolderViewModelMock: ChooseFolderViewModelProtocol { + var headerTitle: String { + cloudPath.path + } + let foundMasterkey = false let canCreateFolder: Bool let cloudPath: CloudPath @@ -180,7 +211,7 @@ private class ChooseFolderViewModelMock: ChooseFolderViewModelProtocol { self.cloudPath = cloudPath } - func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (Item) -> Void) { + func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) { onChange() } diff --git a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift index 7420e1b14..569b2022e 100644 --- a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift +++ b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift @@ -9,33 +9,32 @@ import CocoaLumberjack import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import CryptomatorCommonCore import Foundation protocol ChooseFolderViewModelProtocol { var canCreateFolder: Bool { get } var cloudPath: CloudPath { get } var foundMasterkey: Bool { get } + var headerTitle: String { get } var items: [CloudItemMetadata] { get } - func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (Item) -> Void) + func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) func refreshItems() } -extension ChooseFolderViewModelProtocol { - var headerTitle: String { - return cloudPath.path - } -} - class ChooseFolderViewModel: ChooseFolderViewModelProtocol { var canCreateFolder: Bool var cloudPath: CloudPath var items = [CloudItemMetadata]() var foundMasterkey = false + var headerTitle: String { + return cloudPath.path + } private let provider: CloudProvider private var errorListener: ((Error) -> Void)? private var changeListener: (() -> Void)? - private var vaultListener: ((Item) -> Void)? + private var vaultListener: ((VaultDetailItem) -> Void)? init(canCreateFolder: Bool, cloudPath: CloudPath, provider: CloudProvider) { self.canCreateFolder = canCreateFolder @@ -43,7 +42,7 @@ class ChooseFolderViewModel: ChooseFolderViewModelProtocol { self.provider = provider } - func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (Item) -> Void) { + func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) { errorListener = onError changeListener = onChange vaultListener = onVaultDetection @@ -52,44 +51,16 @@ class ChooseFolderViewModel: ChooseFolderViewModelProtocol { func refreshItems() { provider.fetchItemListExhaustively(forFolderAt: cloudPath).then { itemList in - if let vaultItem = self.getVaultItem(items: itemList.items) { + if let vaultItem = VaultDetector.getVaultItem(items: itemList.items, parentCloudPath: self.cloudPath) { self.foundMasterkey = true self.vaultListener?(vaultItem) } else { self.foundMasterkey = false - self.items = itemList.items + self.items = itemList.items.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } self.changeListener?() } }.catch { error in self.errorListener?(error) } } - - func getVaultItem(items: [CloudItemMetadata]) -> Item? { - if let vaultConfigPath = getVaultConfigCloudPath(items: items) { - return Item(type: .vaultConfig, path: vaultConfigPath) - } else if let legacyMasterkeyPath = getLegacyMasterkeyPath(items: items) { - return Item(type: .legacyMasterkey, path: legacyMasterkeyPath) - } else { - return nil - } - } - - func getVaultConfigCloudPath(items: [CloudItemMetadata]) -> CloudPath? { - let vaultConfigItem = items.first(where: { $0.name == "vault.cryptomator" && $0.itemType == .file }) - guard items.contains(where: { $0.name == "d" && $0.itemType == .folder }) else { - DDLogDebug("Missing d folder") - return nil - } - return vaultConfigItem?.cloudPath - } - - func getLegacyMasterkeyPath(items: [CloudItemMetadata]) -> CloudPath? { - let masterkeyItem = items.first(where: { $0.name == "masterkey.cryptomator" && $0.itemType == .file }) - guard items.contains(where: { $0.name == "d" && $0.itemType == .folder }) else { - DDLogDebug("Missing d folder") - return nil - } - return masterkeyItem?.cloudPath - } } diff --git a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift index 2b31fba6e..4eff4dd59 100644 --- a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift +++ b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift @@ -7,6 +7,7 @@ // import UIKit + class CreateNewFolderViewController: SingleSectionHeaderTableViewController { weak var coordinator: (FolderCreating & Coordinator)? private var viewModel: CreateNewFolderViewModelProtocol @@ -73,6 +74,7 @@ class CreateNewFolderViewController: SingleSectionHeaderTableViewController { import CryptomatorCloudAccessCore import Promises import SwiftUI + private class CreateNewFolderViewModelMock: CreateNewFolderViewModelProtocol { var folderName: String? diff --git a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift index bf888d2e6..e923f6f61 100644 --- a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift +++ b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import Foundation import Promises + protocol CreateNewFolderViewModelProtocol: SingleSectionHeaderTableViewModelProtocol { var folderName: String? { get set } func createFolder() -> Promise diff --git a/Cryptomator/Common/ChooseFolder/FolderChoosing.swift b/Cryptomator/Common/ChooseFolder/FolderChoosing.swift index d74b9ba33..d85b98c04 100644 --- a/Cryptomator/Common/ChooseFolder/FolderChoosing.swift +++ b/Cryptomator/Common/ChooseFolder/FolderChoosing.swift @@ -13,15 +13,13 @@ protocol FolderChoosing: AnyObject { func close() func chooseItem(_ item: Item) func showCreateNewFolder(parentPath: CloudPath) + func handleError(error: Error) } -struct Item { - let type: ItemType - let path: CloudPath +protocol Item { + var path: CloudPath { get } } -enum ItemType { - case folder - case vaultConfig - case legacyMasterkey +struct Folder: Item { + let path: CloudPath } diff --git a/Cryptomator/Common/ChooseFolder/FolderCreating.swift b/Cryptomator/Common/ChooseFolder/FolderCreating.swift index 800d98446..cb49747c5 100644 --- a/Cryptomator/Common/ChooseFolder/FolderCreating.swift +++ b/Cryptomator/Common/ChooseFolder/FolderCreating.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import Foundation + protocol FolderCreating: AnyObject { func createdNewFolder(at folderPath: CloudPath) func stop() diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index 0ee51566d..00cfdb29e 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift @@ -119,10 +119,10 @@ class AccountListViewController: SingleSectionTableViewController { } #if DEBUG -import CryptomatorCloudAccess import CryptomatorCommonCore import Promises import SwiftUI + private class AccountListViewModelMock: AccountListViewModelProtocol { let cloudProviderType = CloudProviderType.googleDrive diff --git a/Cryptomator/Common/DatabaseManager.swift b/Cryptomator/Common/DatabaseManager.swift index 1d59bd9c7..e2953309c 100644 --- a/Cryptomator/Common/DatabaseManager.swift +++ b/Cryptomator/Common/DatabaseManager.swift @@ -17,67 +17,6 @@ class DatabaseManager { init(dbPool: DatabasePool) throws { self.dbPool = dbPool - try DatabaseManager.migrator.migrate(dbPool) - } - - private static var migrator: DatabaseMigrator { - var migrator = DatabaseMigrator() - migrator.registerMigration("main-v1") { db in - try db.create(table: "vaultListPosition") { table in - table.column("position", .integer).unique() - table.column("vaultUID", .text).unique().notNull().references("vaultAccounts", onDelete: .cascade) - table.check(Column("position") != nil) - } - try db.execute(sql: """ - CREATE TRIGGER position_creation - AFTER INSERT - ON vaultAccounts - BEGIN - INSERT INTO vaultListPosition (position, vaultUID) - VALUES (IFNULL((SELECT MAX(position) FROM vaultListPosition), -1)+1, NEW.vaultUID); - END; - """) - - try db.execute(sql: """ - CREATE TRIGGER position_update - AFTER DELETE - ON vaultListPosition - BEGIN - UPDATE vaultListPosition - SET position = position - 1 - WHERE position > OLD.position; - END; - """) - - try db.create(table: "accountListPosition") { table in - table.column("position", .integer) - table.column("cloudProviderType", .text) - table.column("accountUID", .text).unique().notNull().references("cloudProviderAccounts", onDelete: .cascade) - table.uniqueKey(["position", "cloudProviderType"]) - table.check(Column("position") != nil && Column("cloudProviderType") != nil) - } - try db.execute(sql: """ - CREATE TRIGGER accountList_position_creation - AFTER INSERT - ON cloudProviderAccounts - BEGIN - INSERT INTO accountListPosition (position, cloudProviderType, accountUID) - VALUES (IFNULL((SELECT MAX(position) FROM accountListPosition WHERE cloudProviderType = NEW.cloudProviderType), -1)+1, NEW.cloudProviderType, NEW.accountUID); - END; - """) - - try db.execute(sql: """ - CREATE TRIGGER accountList_position_update - AFTER DELETE - ON accountListPosition - BEGIN - UPDATE accountListPosition - SET position = position - 1 - WHERE position > OLD.position AND cloudProviderType = OLD.cloudProviderType; - END; - """) - } - return migrator } func getAllVaults() throws -> [VaultInfo] { diff --git a/Cryptomator/Common/FilesAppUtil.swift b/Cryptomator/Common/FilesAppUtil.swift new file mode 100644 index 000000000..2149b45e7 --- /dev/null +++ b/Cryptomator/Common/FilesAppUtil.swift @@ -0,0 +1,33 @@ +// +// FilesAppUtil.swift +// Cryptomator +// +// Created by Tobias Hagemann on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjack +import CocoaLumberjackSwift +import CryptomatorCommonCore +import UIKit + +public enum FilesAppUtil { + public static func showFilesApp(forVaultUID vaultUID: String) { + guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) else { + DDLogDebug("containerURL is nil") + return + } + let url = containerURL.appendingPathComponent("File Provider Storage").appendingPathComponent(vaultUID) + guard let sharedDocumentsURL = FilesAppUtil.changeSchemeToSharedDocuments(for: url) else { + DDLogDebug("Conversion to shared documents url failed") + return + } + UIApplication.shared.open(sharedDocumentsURL) + } + + private static func changeSchemeToSharedDocuments(for url: URL) -> URL? { + var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) + comps?.scheme = "shareddocuments" + return comps?.url + } +} diff --git a/Cryptomator/Common/VaultDetailItem.swift b/Cryptomator/Common/VaultDetailItem.swift new file mode 100644 index 000000000..8f557daa2 --- /dev/null +++ b/Cryptomator/Common/VaultDetailItem.swift @@ -0,0 +1,21 @@ +// +// VaultDetailItem.swift +// Cryptomator +// +// Created by Philipp Schmid on 24.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +struct VaultDetailItem: Item, VaultItem { + var path: CloudPath { + return vaultPath + } + + let name: String + let vaultPath: CloudPath + let isLegacyVault: Bool +} diff --git a/Cryptomator/Common/VaultDetector.swift b/Cryptomator/Common/VaultDetector.swift new file mode 100644 index 000000000..80c8961a3 --- /dev/null +++ b/Cryptomator/Common/VaultDetector.swift @@ -0,0 +1,48 @@ +// +// VaultDetector.swift +// Cryptomator +// +// Created by Philipp Schmid on 28.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCloudAccessCore +import Foundation + +class VaultDetector { + class func getVaultItem(items: [CloudItemMetadata], parentCloudPath: CloudPath) -> VaultDetailItem? { + if let vaultConfigPath = getVaultConfigCloudPath(items: items) { + let vaultName = getVaultName(for: vaultConfigPath) + return VaultDetailItem(name: vaultName, vaultPath: parentCloudPath, isLegacyVault: false) + } else if let legacyMasterkeyPath = getLegacyMasterkeyPath(items: items) { + let vaultName = getVaultName(for: legacyMasterkeyPath) + return VaultDetailItem(name: vaultName, vaultPath: parentCloudPath, isLegacyVault: true) + } else { + return nil + } + } + + class func getVaultConfigCloudPath(items: [CloudItemMetadata]) -> CloudPath? { + let vaultConfigItem = items.first(where: { $0.name == "vault.cryptomator" && $0.itemType == .file }) + guard items.contains(where: { $0.name == "d" && $0.itemType == .folder }) else { + DDLogDebug("Missing d folder") + return nil + } + return vaultConfigItem?.cloudPath + } + + class func getLegacyMasterkeyPath(items: [CloudItemMetadata]) -> CloudPath? { + let masterkeyItem = items.first(where: { $0.name == "masterkey.cryptomator" && $0.itemType == .file }) + guard items.contains(where: { $0.name == "d" && $0.itemType == .folder }) else { + DDLogDebug("Missing d folder") + return nil + } + return masterkeyItem?.cloudPath + } + + class func getVaultName(for cryptomatorFilePath: CloudPath) -> String { + let parentPath = cryptomatorFilePath.deletingLastPathComponent() + return parentPath.lastPathComponent + } +} diff --git a/Cryptomator/Info.plist b/Cryptomator/Info.plist index 9fbadfb02..f2af1529c 100644 --- a/Cryptomator/Info.plist +++ b/Cryptomator/Info.plist @@ -18,6 +18,16 @@ $(MARKETING_VERSION) CFBundleURLTypes + + CFBundleTypeRole + Editor + CFBundleURLName + org.cryptomator.ios + CFBundleURLSchemes + + cryptomator + + CFBundleURLName diff --git a/Cryptomator/MainCoordinator.swift b/Cryptomator/MainCoordinator.swift index 80b10a1bf..329db03ee 100644 --- a/Cryptomator/MainCoordinator.swift +++ b/Cryptomator/MainCoordinator.swift @@ -24,7 +24,7 @@ class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate { } func addVault() { - let modalNavigationController = UINavigationController() + let modalNavigationController = BaseNavigationController() let child = AddVaultCoordinator(navigationController: modalNavigationController) child.parentCoordinator = self childCoordinators.append(child) @@ -33,7 +33,7 @@ class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate { } func showSettings() { - let modalNavigationController = UINavigationController() + let modalNavigationController = BaseNavigationController() let child = SettingsCoordinator(navigationController: modalNavigationController) child.parentCoordinator = self childCoordinators.append(child) diff --git a/Cryptomator/VaultList/EmptyListMessage.swift b/Cryptomator/VaultList/EmptyListMessage.swift index 05e441204..7124dc7fb 100644 --- a/Cryptomator/VaultList/EmptyListMessage.swift +++ b/Cryptomator/VaultList/EmptyListMessage.swift @@ -78,7 +78,6 @@ class EmptyListMessage: UIView { } #if DEBUG -import CryptomatorCloudAccess import SwiftUI struct EmptyListMessagePreview: PreviewProvider { diff --git a/Cryptomator/VaultList/VaultCell.swift b/Cryptomator/VaultList/VaultCell.swift index 709d8f5f7..92715668c 100644 --- a/Cryptomator/VaultList/VaultCell.swift +++ b/Cryptomator/VaultList/VaultCell.swift @@ -25,7 +25,7 @@ class VaultCell: UITableViewCell { func configure(with vault: VaultInfo) { imageView?.image = UIImage(vaultIconFor: vault.cloudProviderType, state: .normal) imageView?.highlightedImage = UIImage(vaultIconFor: vault.cloudProviderType, state: .highlighted) - textLabel?.text = vault.vaultPath.lastPathComponent + textLabel?.text = vault.vaultName detailTextLabel?.text = vault.vaultPath.path detailTextLabel?.textColor = UIColor(named: "secondaryLabel") } @@ -41,7 +41,7 @@ class VaultCell: UITableViewCell { } else { content.image = UIImage(vaultIconFor: vault.cloudProviderType, state: .normal) } - content.text = vault.vaultPath.lastPathComponent + content.text = vault.vaultName content.secondaryText = vault.vaultPath.path content.secondaryTextProperties.color = .secondaryLabel contentConfiguration = content diff --git a/Cryptomator/VaultList/VaultInfo.swift b/Cryptomator/VaultList/VaultInfo.swift index 068f735e3..5c5edd967 100644 --- a/Cryptomator/VaultList/VaultInfo.swift +++ b/Cryptomator/VaultList/VaultInfo.swift @@ -26,6 +26,10 @@ public struct VaultInfo: Decodable, FetchableRecord { return vaultAccount.vaultPath } + var vaultName: String { + return vaultAccount.vaultName + } + var vaultUID: String { return vaultAccount.vaultUID } diff --git a/Cryptomator/VaultList/VaultListViewController.swift b/Cryptomator/VaultList/VaultListViewController.swift index a0a7d0038..2518a2573 100644 --- a/Cryptomator/VaultList/VaultListViewController.swift +++ b/Cryptomator/VaultList/VaultListViewController.swift @@ -135,6 +135,11 @@ class VaultListViewController: UITableViewController { } return header } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + FilesAppUtil.showFilesApp(forVaultUID: viewModel.vaults[indexPath.row].vaultUID) + } } #if DEBUG @@ -143,8 +148,8 @@ import SwiftUI private class VaultListViewModelMock: VaultListViewModelProtocol { let vaults = [ - VaultInfo(vaultAccount: VaultAccount(vaultUID: "1", delegateAccountUID: "1", vaultPath: CloudPath("/Work")), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV), vaultListPosition: VaultListPosition(position: 1, vaultUID: "1")), - VaultInfo(vaultAccount: VaultAccount(vaultUID: "2", delegateAccountUID: "2", vaultPath: CloudPath("/Family")), cloudProviderAccount: CloudProviderAccount(accountUID: "2", cloudProviderType: .googleDrive), vaultListPosition: VaultListPosition(position: 2, vaultUID: "2")) + VaultInfo(vaultAccount: VaultAccount(vaultUID: "1", delegateAccountUID: "1", vaultPath: CloudPath("/Work"), vaultName: "Work"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV), vaultListPosition: VaultListPosition(position: 1, vaultUID: "1")), + VaultInfo(vaultAccount: VaultAccount(vaultUID: "2", delegateAccountUID: "2", vaultPath: CloudPath("/Family"), vaultName: "Family"), cloudProviderAccount: CloudProviderAccount(accountUID: "2", cloudProviderType: .googleDrive), vaultListPosition: VaultListPosition(position: 2, vaultUID: "2")) ] func refreshItems() throws {} diff --git a/Cryptomator/VaultList/VaultListViewModel.swift b/Cryptomator/VaultList/VaultListViewModel.swift index ac06939ae..085a0feaa 100644 --- a/Cryptomator/VaultList/VaultListViewModel.swift +++ b/Cryptomator/VaultList/VaultListViewModel.swift @@ -11,6 +11,7 @@ import CocoaLumberjackSwift import CryptomatorCommonCore import Foundation import GRDB + class VaultListViewModel: VaultListViewModelProtocol { var vaults = [VaultInfo]() diff --git a/Cryptomator/WebDAV/WebDAVAuthenticator+VC.swift b/Cryptomator/WebDAV/WebDAVAuthenticator+VC.swift index 6d6a20d57..cc4a0ad46 100644 --- a/Cryptomator/WebDAV/WebDAVAuthenticator+VC.swift +++ b/Cryptomator/WebDAV/WebDAVAuthenticator+VC.swift @@ -14,7 +14,7 @@ extension WebDAVAuthenticator { private static var coordinator: WebDAVAuthenticationCoordinator? static func authenticate(from viewController: UIViewController) -> Promise { - let navigationController = UINavigationController() + let navigationController = BaseNavigationController() let webDAVCoordinator = WebDAVAuthenticationCoordinator(navigationController: navigationController) coordinator = webDAVCoordinator viewController.present(navigationController, animated: true) diff --git a/Cryptomator/de.lproj/Localizable.strings b/Cryptomator/de.lproj/Localizable.strings index f60ab0a49..d4115030f 100644 --- a/Cryptomator/de.lproj/Localizable.strings +++ b/Cryptomator/de.lproj/Localizable.strings @@ -7,7 +7,6 @@ */ "common.alert.error.title" = "Fehler"; -"common.button.add" = "Hinzufügen"; "common.button.cancel" = "Abbrechen"; "common.button.choose" = "Auswählen"; "common.button.confirm" = "Bestätigen"; @@ -28,16 +27,21 @@ "addVault.createNewVault.title" = "Neuen Tresor erstellen"; "addVault.createNewVault.chooseCloud.header" = "Wo soll Cryptomator die verschlüsselten Dateien deines Tresors speichern?"; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator hat an diesem Ort einen bestehenden Tresor erkannt.\nUm einen neuen Tresor zu erstellen, gehe bitte zurück und wähle einen anderen Ordner aus."; +"addVault.createNewVault.enterPassword.header" = "Gib ein neues Passwort ein."; +"addVault.createNewVault.confirmPassword.header" = "Bestätige das neue Passwort."; "addVault.createNewVault.alert.confirmPassword.title" = "Passwort bestätigen?"; "addVault.createNewVault.alert.confirmPassword.message" = "WICHTIG: Wenn du dein Passwort vergisst, gibt es keine Möglichkeit, deine Daten wiederherzustellen."; "addVault.openExistingVault.title" = "Existierenden Tresor öffnen"; "addVault.openExistingVault.chooseCloud.header" = "Wo befindet sich der Tresor?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator hat den Tresor „%@“ erkannt.\nMöchtest du den Tresor hinzufügen?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Diesen Tresor hinzufügen"; "addVault.openExistingVault.password.footer" = "Gib das Passwort für „%@“ ein"; "addVault.success.info" = "Tresor „%@“ erfolgreich hinzugefügt.\nGreife auf diesen Tresor über die Dateien-App zu."; "addVault.success.openFilesApp" = "Dateien-App öffnen"; "addVault.success.footer" = "Falls du es noch nicht getan hast, aktiviere Cryptomator in der Dateien-App."; +"chooseFolder.emptyFolder.footer" = "Ordner ist leer"; + "settings.title" = "Einstellungen"; "settings.aboutCryptomator" = "Über Cryptomator"; "settings.aboutCryptomator.title" = "Version %@ (%@)"; @@ -61,4 +65,7 @@ "createNewFolder.header.title" = "Wähle einen Namen für den Ordner."; "createNewFolder.cells.name" = "Ordnername"; -"testFlight.otherFileProviders.alert.text" = "Andere Datei-Anbieter sind derzeit nicht verfügbar. Diese Funktion folgt bald in einem Update!"; +"localFileSystemAuthentication.createNewVault.header" = "Wähle im nächsten Schritt den Speicherort für deinen neuen Tresor aus."; +"localFileSystemAuthentication.createNewVault.button" = "Speicherort auswählen"; +"localFileSystemAuthentication.openExistingVault.header" = "Wähle im nächsten Schritt den Ordner deines vorhandenen Tresors aus."; +"localFileSystemAuthentication.openExistingVault.button" = "Tresorordner auswählen"; diff --git a/Cryptomator/en.lproj/Localizable.strings b/Cryptomator/en.lproj/Localizable.strings index f1d103eb6..07c6fa3c6 100644 --- a/Cryptomator/en.lproj/Localizable.strings +++ b/Cryptomator/en.lproj/Localizable.strings @@ -7,7 +7,6 @@ */ "common.alert.error.title" = "Error"; -"common.button.add" = "Add"; "common.button.cancel" = "Cancel"; "common.button.choose" = "Choose"; "common.button.confirm" = "Confirm"; @@ -28,16 +27,21 @@ "addVault.createNewVault.title" = "Create New Vault"; "addVault.createNewVault.chooseCloud.header" = "Where should Cryptomator store the encrypted files of your vault?"; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator detected an existing vault at this location.\nIn order to create a new vault, please go back and choose a different folder."; +"addVault.createNewVault.enterPassword.header" = "Enter a new password."; +"addVault.createNewVault.confirmPassword.header" = "Confirm the new password."; "addVault.createNewVault.alert.confirmPassword.title" = "Confirm Password?"; "addVault.createNewVault.alert.confirmPassword.message" = "IMPORTANT: If you forget your password, there is no way to recover your data."; "addVault.openExistingVault.title" = "Open Existing Vault"; "addVault.openExistingVault.chooseCloud.header" = "Where is the vault located?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detected the vault \"%@\".\nWould you like to add this vault?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Add This Vault"; "addVault.openExistingVault.password.footer" = "Enter password for \"%@\""; "addVault.success.info" = "Successfully added vault \"%@\".\nAccess this vault via the Files app."; "addVault.success.openFilesApp" = "Open Files App"; "addVault.success.footer" = "If you haven't already, enable Cryptomator in the Files app."; +"chooseFolder.emptyFolder.footer" = "Folder is Empty"; + "settings.title" = "Settings"; "settings.aboutCryptomator" = "About Cryptomator"; "settings.aboutCryptomator.title" = "Version %@ (%@)"; @@ -61,5 +65,7 @@ "createNewFolder.header.title" = "Choose a name for the folder."; "createNewFolder.cells.name" = "Folder Name"; - -"testFlight.otherFileProviders.alert.text" = "Other file providers are currently not available. Stay tuned for an update!"; +"localFileSystemAuthentication.createNewVault.header" = "In the next screen, choose the storage location for your new vault."; +"localFileSystemAuthentication.createNewVault.button" = "Select Storage Location"; +"localFileSystemAuthentication.openExistingVault.header" = "In the next screen, choose the folder of your existing vault."; +"localFileSystemAuthentication.openExistingVault.button" = "Select Vault Folder"; diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index 102740e79..ae97dd1f6 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -31,21 +31,86 @@ public class CryptomatorDatabase { private static var migrator: DatabaseMigrator { var migrator = DatabaseMigrator() + // Speed up development by nuking the database when migrations change + // See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md#the-erasedatabaseonschemachange-option + migrator.eraseDatabaseOnSchemaChange = true + migrator.registerMigration("v1") { db in - try db.create(table: "cloudProviderAccounts") { table in - table.column("accountUID", .text).primaryKey() - table.column("cloudProviderType", .text).notNull() - } - try db.create(table: "vaultAccounts") { table in - table.column("vaultUID", .text).primaryKey() - table.column("delegateAccountUID", .text).notNull().references("cloudProviderAccounts") - table.column("vaultPath", .text).notNull() - table.column("lastUpToDateCheck", .date).notNull() - } + try v1Migration(db) } return migrator } + // swiftlint:disable:next function_body_length + public class func v1Migration(_ db: Database) throws { + // Common + try db.create(table: "cloudProviderAccounts") { table in + table.column("accountUID", .text).primaryKey() + table.column("cloudProviderType", .text).notNull() + } + try db.create(table: "vaultAccounts") { table in + table.column("vaultUID", .text).primaryKey() + table.column("delegateAccountUID", .text).notNull().references("cloudProviderAccounts") + table.column("vaultPath", .text).notNull() + table.column("vaultName", .text).notNull() + table.column("lastUpToDateCheck", .date).notNull() + } + // Main App + try db.create(table: "vaultListPosition") { table in + table.column("position", .integer).unique() + table.column("vaultUID", .text).unique().notNull().references("vaultAccounts", onDelete: .cascade) + table.check(Column("position") != nil) + } + try db.execute(sql: """ + CREATE TRIGGER position_creation + AFTER INSERT + ON vaultAccounts + BEGIN + INSERT INTO vaultListPosition (position, vaultUID) + VALUES (IFNULL((SELECT MAX(position) FROM vaultListPosition), -1)+1, NEW.vaultUID); + END; + """) + + try db.execute(sql: """ + CREATE TRIGGER position_update + AFTER DELETE + ON vaultListPosition + BEGIN + UPDATE vaultListPosition + SET position = position - 1 + WHERE position > OLD.position; + END; + """) + + try db.create(table: "accountListPosition") { table in + table.column("position", .integer) + table.column("cloudProviderType", .text) + table.column("accountUID", .text).unique().notNull().references("cloudProviderAccounts", onDelete: .cascade) + table.uniqueKey(["position", "cloudProviderType"]) + table.check(Column("position") != nil && Column("cloudProviderType") != nil) + } + try db.execute(sql: """ + CREATE TRIGGER accountList_position_creation + AFTER INSERT + ON cloudProviderAccounts + BEGIN + INSERT INTO accountListPosition (position, cloudProviderType, accountUID) + VALUES (IFNULL((SELECT MAX(position) FROM accountListPosition WHERE cloudProviderType = NEW.cloudProviderType), -1)+1, NEW.cloudProviderType, NEW.accountUID); + END; + """) + + try db.execute(sql: """ + CREATE TRIGGER accountList_position_update + AFTER DELETE + ON accountListPosition + BEGIN + UPDATE accountListPosition + SET position = position - 1 + WHERE position > OLD.position AND cloudProviderType = OLD.cloudProviderType; + END; + """) + } + public static func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index f98c0fe91..8e870c318 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -54,8 +54,11 @@ public class CloudProviderDBManager: CloudProviderManager { client = WebDAVClient(credential: credential) } provider = WebDAVProvider(with: client) - default: - throw CloudProviderAccountError.accountNotFoundError + case .localFileSystem: + guard let rootURL = try LocalFileSystemBookmarkManager.getBookmarkedRootURL(for: accountUID) else { + throw CloudProviderAccountError.accountNotFoundError + } + provider = LocalFileSystemProvider(rootURL: rootURL) } CloudProviderDBManager.cachedProvider[accountUID] = provider return provider diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift index 824a817e2..e99cd1bbf 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift @@ -16,17 +16,20 @@ public struct VaultAccount: Decodable, FetchableRecord, TableRecord { static let delegateAccountUIDKey = "delegateAccountUID" static let vaultPathKey = "vaultPath" static let lastUpToDateCheckKey = "lastUpToDateCheck" + static let vaultNameKey = "vaultName" public let vaultUID: String let delegateAccountUID: String public let vaultPath: CloudPath + public let vaultName: String let lastUpToDateCheck: Date public static let delegateAccount = belongsTo(CloudProviderAccount.self) - public init(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, lastUpToDateCheck: Date = Date()) { + public init(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, vaultName: String, lastUpToDateCheck: Date = Date()) { self.vaultUID = vaultUID self.delegateAccountUID = delegateAccountUID self.vaultPath = vaultPath + self.vaultName = vaultName self.lastUpToDateCheck = lastUpToDateCheck } } @@ -36,6 +39,7 @@ extension VaultAccount: PersistableRecord { container[VaultAccount.vaultUIDKey] = vaultUID container[VaultAccount.delegateAccountUIDKey] = delegateAccountUID container[VaultAccount.vaultPathKey] = vaultPath + container[VaultAccount.vaultNameKey] = vaultName container[VaultAccount.lastUpToDateCheckKey] = lastUpToDateCheck } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index cc52ffe8e..9ce7c9356 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -30,8 +30,8 @@ public protocol VaultManager { func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise func manualUnlockVault(withUID vaultUID: String, password: String) throws -> CloudProvider func getDecorator(forVaultUID vaultUID: String) throws -> CloudProvider - func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultConfigPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise - func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, masterkeyPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise + func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise + func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise func removeVault(withUID vaultUID: String) throws -> Promise func removeAllUnusedFileProviderDomains() -> Promise func getVaultPath(from masterkeyPath: CloudPath) -> CloudPath @@ -85,15 +85,15 @@ public class VaultDBManager: VaultManager { try self.uploadVaultConfigToken(vaultConfigToken, vaultPath: vaultPath, delegate: delegate, tmpDirURL: tmpDirURL) }.then { _ -> Promise in try self.createVaultFolderStructure(masterkey: masterkey, vaultPath: vaultPath, delegate: delegate) - }.then { _ -> Void in + }.then { _ -> Promise in let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: delegate) - let account = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) - try self.vaultAccountManager.saveNewAccount(account) try self.saveFileProviderConformMasterkeyToKeychain(masterkey, forVaultUID: vaultUID, vaultConfigToken: vaultConfigToken, password: password, storePasswordInKeychain: storePasswordInKeychain) VaultDBManager.cachedDecorators[vaultUID] = decorator + return self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultPath.lastPathComponent) }.then { - self.addFileProviderDomain(forVaultUID: vaultUID, vaultPath: vaultPath) + let account = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultPath.lastPathComponent, lastUpToDateCheck: Date()) + try self.vaultAccountManager.saveNewAccount(account) } } @@ -175,7 +175,7 @@ public class VaultDBManager: VaultManager { // MARK: Open Existing Vault - public func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultConfigPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + public func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { let delegate: CloudProvider do { guard VaultDBManager.cachedDecorators[vaultUID] == nil else { @@ -188,22 +188,23 @@ public class VaultDBManager: VaultManager { let tmpDirURL = FileManager.default.temporaryDirectory let localVaultConfigURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) - let masterkeyPath = vaultConfigPath.deletingLastPathComponent().appendingPathComponent("masterkey.cryptomator") + let vaultPath = vaultItem.vaultPath + let vaultConfigPath = vaultPath.appendingPathComponent("vault.cryptomator") + let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") return delegate.downloadFile(from: vaultConfigPath, to: localVaultConfigURL).then { delegate.downloadFile(from: masterkeyPath, to: localMasterkeyURL) - }.then { + }.then { _ -> Promise in let token = try String(contentsOf: localVaultConfigURL, encoding: .utf8) let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) let masterkeyFile = try MasterkeyFile.withContentFromURL(url: localMasterkeyURL) let masterkey = try masterkeyFile.unlock(passphrase: password) - let vaultPath = self.getVaultPath(from: masterkeyPath) let vaultProvider = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: delegate) - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) try self.saveFileProviderConformMasterkeyToKeychain(masterkey, forVaultUID: vaultUID, vaultConfigToken: token, password: password, storePasswordInKeychain: storePasswordInKeychain) - try self.vaultAccountManager.saveNewAccount(vaultAccount) VaultDBManager.cachedDecorators[vaultUID] = vaultProvider + return self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name) }.then { - self.addFileProviderDomain(forVaultUID: vaultUID, vaultPath: self.getVaultPath(from: masterkeyPath)) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name, lastUpToDateCheck: Date()) + try self.vaultAccountManager.saveNewAccount(vaultAccount) }.catch { _ in VaultDBManager.cachedDecorators[vaultUID] = nil } @@ -217,7 +218,7 @@ public class VaultDBManager: VaultManager { - Postcondition: The passed `vaultUID`, `delegateAccountUID` and the `vaultPath` derived from `masterkeyPath` are stored as VaultAccount in the database - Postcondition: The created VaultDecorator is cached under the corresponding `vaultUID` */ - public func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, masterkeyPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + public func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { do { guard VaultDBManager.cachedDecorators[vaultUID] == nil else { throw VaultManagerError.vaultAlreadyExists @@ -225,16 +226,18 @@ public class VaultDBManager: VaultManager { let delegate = try providerManager.getProvider(with: delegateAccountUID) let tmpDirURL = FileManager.default.temporaryDirectory let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) - return delegate.downloadFile(from: masterkeyPath, to: localMasterkeyURL).then { + let vaultPath = vaultItem.vaultPath + let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") + return delegate.downloadFile(from: masterkeyPath, to: localMasterkeyURL).then { _ -> Promise in let masterkeyFile = try MasterkeyFile.withContentFromURL(url: localMasterkeyURL) let masterkey = try masterkeyFile.unlock(passphrase: password) - let vaultPath = self.getVaultPath(from: masterkeyPath) - _ = try self.createLegacyVaultDecorator(from: masterkey, delegate: delegate, vaultPath: vaultPath, vaultUID: vaultUID, vaultVersion: masterkeyFile.version) - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) + let vaultProvider = try self.createLegacyVaultDecorator(from: masterkey, delegate: delegate, vaultPath: vaultPath, vaultUID: vaultUID, vaultVersion: masterkeyFile.version) + VaultDBManager.cachedDecorators[vaultUID] = vaultProvider try self.saveFileProviderConformMasterkeyToKeychain(masterkey, forVaultUID: vaultUID, vaultVersion: masterkeyFile.version, password: password, storePasswordInKeychain: storePasswordInKeychain) - try self.vaultAccountManager.saveNewAccount(vaultAccount) + return self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name) }.then { - self.addFileProviderDomain(forVaultUID: vaultUID, vaultPath: self.getVaultPath(from: masterkeyPath)) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name, lastUpToDateCheck: Date()) + try self.vaultAccountManager.saveNewAccount(vaultAccount) } } catch { VaultDBManager.cachedDecorators[vaultUID] = nil @@ -346,9 +349,9 @@ public class VaultDBManager: VaultManager { } } - func addFileProviderDomain(forVaultUID vaultUID: String, vaultPath: CloudPath) -> Promise { + func addFileProviderDomain(forVaultUID vaultUID: String, displayName: String) -> Promise { let identifier = NSFileProviderDomainIdentifier(vaultUID) - let domain = NSFileProviderDomain(identifier: identifier, displayName: vaultPath.lastPathComponent, pathRelativeToDocumentStorage: vaultUID) + let domain = NSFileProviderDomain(identifier: identifier, displayName: displayName, pathRelativeToDocumentStorage: vaultUID) return NSFileProviderManager.add(domain) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift new file mode 100644 index 000000000..04e18fb62 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift @@ -0,0 +1,13 @@ +// +// VaultItem.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 24.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// +import CryptomatorCloudAccessCore +import Foundation +public protocol VaultItem { + var name: String { get } + var vaultPath: CloudPath { get } +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index 9951041a0..6956f0d6c 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -19,7 +19,7 @@ class VaultManagerMock: VaultDBManager { var savedPasswords = [String: String]() var savedVaultConfigTokens = [String: String]() var removedVaultUIDs = [String]() - var addedFileProviderDomains = [String: CloudPath]() + var addedFileProviderDomains = [String: String]() override func saveFileProviderConformMasterkeyToKeychain(_ masterkey: Masterkey, forVaultUID vaultUID: String, vaultVersion: Int, password: String, storePasswordInKeychain: Bool) throws { savedMasterkeys[vaultUID] = masterkey @@ -47,8 +47,8 @@ class VaultManagerMock: VaultDBManager { return Promise(()) } - override func addFileProviderDomain(forVaultUID vaultUID: String, vaultPath: CloudPath) -> Promise { - addedFileProviderDomains[vaultUID] = vaultPath + override func addFileProviderDomain(forVaultUID vaultUID: String, displayName: String) -> Promise { + addedFileProviderDomains[vaultUID] = displayName return Promise(()) } } @@ -80,6 +80,7 @@ class VaultManagerTests: XCTestCase { table.column(VaultAccount.vaultUIDKey, .text).primaryKey() table.column(VaultAccount.delegateAccountUIDKey, .text).notNull() table.column(VaultAccount.vaultPathKey, .text).notNull() + table.column(VaultAccount.vaultNameKey, .text).notNull() table.column(VaultAccount.lastUpToDateCheckKey, .date).notNull() } } @@ -137,7 +138,7 @@ class VaultManagerTests: XCTestCase { } XCTAssertEqual(masterkey, managerMock.savedMasterkeys[vaultUID]) XCTAssertEqual(1, managerMock.addedFileProviderDomains.count) - XCTAssertEqual(vaultPath, managerMock.addedFileProviderDomains[vaultUID]) + XCTAssertEqual(vaultPath.lastPathComponent, managerMock.addedFileProviderDomains[vaultUID]) // Vault config checks let vaultConfigPath = vaultPath.appendingPathComponent("vault.cryptomator") @@ -187,7 +188,8 @@ class VaultManagerTests: XCTestCase { cloudProviderMock.filesToDownload[vaultConfigPath.path] = token.data(using: .utf8) let vaultUID = UUID().uuidString - manager.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultConfigPath: vaultConfigPath, password: "pw", storePasswordInKeychain: true).then { [self] in + let vaultDetails = VaultDetails(name: "ExistingVault", vaultPath: vaultPath) + manager.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultDetails, password: "pw", storePasswordInKeychain: true).then { [self] in XCTAssertNotNil(VaultDBManager.cachedDecorators[vaultUID]) guard VaultDBManager.cachedDecorators[vaultUID] is VaultFormat8ShorteningProviderDecorator else { XCTFail("VaultDecorator has wrong type") @@ -203,7 +205,7 @@ class VaultManagerTests: XCTestCase { } XCTAssertEqual(managerMock.savedMasterkeys[vaultUID], masterkey) XCTAssertEqual(1, managerMock.addedFileProviderDomains.count) - XCTAssertEqual(vaultPath, managerMock.addedFileProviderDomains[vaultUID]) + XCTAssertEqual(vaultPath.lastPathComponent, managerMock.addedFileProviderDomains[vaultUID]) XCTAssertEqual(1, managerMock.savedVaultConfigTokens.count) guard let savedVaultConfigToken = managerMock.savedVaultConfigTokens[vaultUID] else { @@ -239,7 +241,8 @@ class VaultManagerTests: XCTestCase { let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) cloudProviderMock.filesToDownload[masterkeyPath.path] = try managerMock.exportMasterkey(masterkey, vaultVersion: 7, password: "pw") let vaultUID = UUID().uuidString - manager.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: delegateAccountUID, masterkeyPath: masterkeyPath, password: "pw", storePasswordInKeychain: true).then { [self] in + let legacyVaultDetails = VaultDetails(name: "ExistingVault", vaultPath: vaultPath) + manager.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: legacyVaultDetails, password: "pw", storePasswordInKeychain: true).then { [self] in XCTAssertNotNil(VaultDBManager.cachedDecorators[vaultUID]) guard VaultDBManager.cachedDecorators[vaultUID] is VaultFormat7ShorteningProviderDecorator else { XCTFail("VaultDecorator has wrong type") @@ -255,7 +258,7 @@ class VaultManagerTests: XCTestCase { } XCTAssertEqual(managerMock.savedMasterkeys[vaultUID], masterkey) XCTAssertEqual(1, managerMock.addedFileProviderDomains.count) - XCTAssertEqual(vaultPath, managerMock.addedFileProviderDomains[vaultUID]) + XCTAssertEqual(vaultPath.lastPathComponent, managerMock.addedFileProviderDomains[vaultUID]) }.catch { error in XCTFail("Promise failed with error: \(error)") }.always { @@ -270,7 +273,7 @@ class VaultManagerTests: XCTestCase { try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString let vaultPath = CloudPath("/VaultV8/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV8", lastUpToDateCheck: Date()) try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCTRMAC, shorteningThreshold: 220) @@ -289,7 +292,7 @@ class VaultManagerTests: XCTestCase { try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString let vaultPath = CloudPath("/VaultV7/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV7", lastUpToDateCheck: Date()) try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) let decorator = try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 7, vaultConfigToken: nil) @@ -306,7 +309,7 @@ class VaultManagerTests: XCTestCase { try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString let vaultPath = CloudPath("/VaultV6/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV6", lastUpToDateCheck: Date()) try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) let decorator = try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 6, vaultConfigToken: nil) @@ -323,7 +326,7 @@ class VaultManagerTests: XCTestCase { try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString let vaultPath = CloudPath("/VaultV1/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, lastUpToDateCheck: Date()) + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV1", lastUpToDateCheck: Date()) try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) XCTAssertThrowsError(try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 1, vaultConfigToken: nil)) { error in @@ -335,6 +338,11 @@ class VaultManagerTests: XCTestCase { } } +struct VaultDetails: VaultItem { + let name: String + let vaultPath: CloudPath +} + extension Masterkey: Equatable { public static func == (lhs: Masterkey, rhs: Masterkey) -> Bool { return lhs.aesMasterKey == rhs.aesMasterKey && lhs.macMasterKey == rhs.macMasterKey diff --git a/CryptomatorFileProvider/RootFileProviderItem.swift b/CryptomatorFileProvider/RootFileProviderItem.swift index 941845c50..dd3b2149b 100644 --- a/CryptomatorFileProvider/RootFileProviderItem.swift +++ b/CryptomatorFileProvider/RootFileProviderItem.swift @@ -9,10 +9,11 @@ import FileProvider import Foundation import MobileCoreServices + public class RootFileProviderItem: NSObject, NSFileProviderItem { public var itemIdentifier = NSFileProviderItemIdentifier.rootContainer public let parentItemIdentifier = NSFileProviderItemIdentifier.rootContainer - public let filename = "Home" + public let filename = "Cryptomator" public let typeIdentifier = kUTTypeFolder as String public let documentSize: NSNumber? = nil public let capabilities: NSFileProviderItemCapabilities = [.allowsAll] diff --git a/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift b/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift new file mode 100644 index 000000000..7a8c6861b --- /dev/null +++ b/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift @@ -0,0 +1,89 @@ +// +// AddLocalVaultViewModelTestCase.swift +// CryptomatorTests +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import XCTest +@testable import Cryptomator +@testable import CryptomatorCloudAccessCore +@testable import CryptomatorCryptoLib + +class AddLocalVaultViewModelTestCase: XCTestCase { + var tmpDirURL: URL! + var accountManagerMock: CloudProviderAccountManagerMock! + override func setUpWithError() throws { + tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: tmpDirURL, withIntermediateDirectories: false) + accountManagerMock = CloudProviderAccountManagerMock() + } + + override func tearDownWithError() throws { + try FileManager.default.removeItem(at: tmpDirURL) + } + + func createVault(at url: URL) throws { + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCTRMAC, shorteningThreshold: 220) + + let cryptor = Cryptor(masterkey: masterkey) + let rootDirPath = try getRootDirectoryURL(for: cryptor, vaultURL: url) + try FileManager.default.createDirectory(at: rootDirPath, withIntermediateDirectories: true, attributes: nil) + let masterkeyData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 999, passphrase: "password", scryptCostParam: 2) + let masterkeyURL = url.appendingPathComponent("masterkey.cryptomator") + try masterkeyData.write(to: masterkeyURL) + let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let vaultConfigURL = url.appendingPathComponent("vault.cryptomator") + try token.write(to: vaultConfigURL, atomically: true, encoding: .utf8) + } + + func createLegacyVault(at url: URL) throws { + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + let cryptor = Cryptor(masterkey: masterkey) + let rootDirPath = try getRootDirectoryURL(for: cryptor, vaultURL: url) + try FileManager.default.createDirectory(at: rootDirPath, withIntermediateDirectories: true, attributes: nil) + let masterkeyData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 7, passphrase: "password", scryptCostParam: 2) + let masterkeyURL = url.appendingPathComponent("masterkey.cryptomator") + try masterkeyData.write(to: masterkeyURL) + } + + private func getRootDirectoryURL(for cryptor: Cryptor, vaultURL: URL) throws -> URL { + let digest = try cryptor.encryptDirId(Data()) + let i = digest.index(digest.startIndex, offsetBy: 2) + return vaultURL.appendingPathComponent("d/\(digest[.. CloudProviderType { + throw MockError.notMocked + } + + func getAllAccountUIDs(for type: CloudProviderType) throws -> [String] { + throw MockError.notMocked + } + + func saveNewAccount(_ account: CloudProviderAccount) throws { + savedAccounts.append(account) + } + + func removeAccount(with accountUID: String) throws { + throw MockError.notMocked + } + } +} + +extension LocalFileSystemCredential: Equatable { + public static func == (lhs: LocalFileSystemCredential, rhs: LocalFileSystemCredential) -> Bool { + lhs.identifier == rhs.identifier && lhs.rootURL == rhs.rootURL + } +} + +extension CloudProviderAccount: Equatable { + public static func == (lhs: CloudProviderAccount, rhs: CloudProviderAccount) -> Bool { + lhs.accountUID == rhs.accountUID && lhs.cloudProviderType == rhs.cloudProviderType + } +} diff --git a/CryptomatorTests/AddLocalVault/CreateNewLocalVaultViewModelTests.swift b/CryptomatorTests/AddLocalVault/CreateNewLocalVaultViewModelTests.swift new file mode 100644 index 000000000..a28ce9f4c --- /dev/null +++ b/CryptomatorTests/AddLocalVault/CreateNewLocalVaultViewModelTests.swift @@ -0,0 +1,96 @@ +// +// CreateNewLocalVaultViewModelTests.swift +// CryptomatorTests +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import XCTest +@testable import Cryptomator + +class CreateNewLocalVaultViewModelTests: AddLocalVaultViewModelTestCase { + func testAddVault() throws { + let expectation = XCTestExpectation() + let viewModel = CreateNewLocalVaultViewModel(vaultName: "MyVault", accountManager: accountManagerMock) + let credential = LocalFileSystemCredential(rootURL: tmpDirURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { result in + XCTAssertEqual(credential, result.credential) + XCTAssertEqual(credential.identifier, result.account.accountUID) + XCTAssertEqual(CloudProviderType.localFileSystem, result.account.cloudProviderType) + + guard let chosenFolder = result.item as? Folder else { + XCTFail("result item is not a Folder") + return + } + XCTAssertEqual(CloudPath("/MyVault"), chosenFolder.path) + + XCTAssertEqual(1, self.accountManagerMock.savedAccounts.count) + XCTAssertEqual(result.account, self.accountManagerMock.savedAccounts[0]) + }.catch { error in + XCTFail("Promise failed with error: \(error)") + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testAddVaultWithNameCollision() throws { + let expectation = XCTestExpectation() + let viewModel = CreateNewLocalVaultViewModel(vaultName: "MyVault", accountManager: accountManagerMock) + try FileManager.default.createDirectory(at: tmpDirURL.appendingPathComponent("MyVault"), withIntermediateDirectories: false) + let credential = LocalFileSystemCredential(rootURL: tmpDirURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { _ in + XCTFail("Promise fulfilled") + }.catch { error in + guard case CreateNewVaultChooseFolderViewModelError.vaultNameCollision = error else { + XCTFail("Promise rejected with wrong error: \(error)") + return + } + XCTAssertEqual(0, self.accountManagerMock.savedAccounts.count) + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testAddVaultWithExistingVaultAtChosenURL() throws { + let expectation = XCTestExpectation() + let viewModel = CreateNewLocalVaultViewModel(vaultName: "MyVault", accountManager: accountManagerMock) + try createVault(at: tmpDirURL) + let credential = LocalFileSystemCredential(rootURL: tmpDirURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { _ in + XCTFail("Promise fulfilled") + }.catch { error in + guard case CreateNewLocalVaultViewModelError.detectedExistingVault = error else { + XCTFail("Promise rejected with wrong error: \(error)") + return + } + XCTAssertEqual(0, self.accountManagerMock.savedAccounts.count) + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testAddVaultWithExistingLegacyVaultAtChosenURL() throws { + let expectation = XCTestExpectation() + let viewModel = CreateNewLocalVaultViewModel(vaultName: "MyVault", accountManager: accountManagerMock) + try createLegacyVault(at: tmpDirURL) + let credential = LocalFileSystemCredential(rootURL: tmpDirURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { _ in + XCTFail("Promise fulfilled") + }.catch { error in + guard case CreateNewLocalVaultViewModelError.detectedExistingVault = error else { + XCTFail("Promise rejected with wrong error: \(error)") + return + } + XCTAssertEqual(0, self.accountManagerMock.savedAccounts.count) + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/CryptomatorTests/AddLocalVault/OpenExistingLocalVaultViewModelTests.swift b/CryptomatorTests/AddLocalVault/OpenExistingLocalVaultViewModelTests.swift new file mode 100644 index 000000000..23352dbb9 --- /dev/null +++ b/CryptomatorTests/AddLocalVault/OpenExistingLocalVaultViewModelTests.swift @@ -0,0 +1,96 @@ +// +// OpenExistingLocalVaultViewModelTests.swift +// CryptomatorTests +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import XCTest +@testable import Cryptomator + +class OpenExistingLocalVaultViewModelTests: AddLocalVaultViewModelTestCase { + var viewModel: OpenExistingLocalVaultViewModel! + + override func setUpWithError() throws { + try super.setUpWithError() + viewModel = OpenExistingLocalVaultViewModel(accountManager: accountManagerMock) + } + + func testAddVault() throws { + let expectation = XCTestExpectation() + let vaultURL = tmpDirURL.appendingPathComponent("MyVault") + try createVault(at: vaultURL) + let rootURL = vaultURL.appendingPathComponent("/") + let credential = LocalFileSystemCredential(rootURL: rootURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { result in + XCTAssertEqual(credential, result.credential) + XCTAssertEqual(credential.identifier, result.account.accountUID) + XCTAssertEqual(CloudProviderType.localFileSystem, result.account.cloudProviderType) + + guard let vaultDetailItem = result.item as? VaultDetailItem else { + XCTFail("result item is not a VaultDetailItem") + return + } + XCTAssertEqual("MyVault", vaultDetailItem.name) + XCTAssertEqual(CloudPath("/"), vaultDetailItem.path) + XCTAssertFalse(vaultDetailItem.isLegacyVault) + + XCTAssertEqual(1, self.accountManagerMock.savedAccounts.count) + XCTAssertEqual(result.account, self.accountManagerMock.savedAccounts[0]) + }.catch { error in + XCTFail("Promise failed with error: \(error)") + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testAddLegacyVault() throws { + let expectation = XCTestExpectation() + let vaultURL = tmpDirURL.appendingPathComponent("MyLegacyVault") + try createLegacyVault(at: vaultURL) + let rootURL = vaultURL.appendingPathComponent("/") + let credential = LocalFileSystemCredential(rootURL: rootURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { result in + XCTAssertEqual(credential, result.credential) + XCTAssertEqual(credential.identifier, result.account.accountUID) + XCTAssertEqual(CloudProviderType.localFileSystem, result.account.cloudProviderType) + + guard let vaultDetailItem = result.item as? VaultDetailItem else { + XCTFail("result item is not a VaultDetailItem") + return + } + XCTAssertEqual("MyLegacyVault", vaultDetailItem.name) + XCTAssertEqual(CloudPath("/"), vaultDetailItem.path) + XCTAssertTrue(vaultDetailItem.isLegacyVault) + + XCTAssertEqual(1, self.accountManagerMock.savedAccounts.count) + XCTAssertEqual(result.account, self.accountManagerMock.savedAccounts[0]) + }.catch { error in + XCTFail("Promise failed with error: \(error)") + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testAddVaultWithMissingVault() throws { + let expectation = XCTestExpectation() + let credential = LocalFileSystemCredential(rootURL: tmpDirURL, identifier: UUID().uuidString) + viewModel.addVault(for: credential).then { _ in + XCTFail("Promise fulfilled") + }.catch { error in + guard case OpenExistingLocalVaultViewModelError.noVaultFound = error else { + XCTFail("Promise rejected with wrong error: \(error)") + return + } + XCTAssertEqual(0, self.accountManagerMock.savedAccounts.count) + }.always { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/CryptomatorTests/CreateNewFolderViewModelTests.swift b/CryptomatorTests/CreateNewFolderViewModelTests.swift index 879682ac0..ef305fe51 100644 --- a/CryptomatorTests/CreateNewFolderViewModelTests.swift +++ b/CryptomatorTests/CreateNewFolderViewModelTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import Promises import XCTest @testable import Cryptomator + class CreateNewFolderViewModelTests: XCTestCase { private var cloudProviderMock: CloudProviderMock! diff --git a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift index fb8c324bc..985735262 100644 --- a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift +++ b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift @@ -11,6 +11,7 @@ import CryptomatorCommonCore import Promises import XCTest @testable import Cryptomator + class CreateNewVaultPasswordViewModelTests: XCTestCase { private var vaultManagerMock: VaultManagerMock! @@ -185,11 +186,11 @@ private class VaultManagerMock: VaultManager { throw MockError.notMocked } - func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultConfigPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { return Promise(MockError.notMocked) } - func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, masterkeyPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { return Promise(MockError.notMocked) } @@ -206,10 +207,6 @@ private class VaultManagerMock: VaultManager { } } -private enum MockError: Error { - case notMocked -} - private struct CreatedVault { let vaultUID: String let delegateAccountUID: String diff --git a/CryptomatorTests/DatabaseManagerTests.swift b/CryptomatorTests/DatabaseManagerTests.swift index 7056cea14..689cfddf5 100644 --- a/CryptomatorTests/DatabaseManagerTests.swift +++ b/CryptomatorTests/DatabaseManagerTests.swift @@ -41,7 +41,7 @@ class DatabaseManagerTests: XCTestCase { let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV) try cloudAccountManager.saveNewAccount(cloudProviderAccount) - let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1")) + let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1"), vaultName: "Vault1") try vaultAccountManager.saveNewAccount(vaultAccount) let firstVaultListPosition = try dbPool.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault1").fetchOne(db) @@ -50,7 +50,7 @@ class DatabaseManagerTests: XCTestCase { XCTAssertEqual(0, firstVaultListPosition?.position) XCTAssertEqual(1, firstVaultListPosition?.id) - let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2")) + let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2"), vaultName: "Vault2") try vaultAccountManager.saveNewAccount(secondVaultAccount) let secondVaultListPosition = try dbPool.read { db in @@ -67,11 +67,11 @@ class DatabaseManagerTests: XCTestCase { let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV) try cloudAccountManager.saveNewAccount(cloudProviderAccount) - let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1")) + let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1"), vaultName: "Vault1") try vaultAccountManager.saveNewAccount(vaultAccount) - let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2")) + let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2"), vaultName: "Vault2") try vaultAccountManager.saveNewAccount(secondVaultAccount) - let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3")) + let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3"), vaultName: "Vault3") try vaultAccountManager.saveNewAccount(thirdVaultAccount) _ = try dbPool.write { db in @@ -102,11 +102,11 @@ class DatabaseManagerTests: XCTestCase { let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV) try cloudAccountManager.saveNewAccount(cloudProviderAccount) - let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1")) + let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1"), vaultName: "Vault1") try vaultAccountManager.saveNewAccount(vaultAccount) - let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2")) + let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2"), vaultName: "Vault2") try vaultAccountManager.saveNewAccount(secondVaultAccount) - let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3")) + let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3"), vaultName: "Vault3") try vaultAccountManager.saveNewAccount(thirdVaultAccount) let vaults = try dbManager.getAllVaults() diff --git a/CryptomatorTests/MockError.swift b/CryptomatorTests/MockError.swift new file mode 100644 index 000000000..f2649f309 --- /dev/null +++ b/CryptomatorTests/MockError.swift @@ -0,0 +1,13 @@ +// +// MockError.swift +// CryptomatorTests +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import Foundation + +enum MockError: Error { + case notMocked +} diff --git a/CryptomatorTests/SetVaultNameViewModelTests.swift b/CryptomatorTests/SetVaultNameViewModelTests.swift index 7c3bb19d1..3a2de39da 100644 --- a/CryptomatorTests/SetVaultNameViewModelTests.swift +++ b/CryptomatorTests/SetVaultNameViewModelTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import Cryptomator + class SetVaultNameViewModelTests: XCTestCase { var viewModel: SetVaultNameViewModel! override func setUpWithError() throws { diff --git a/CryptomatorTests/VaultListViewModelTests.swift b/CryptomatorTests/VaultListViewModelTests.swift index 79096667a..e86c10d9a 100644 --- a/CryptomatorTests/VaultListViewModelTests.swift +++ b/CryptomatorTests/VaultListViewModelTests.swift @@ -100,10 +100,10 @@ class VaultListViewModelTests: XCTestCase { private class DatabaseManagerMock: DatabaseManager { var updatedPositions = [VaultListPosition]() - let vaults = [VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault1", delegateAccountUID: "1", vaultPath: CloudPath("/vault1")), + let vaults = [VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault1", delegateAccountUID: "1", vaultPath: CloudPath("/vault1"), vaultName: "vault1"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV), vaultListPosition: VaultListPosition(position: 1, vaultUID: "vault1")), - VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault2", delegateAccountUID: "1", vaultPath: CloudPath("/vault1")), + VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault2", delegateAccountUID: "1", vaultPath: CloudPath("/vault1"), vaultName: "vautlt1"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .webDAV), vaultListPosition: VaultListPosition(position: 0, vaultUID: "vault2"))] @@ -136,10 +136,6 @@ private class VaultAccountManagerMock: VaultAccountManager { } } -private enum MockError: Error { - case notMocked -} - private class VaultManagerMock: VaultDBManager { var removedFileProviderDomains = [String]() override func removeFileProviderDomain(withVaultUID vaultUID: String) -> Promise { diff --git a/FileProviderExtensionUI/Base.lproj/MainInterface.storyboard b/FileProviderExtensionUI/Base.lproj/MainInterface.storyboard deleted file mode 100644 index bba4f6337..000000000 --- a/FileProviderExtensionUI/Base.lproj/MainInterface.storyboard +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FileProviderExtensionUI/DocumentActionViewController.swift b/FileProviderExtensionUI/DocumentActionViewController.swift deleted file mode 100644 index 49d5f0466..000000000 --- a/FileProviderExtensionUI/DocumentActionViewController.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// DocumentActionViewController.swift -// FileProviderExtensionUI -// -// Created by Philipp Schmid on 17.06.20. -// Copyright © 2020 Skymatic GmbH. All rights reserved. -// - -import FileProviderUI -import UIKit - -class DocumentActionViewController: FPUIActionExtensionViewController { - @IBOutlet var identifierLabel: UILabel! - @IBOutlet var actionTypeLabel: UILabel! - - override func prepare(forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]) { - identifierLabel?.text = actionIdentifier - actionTypeLabel?.text = "Custom action" - } - - override func prepare(forError error: Error) { - identifierLabel?.text = error.localizedDescription - actionTypeLabel?.text = "Authenticate" - } - - @IBAction func doneButtonTapped(_ sender: Any) { - // Perform the action and call the completion block. If an unrecoverable error occurs you must still call the completion block with an error. Use the error code FPUIExtensionErrorCode.failed to signal the failure. - extensionContext.completeRequest() - } - - @IBAction func cancelButtonTapped(_ sender: Any) { - extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) - } -} diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift new file mode 100644 index 000000000..882828784 --- /dev/null +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -0,0 +1,41 @@ +// +// FileProviderCoordinator.swift +// FileProviderExtensionUI +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import FileProviderUI +import UIKit + +class FileProviderCoordinator { + let navigationController: UINavigationController + let extensionContext: FPUIActionExtensionContext + + init(extensionContext: FPUIActionExtensionContext, navigationController: UINavigationController) { + self.extensionContext = extensionContext + self.navigationController = navigationController + } + + func userCancelled() { + extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) + } + + // MARK: - Onboarding + + func showOnboarding() { + let onboardingVC = OnboardingViewController() + onboardingVC.coordinator = self + navigationController.pushViewController(onboardingVC, animated: false) + } + + func openCryptomatorApp() { + let url = URL(string: "cryptomator:")! + extensionContext.open(url) { success in + if success { + self.userCancelled() + } + } + } +} diff --git a/FileProviderExtensionUI/Info.plist b/FileProviderExtensionUI/Info.plist index 3ca6d2a9e..1e91f62cd 100644 --- a/FileProviderExtensionUI/Info.plist +++ b/FileProviderExtensionUI/Info.plist @@ -33,8 +33,8 @@ Custom Action - NSExtensionMainStoryboard - MainInterface + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).RootViewController NSExtensionPointIdentifier com.apple.fileprovider-actionsui diff --git a/FileProviderExtensionUI/OnboardingView.swift b/FileProviderExtensionUI/OnboardingView.swift new file mode 100644 index 000000000..5215b1d25 --- /dev/null +++ b/FileProviderExtensionUI/OnboardingView.swift @@ -0,0 +1,61 @@ +// +// OnboardingView.swift +// FileProviderExtensionUI +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class OnboardingView: UIView { + private lazy var label: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .center + return label + }() + + lazy var cancelButton: UIButton = { + let button = UIButton() + button.setTitle(NSLocalizedString("onboarding.button.ok", comment: ""), for: .normal) + button.setTitleColor(UIColor(named: "primary"), for: .normal) + return button + }() + + init() { + super.init(frame: .zero) + + let botVaultImage = UIImage(named: "bot-vault") + let imageView = UIImageView(image: botVaultImage) + + label.text = NSLocalizedString("onboarding.text", comment: "") + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + label.translatesAutoresizingMaskIntoConstraints = false + cancelButton.translatesAutoresizingMaskIntoConstraints = false + + addSubview(imageView) + addSubview(label) + addSubview(cancelButton) + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + label.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + label.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 20), + + label.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20), + label.bottomAnchor.constraint(equalTo: cancelButton.topAnchor, constant: -20), + + cancelButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20), + cancelButton.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/FileProviderExtensionUI/OnboardingViewController.swift b/FileProviderExtensionUI/OnboardingViewController.swift new file mode 100644 index 000000000..1decb92b1 --- /dev/null +++ b/FileProviderExtensionUI/OnboardingViewController.swift @@ -0,0 +1,98 @@ +// +// OnboardingViewController.swift +// FileProviderExtensionUI +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit + +class OnboardingViewController: UITableViewController { + weak var coordinator: FileProviderCoordinator? + + private lazy var openCryptomatorCell: UITableViewCell = { + let cell = UITableViewCell() + cell.textLabel?.text = NSLocalizedString("onboarding.button.openCryptomator", comment: "") + cell.textLabel?.textColor = UIColor(named: "primary") + return cell + }() + + override func loadView() { + tableView = UITableView(frame: .zero, style: .grouped) + } + + override func viewDidLoad() { + super.viewDidLoad() + title = NSLocalizedString("onboarding.title", comment: "") + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) + navigationItem.rightBarButtonItem = doneButton + } + + @objc func done() { + coordinator?.userCancelled() + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return openCryptomatorCell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return OnboardingHeaderView() + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + coordinator?.openCryptomatorApp() + } +} + +private class OnboardingHeaderView: UIView { + private lazy var imageView: UIImageView = { + let image = UIImage(named: "bot") + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var infoLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + init() { + super.init(frame: .zero) + infoLabel.text = NSLocalizedString("onboarding.info", comment: "") + let stack = UIStackView(arrangedSubviews: [imageView, infoLabel]) + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.spacing = 20 + addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor, constant: 20), + stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift new file mode 100644 index 000000000..f1e680769 --- /dev/null +++ b/FileProviderExtensionUI/RootViewController.swift @@ -0,0 +1,31 @@ +// +// RootViewController.swift +// FileProviderExtensionUI +// +// Created by Philipp Schmid on 29.06.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import FileProviderUI +import UIKit + +class RootViewController: FPUIActionExtensionViewController { + private var coordinator: FileProviderCoordinator? + override func viewDidLoad() { + super.viewDidLoad() + let navigationController = UINavigationController() + navigationController.navigationBar.barTintColor = UIColor(named: "primary") + navigationController.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] + navigationController.navigationBar.tintColor = .white + addChild(navigationController) + view.addSubview(navigationController.view) + navigationController.didMove(toParent: self) + + coordinator = FileProviderCoordinator(extensionContext: extensionContext, navigationController: navigationController) + coordinator?.showOnboarding() + } + + @objc func cancel() { + extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) + } +} diff --git a/FileProviderExtensionUI/de.lproj/Localizable.strings b/FileProviderExtensionUI/de.lproj/Localizable.strings new file mode 100644 index 000000000..11175395e --- /dev/null +++ b/FileProviderExtensionUI/de.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + Cryptomator + + Created by Philipp Schmid on 29.06.21. + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"onboarding.title" = "Willkommen"; +"onboarding.info" = "Vielen Dank, dass du Cryptomator zum Schutz deiner Dateien gewählt hast. Um loszulegen, gehe zur Haupt-App und füge einen Tresor hinzu."; +"onboarding.button.openCryptomator" = "Cryptomator öffnen"; diff --git a/FileProviderExtensionUI/en.lproj/Localizable.strings b/FileProviderExtensionUI/en.lproj/Localizable.strings new file mode 100644 index 000000000..ed6b8482a --- /dev/null +++ b/FileProviderExtensionUI/en.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + Cryptomator + + Created by Philipp Schmid on 29.06.21. + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"onboarding.title" = "Welcome"; +"onboarding.info" = "Thanks for choosing Cryptomator to protect your files. To get started, go to the main app and add a vault."; +"onboarding.button.openCryptomator" = "Open Cryptomator"; diff --git a/README.md b/README.md index 84e99b20c..4357ff2cc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ # Cryptomator for iOS +[![Build](https://github.com/cryptomator/ios/actions/workflows/build.yml/badge.svg)](https://github.com/cryptomator/ios/actions/workflows/build.yml) +[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator) +[![Crowdin](https://badges.crowdin.net/cryptomator-ios/localized.svg)](https://crowdin.com/project/cryptomator-ios) +[![Community](https://img.shields.io/badge/help-Community-orange.svg)](https://community.cryptomator.org) + +Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud. + +This is the repository for the new iOS app of Cryptomator, which has been rewritten from the ground up and is currently in a beta phase. + +Try it out for free via TestFlight: https://testflight.apple.com/join/WMtYSrzD + +If you're looking for the current App Store version and would like to report an issue, check out the other repository: https://github.com/cryptomator/cryptomator-ios + ## Building ### Create Secrets diff --git a/Cryptomator/Assets.xcassets/740-gear.imageset/740-gear.png b/SharedResources/Assets.xcassets/740-gear.imageset/740-gear.png similarity index 100% rename from Cryptomator/Assets.xcassets/740-gear.imageset/740-gear.png rename to SharedResources/Assets.xcassets/740-gear.imageset/740-gear.png diff --git a/Cryptomator/Assets.xcassets/740-gear.imageset/740-gear@2x.png b/SharedResources/Assets.xcassets/740-gear.imageset/740-gear@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/740-gear.imageset/740-gear@2x.png rename to SharedResources/Assets.xcassets/740-gear.imageset/740-gear@2x.png diff --git a/Cryptomator/Assets.xcassets/740-gear.imageset/740-gear@3x.png b/SharedResources/Assets.xcassets/740-gear.imageset/740-gear@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/740-gear.imageset/740-gear@3x.png rename to SharedResources/Assets.xcassets/740-gear.imageset/740-gear@3x.png diff --git a/Cryptomator/Assets.xcassets/740-gear.imageset/Contents.json b/SharedResources/Assets.xcassets/740-gear.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/740-gear.imageset/Contents.json rename to SharedResources/Assets.xcassets/740-gear.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Contents.json b/SharedResources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Contents.json rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-76.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-76.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-76.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png diff --git a/Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon.png b/SharedResources/Assets.xcassets/AppIcon.appiconset/Icon.png similarity index 100% rename from Cryptomator/Assets.xcassets/AppIcon.appiconset/Icon.png rename to SharedResources/Assets.xcassets/AppIcon.appiconset/Icon.png diff --git a/Cryptomator/Assets.xcassets/Contents.json b/SharedResources/Assets.xcassets/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/Contents.json rename to SharedResources/Assets.xcassets/Contents.json diff --git a/Cryptomator/Assets.xcassets/actions-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/actions-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/actions-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/actions-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected.png b/SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected.png rename to SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected.png diff --git a/Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected@2x.png b/SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected@2x.png rename to SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected@3x.png b/SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions-selected.imageset/actions-selected@3x.png rename to SharedResources/Assets.xcassets/actions-selected.imageset/actions-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/actions.imageset/Contents.json b/SharedResources/Assets.xcassets/actions.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/actions.imageset/Contents.json rename to SharedResources/Assets.xcassets/actions.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/actions.imageset/actions.png b/SharedResources/Assets.xcassets/actions.imageset/actions.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions.imageset/actions.png rename to SharedResources/Assets.xcassets/actions.imageset/actions.png diff --git a/Cryptomator/Assets.xcassets/actions.imageset/actions@2x.png b/SharedResources/Assets.xcassets/actions.imageset/actions@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions.imageset/actions@2x.png rename to SharedResources/Assets.xcassets/actions.imageset/actions@2x.png diff --git a/Cryptomator/Assets.xcassets/actions.imageset/actions@3x.png b/SharedResources/Assets.xcassets/actions.imageset/actions@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/actions.imageset/actions@3x.png rename to SharedResources/Assets.xcassets/actions.imageset/actions@3x.png diff --git a/Cryptomator/Assets.xcassets/bot-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/bot-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/bot-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/bot-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault.png b/SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault.png rename to SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault.png diff --git a/Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault@2x.png b/SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault@2x.png rename to SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault@3x.png b/SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot-vault.imageset/bot-vault@3x.png rename to SharedResources/Assets.xcassets/bot-vault.imageset/bot-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/bot.imageset/Contents.json b/SharedResources/Assets.xcassets/bot.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/bot.imageset/Contents.json rename to SharedResources/Assets.xcassets/bot.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/bot.imageset/bot.png b/SharedResources/Assets.xcassets/bot.imageset/bot.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot.imageset/bot.png rename to SharedResources/Assets.xcassets/bot.imageset/bot.png diff --git a/Cryptomator/Assets.xcassets/bot.imageset/bot@2x.png b/SharedResources/Assets.xcassets/bot.imageset/bot@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot.imageset/bot@2x.png rename to SharedResources/Assets.xcassets/bot.imageset/bot@2x.png diff --git a/Cryptomator/Assets.xcassets/bot.imageset/bot@3x.png b/SharedResources/Assets.xcassets/bot.imageset/bot@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/bot.imageset/bot@3x.png rename to SharedResources/Assets.xcassets/bot.imageset/bot@3x.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected.png b/SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected.png rename to SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@2x.png b/SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@2x.png rename to SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@3x.png b/SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@3x.png rename to SharedResources/Assets.xcassets/dropbox-vault-selected.imageset/dropbox-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/dropbox-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/dropbox-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault.png b/SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault.png rename to SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@2x.png b/SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@2x.png rename to SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@3x.png b/SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@3x.png rename to SharedResources/Assets.xcassets/dropbox-vault.imageset/dropbox-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/dropbox.imageset/Contents.json b/SharedResources/Assets.xcassets/dropbox.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox.imageset/Contents.json rename to SharedResources/Assets.xcassets/dropbox.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/dropbox.imageset/dropbox.png b/SharedResources/Assets.xcassets/dropbox.imageset/dropbox.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox.imageset/dropbox.png rename to SharedResources/Assets.xcassets/dropbox.imageset/dropbox.png diff --git a/Cryptomator/Assets.xcassets/dropbox.imageset/dropbox@2x.png b/SharedResources/Assets.xcassets/dropbox.imageset/dropbox@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox.imageset/dropbox@2x.png rename to SharedResources/Assets.xcassets/dropbox.imageset/dropbox@2x.png diff --git a/Cryptomator/Assets.xcassets/dropbox.imageset/dropbox@3x.png b/SharedResources/Assets.xcassets/dropbox.imageset/dropbox@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/dropbox.imageset/dropbox@3x.png rename to SharedResources/Assets.xcassets/dropbox.imageset/dropbox@3x.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected.png b/SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected.png rename to SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@2x.png b/SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@2x.png rename to SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@3x.png b/SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@3x.png rename to SharedResources/Assets.xcassets/file-provider-vault-selected.imageset/file-provider-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/file-provider-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/file-provider-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault.png b/SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault.png rename to SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@2x.png b/SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@2x.png rename to SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@3x.png b/SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@3x.png rename to SharedResources/Assets.xcassets/file-provider-vault.imageset/file-provider-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/file-provider.imageset/Contents.json b/SharedResources/Assets.xcassets/file-provider.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider.imageset/Contents.json rename to SharedResources/Assets.xcassets/file-provider.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/file-provider.imageset/file-provider.png b/SharedResources/Assets.xcassets/file-provider.imageset/file-provider.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider.imageset/file-provider.png rename to SharedResources/Assets.xcassets/file-provider.imageset/file-provider.png diff --git a/Cryptomator/Assets.xcassets/file-provider.imageset/file-provider@2x.png b/SharedResources/Assets.xcassets/file-provider.imageset/file-provider@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider.imageset/file-provider@2x.png rename to SharedResources/Assets.xcassets/file-provider.imageset/file-provider@2x.png diff --git a/Cryptomator/Assets.xcassets/file-provider.imageset/file-provider@3x.png b/SharedResources/Assets.xcassets/file-provider.imageset/file-provider@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-provider.imageset/file-provider@3x.png rename to SharedResources/Assets.xcassets/file-provider.imageset/file-provider@3x.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected.png b/SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected.png rename to SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@2x.png b/SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@2x.png rename to SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@3x.png b/SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@3x.png rename to SharedResources/Assets.xcassets/file-type-unknown-selected.imageset/file-type-unknown-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown.imageset/Contents.json b/SharedResources/Assets.xcassets/file-type-unknown.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown.imageset/Contents.json rename to SharedResources/Assets.xcassets/file-type-unknown.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown.png b/SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown.png rename to SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@2x.png b/SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@2x.png rename to SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@2x.png diff --git a/Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@3x.png b/SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@3x.png rename to SharedResources/Assets.xcassets/file-type-unknown.imageset/file-type-unknown@3x.png diff --git a/Cryptomator/Assets.xcassets/folder-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/folder-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/folder-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/folder-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected.png b/SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected.png rename to SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected.png diff --git a/Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected@2x.png b/SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected@2x.png rename to SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected@3x.png b/SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder-selected.imageset/folder-selected@3x.png rename to SharedResources/Assets.xcassets/folder-selected.imageset/folder-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/folder.imageset/Contents.json b/SharedResources/Assets.xcassets/folder.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/folder.imageset/Contents.json rename to SharedResources/Assets.xcassets/folder.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/folder.imageset/folder.png b/SharedResources/Assets.xcassets/folder.imageset/folder.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder.imageset/folder.png rename to SharedResources/Assets.xcassets/folder.imageset/folder.png diff --git a/Cryptomator/Assets.xcassets/folder.imageset/folder@2x.png b/SharedResources/Assets.xcassets/folder.imageset/folder@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder.imageset/folder@2x.png rename to SharedResources/Assets.xcassets/folder.imageset/folder@2x.png diff --git a/Cryptomator/Assets.xcassets/folder.imageset/folder@3x.png b/SharedResources/Assets.xcassets/folder.imageset/folder@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/folder.imageset/folder@3x.png rename to SharedResources/Assets.xcassets/folder.imageset/folder@3x.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected.png b/SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected.png rename to SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@2x.png b/SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@2x.png rename to SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@3x.png b/SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@3x.png rename to SharedResources/Assets.xcassets/google-drive-vault-selected.imageset/google-drive-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/google-drive-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/google-drive-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault.png b/SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault.png rename to SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@2x.png b/SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@2x.png rename to SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@3x.png b/SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@3x.png rename to SharedResources/Assets.xcassets/google-drive-vault.imageset/google-drive-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/google-drive.imageset/Contents.json b/SharedResources/Assets.xcassets/google-drive.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive.imageset/Contents.json rename to SharedResources/Assets.xcassets/google-drive.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/google-drive.imageset/google-drive.png b/SharedResources/Assets.xcassets/google-drive.imageset/google-drive.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive.imageset/google-drive.png rename to SharedResources/Assets.xcassets/google-drive.imageset/google-drive.png diff --git a/Cryptomator/Assets.xcassets/google-drive.imageset/google-drive@2x.png b/SharedResources/Assets.xcassets/google-drive.imageset/google-drive@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive.imageset/google-drive@2x.png rename to SharedResources/Assets.xcassets/google-drive.imageset/google-drive@2x.png diff --git a/Cryptomator/Assets.xcassets/google-drive.imageset/google-drive@3x.png b/SharedResources/Assets.xcassets/google-drive.imageset/google-drive@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/google-drive.imageset/google-drive@3x.png rename to SharedResources/Assets.xcassets/google-drive.imageset/google-drive@3x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected.png b/SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected.png rename to SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@2x.png b/SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@2x.png rename to SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@3x.png b/SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@3x.png rename to SharedResources/Assets.xcassets/icloud-drive-vault-selected.imageset/icloud-drive-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/icloud-drive-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/icloud-drive-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault.png b/SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault.png rename to SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@2x.png b/SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@2x.png rename to SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@3x.png b/SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@3x.png rename to SharedResources/Assets.xcassets/icloud-drive-vault.imageset/icloud-drive-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive.imageset/Contents.json b/SharedResources/Assets.xcassets/icloud-drive.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive.imageset/Contents.json rename to SharedResources/Assets.xcassets/icloud-drive.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive.png b/SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive.png rename to SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive@2x.png b/SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive@2x.png rename to SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive@2x.png diff --git a/Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive@3x.png b/SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/icloud-drive.imageset/icloud-drive@3x.png rename to SharedResources/Assets.xcassets/icloud-drive.imageset/icloud-drive@3x.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected.png b/SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected.png rename to SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@2x.png b/SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@2x.png rename to SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@3x.png b/SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@3x.png rename to SharedResources/Assets.xcassets/onedrive-vault-selected.imageset/onedrive-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/onedrive-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/onedrive-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault.png b/SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault.png rename to SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@2x.png b/SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@2x.png rename to SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@3x.png b/SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@3x.png rename to SharedResources/Assets.xcassets/onedrive-vault.imageset/onedrive-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/onedrive.imageset/Contents.json b/SharedResources/Assets.xcassets/onedrive.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive.imageset/Contents.json rename to SharedResources/Assets.xcassets/onedrive.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/onedrive.imageset/onedrive.png b/SharedResources/Assets.xcassets/onedrive.imageset/onedrive.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive.imageset/onedrive.png rename to SharedResources/Assets.xcassets/onedrive.imageset/onedrive.png diff --git a/Cryptomator/Assets.xcassets/onedrive.imageset/onedrive@2x.png b/SharedResources/Assets.xcassets/onedrive.imageset/onedrive@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive.imageset/onedrive@2x.png rename to SharedResources/Assets.xcassets/onedrive.imageset/onedrive@2x.png diff --git a/Cryptomator/Assets.xcassets/onedrive.imageset/onedrive@3x.png b/SharedResources/Assets.xcassets/onedrive.imageset/onedrive@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/onedrive.imageset/onedrive@3x.png rename to SharedResources/Assets.xcassets/onedrive.imageset/onedrive@3x.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/webdav-vault-selected.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/Contents.json rename to SharedResources/Assets.xcassets/webdav-vault-selected.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected.png b/SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected.png rename to SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@2x.png b/SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@2x.png rename to SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@2x.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@3x.png b/SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@3x.png rename to SharedResources/Assets.xcassets/webdav-vault-selected.imageset/webdav-vault-selected@3x.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/webdav-vault.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault.imageset/Contents.json rename to SharedResources/Assets.xcassets/webdav-vault.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault.png b/SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault.png rename to SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault@2x.png b/SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault@2x.png rename to SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault@2x.png diff --git a/Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault@3x.png b/SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav-vault.imageset/webdav-vault@3x.png rename to SharedResources/Assets.xcassets/webdav-vault.imageset/webdav-vault@3x.png diff --git a/Cryptomator/Assets.xcassets/webdav.imageset/Contents.json b/SharedResources/Assets.xcassets/webdav.imageset/Contents.json similarity index 100% rename from Cryptomator/Assets.xcassets/webdav.imageset/Contents.json rename to SharedResources/Assets.xcassets/webdav.imageset/Contents.json diff --git a/Cryptomator/Assets.xcassets/webdav.imageset/webdav.png b/SharedResources/Assets.xcassets/webdav.imageset/webdav.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav.imageset/webdav.png rename to SharedResources/Assets.xcassets/webdav.imageset/webdav.png diff --git a/Cryptomator/Assets.xcassets/webdav.imageset/webdav@2x.png b/SharedResources/Assets.xcassets/webdav.imageset/webdav@2x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav.imageset/webdav@2x.png rename to SharedResources/Assets.xcassets/webdav.imageset/webdav@2x.png diff --git a/Cryptomator/Assets.xcassets/webdav.imageset/webdav@3x.png b/SharedResources/Assets.xcassets/webdav.imageset/webdav@3x.png similarity index 100% rename from Cryptomator/Assets.xcassets/webdav.imageset/webdav@3x.png rename to SharedResources/Assets.xcassets/webdav.imageset/webdav@3x.png diff --git a/Cryptomator/Colors.xcassets/Contents.json b/SharedResources/Colors.xcassets/Contents.json similarity index 100% rename from Cryptomator/Colors.xcassets/Contents.json rename to SharedResources/Colors.xcassets/Contents.json diff --git a/Cryptomator/Colors.xcassets/primary-D1.colorset/Contents.json b/SharedResources/Colors.xcassets/primary-D1.colorset/Contents.json similarity index 100% rename from Cryptomator/Colors.xcassets/primary-D1.colorset/Contents.json rename to SharedResources/Colors.xcassets/primary-D1.colorset/Contents.json diff --git a/Cryptomator/Colors.xcassets/primary.colorset/Contents.json b/SharedResources/Colors.xcassets/primary.colorset/Contents.json similarity index 100% rename from Cryptomator/Colors.xcassets/primary.colorset/Contents.json rename to SharedResources/Colors.xcassets/primary.colorset/Contents.json diff --git a/Cryptomator/Colors.xcassets/secondaryLabel.colorset/Contents.json b/SharedResources/Colors.xcassets/secondaryLabel.colorset/Contents.json similarity index 100% rename from Cryptomator/Colors.xcassets/secondaryLabel.colorset/Contents.json rename to SharedResources/Colors.xcassets/secondaryLabel.colorset/Contents.json diff --git a/Cryptomator/Colors.xcassets/yellow.colorset/Contents.json b/SharedResources/Colors.xcassets/yellow.colorset/Contents.json similarity index 100% rename from Cryptomator/Colors.xcassets/yellow.colorset/Contents.json rename to SharedResources/Colors.xcassets/yellow.colorset/Contents.json diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 87a99d411..31a987895 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,7 +1,5 @@ -Welcome to the first alpha version of the new Cryptomator for iOS app! +Welcome to the first beta version of the new Cryptomator for iOS app! This new app is fully integrated into the Files app of iOS. -This new app is fully integrated into the Files app. - -Be aware that a lot of things aren't working yet. E.g., you can't use iCloud Drive and "other" file providers yet. +Keep in mind that this is a pre-release version intended for testing and that some features like auto-lock are still missing. Check out our open-source repository on GitHub: https://github.com/cryptomator/ios We're looking forward to your feedback! \ No newline at end of file