diff --git a/DuckDuckGo/DataImport/DataImport.swift b/DuckDuckGo/DataImport/DataImport.swift index 5d56c60b02..0c92606062 100644 --- a/DuckDuckGo/DataImport/DataImport.swift +++ b/DuckDuckGo/DataImport/DataImport.swift @@ -338,7 +338,14 @@ enum DataImport { } static func < (lhs: DataImport.BrowserProfile, rhs: DataImport.BrowserProfile) -> Bool { - return lhs.profileName.localizedCompare(rhs.profileName) == .orderedAscending + // first sort by profiles folder name if multiple profiles folders are present (Chrome, Chrome Canary…) + let profilesDirName1 = lhs.profileURL.deletingLastPathComponent().lastPathComponent + let profilesDirName2 = rhs.profileURL.deletingLastPathComponent().lastPathComponent + if profilesDirName1 == profilesDirName2 { + return lhs.profileName.localizedCompare(rhs.profileName) == .orderedAscending + } else { + return profilesDirName1.localizedCompare(profilesDirName2) == .orderedAscending + } } static func == (lhs: DataImport.BrowserProfile, rhs: DataImport.BrowserProfile) -> Bool { diff --git a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift index 3eae669134..68539c3137 100644 --- a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift +++ b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift @@ -179,11 +179,14 @@ enum ThirdPartyBrowser: CaseIterable { func browserProfiles(applicationSupportURL: URL? = nil) -> DataImport.BrowserProfileList { var potentialProfileURLs: [URL] { - guard let profilePath = profilesDirectory(applicationSupportURL: applicationSupportURL) else { return [] } - let potentialProfileURLs = (try? FileManager.default.contentsOfDirectory(at: profilePath, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles]).filter(\.hasDirectoryPath)) ?? [] - return potentialProfileURLs + [profilePath] + let fm = FileManager() + let profilesDirectories = self.profilesDirectories(applicationSupportURL: applicationSupportURL) + return profilesDirectories.reduce(into: []) { result, profilesDir in + result.append(contentsOf: (try? fm.contentsOfDirectory(at: profilesDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles]) + .filter(\.hasDirectoryPath)) ?? []) + } + profilesDirectories } let profiles: [DataImport.BrowserProfile] @@ -191,7 +194,7 @@ enum ThirdPartyBrowser: CaseIterable { case .safari, .safariTechnologyPreview: // Safari is an exception, as it may need permissions granted before being able to read the contents of the profile path. To be safe, // return the profile anyway and check the file system permissions when preparing to import. - guard let profileURL = profilesDirectory(applicationSupportURL: applicationSupportURL) else { + guard let profileURL = profilesDirectories(applicationSupportURL: applicationSupportURL).first else { assertionFailure("Unexpected nil profileURL for Safari") profiles = [] break @@ -239,23 +242,28 @@ enum ThirdPartyBrowser: CaseIterable { // Returns the URL to the profiles for a given browser. This directory will contain a list of directories, each representing a profile. // swiftlint:disable:next cyclomatic_complexity - func profilesDirectory(applicationSupportURL: URL? = nil) -> URL? { + func profilesDirectories(applicationSupportURL: URL? = nil) -> [URL] { let applicationSupportURL = applicationSupportURL ?? URL.nonSandboxApplicationSupportDirectoryURL - switch self { - case .brave: return applicationSupportURL.appendingPathComponent("BraveSoftware/Brave-Browser/") - case .chrome: return applicationSupportURL.appendingPathComponent("Google/Chrome/") - case .chromium: return applicationSupportURL.appendingPathComponent("Chromium/") - case .coccoc: return applicationSupportURL.appendingPathComponent("Coccoc/") - case .edge: return applicationSupportURL.appendingPathComponent("Microsoft Edge/") - case .firefox: return applicationSupportURL.appendingPathComponent("Firefox/Profiles/") - case .opera: return applicationSupportURL.appendingPathComponent("com.operasoftware.Opera/") - case .operaGX: return applicationSupportURL.appendingPathComponent("com.operasoftware.OperaGX/") - case .safari: return URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("Safari/") - case .safariTechnologyPreview: return URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("SafariTechnologyPreview/") - case .tor: return applicationSupportURL.appendingPathComponent("TorBrowser-Data/Browser/") - case .vivaldi: return applicationSupportURL.appendingPathComponent("Vivaldi/") - case .yandex: return applicationSupportURL.appendingPathComponent("Yandex/YandexBrowser/") - case .bitwarden, .lastPass, .onePassword7, .onePassword8: return nil + return switch self { + case .brave: [applicationSupportURL.appendingPathComponent("BraveSoftware/Brave-Browser/")] + case .chrome: [ + applicationSupportURL.appendingPathComponent("Google/Chrome/"), + applicationSupportURL.appendingPathComponent("Google/Chrome Beta/"), + applicationSupportURL.appendingPathComponent("Google/Chrome Dev/"), + applicationSupportURL.appendingPathComponent("Google/Chrome Canary/"), + ] + case .chromium: [applicationSupportURL.appendingPathComponent("Chromium/")] + case .coccoc: [applicationSupportURL.appendingPathComponent("Coccoc/")] + case .edge: [applicationSupportURL.appendingPathComponent("Microsoft Edge/")] + case .firefox: [applicationSupportURL.appendingPathComponent("Firefox/Profiles/")] + case .opera: [applicationSupportURL.appendingPathComponent("com.operasoftware.Opera/")] + case .operaGX: [applicationSupportURL.appendingPathComponent("com.operasoftware.OperaGX/")] + case .safari: [URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("Safari/")] + case .safariTechnologyPreview: [URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("SafariTechnologyPreview/")] + case .tor: [applicationSupportURL.appendingPathComponent("TorBrowser-Data/Browser/")] + case .vivaldi: [applicationSupportURL.appendingPathComponent("Vivaldi/")] + case .yandex: [applicationSupportURL.appendingPathComponent("Yandex/YandexBrowser/")] + case .bitwarden, .lastPass, .onePassword7, .onePassword8: [] } } diff --git a/DuckDuckGo/DataImport/View/DataImportProfilePicker.swift b/DuckDuckGo/DataImport/View/DataImportProfilePicker.swift index 58da32140b..f25e5725a9 100644 --- a/DuckDuckGo/DataImport/View/DataImportProfilePicker.swift +++ b/DuckDuckGo/DataImport/View/DataImportProfilePicker.swift @@ -22,10 +22,14 @@ struct DataImportProfilePicker: View { private let profiles: [DataImport.BrowserProfile] @Binding private var selectedProfile: DataImport.BrowserProfile? + private let shouldDisplayFolderName: Bool init(profileList: DataImport.BrowserProfileList?, selectedProfile: Binding) { self.profiles = profileList?.validImportableProfiles ?? [] self._selectedProfile = selectedProfile + shouldDisplayFolderName = Set(self.profiles.map { + $0.profileURL.deletingLastPathComponent() + }).count > 1 } var body: some View { @@ -39,7 +43,16 @@ struct DataImportProfilePicker: View { selectedProfile = profiles[safe: $0] }) { ForEach(profiles.indices, id: \.self) { idx in - Text(profiles[idx].profileName) + // display profiles folder name if multiple profiles folders are present (Chrome, Chrome Canary…) + if shouldDisplayFolderName { + Text(profiles[idx].profileName + " ") + + Text(profiles[idx].profileURL + .deletingLastPathComponent().lastPathComponent) + .font(.system(size: 10)) + .fontWeight(.light) + } else { + Text(profiles[idx].profileName) + } } } label: {} .pickerStyle(.menu) @@ -52,11 +65,11 @@ struct DataImportProfilePicker: View { #Preview { DataImportProfilePicker(profileList: .init(browser: .chrome, profiles: [ .init(browser: .chrome, - profileURL: URL(fileURLWithPath: "/test/Default Profile")), + profileURL: URL(fileURLWithPath: "/Chrome/Default Profile")), .init(browser: .chrome, - profileURL: URL(fileURLWithPath: "/test/Profile 1")), + profileURL: URL(fileURLWithPath: "/Chrome Dev/Profile 1")), .init(browser: .chrome, - profileURL: URL(fileURLWithPath: "/test/Profile 2")), + profileURL: URL(fileURLWithPath: "/Chrome Canary/Profile 2")), ], validateProfileData: { _ in { .init(logins: .available, bookmarks: .available) } }), selectedProfile: Binding { .init(browser: .chrome, profileURL: URL(fileURLWithPath: "/test/Profile 1"))