diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 0832cbb564..73450674f0 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -59,6 +59,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel private var appIconChanger: AppIconChanger! private(set) var syncService: DDGSyncing! private(set) var syncPersistence: SyncDataPersistor! + let bookmarksManager = LocalBookmarkManager.shared #if !APPSTORE var updateController: UpdateController! @@ -156,7 +157,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel HistoryCoordinator.shared.loadHistory() PrivacyFeatures.httpsUpgrade.loadDataAsync() - LocalBookmarkManager.shared.loadBookmarks() + bookmarksManager.loadBookmarks() FaviconManager.shared.loadFavicons() ConfigurationManager.shared.start() FileDownloadManager.shared.delegate = self diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.xib b/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.xib index 0a2684801a..5f156db026 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.xib +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.xib @@ -1,8 +1,7 @@ - + - - + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 35b356b7f1..963b9cf67d 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -202,8 +202,11 @@ struct UserText { static let passwordManagementLock = NSLocalizedString("passsword.management.lock", value: "Lock", comment: "Lock Logins Vault menu") static let passwordManagementUnlock = NSLocalizedString("passsword.management.unlock", value: "Unlock", comment: "Unlock Logins Vault menu") - static let importBrowserData = NSLocalizedString("import.browser.data", value: "Import Bookmarks and Passwords…", comment: "Opens Import Browser Data dialog") - +// static let importBrowserData = NSLocalizedString("import.browser.data", value: "Import Bookmarks and Passwords…", comment: "Opens Import Browser Data dialog") + static let importBookmarks = NSLocalizedString("import.browser.data", value: "Import Bookmarks…", comment: "Opens Import Browser Data dialog") + static let importPasswords = NSLocalizedString("import.browser.data", value: "Import Passwords…", comment: "Opens Import Browser Data dialog") + static let exportLogins = NSLocalizedString("export.logins.data", value: "Export Passwords…", comment: "Opens Export Logins Data dialog") + static let exportBookmarks = NSLocalizedString("export.bookmarks.menu.item", value: "Export Bookmarks…", comment: "Export bookmarks menu item") static let bookmarks = NSLocalizedString("bookmarks", value: "Bookmarks", comment: "Button for bookmarks") static let favorites = NSLocalizedString("favorites", value: "Favorites", comment: "Title text for the Favorites menu item") static let bookmarksOpenInNewTabs = NSLocalizedString("bookmarks.open.in.new.tabs", value: "Open in New Tabs", comment: "Open all bookmarks in folder in new tabs") @@ -492,12 +495,12 @@ struct UserText { static let downloadsActiveAlertMessageFormat = NSLocalizedString("downloads.active.alert.message.format", value: "Are you sure you want to quit? DuckDuckGo Privacy Browser is currently downloading “%@”%@. If you quit now DuckDuckGo Privacy Browser won’t finish downloading this file.", comment: "Alert text format when trying to quit application while file “filename”[, and others] are being downloaded") static let downloadsActiveAlertMessageAndOthers = NSLocalizedString("downloads.active.alert.message.and.others", value: ", and other files", comment: "Alert text format element for “, and other files”") - static let exportLoginsFailedMessage = NSLocalizedString("export.logins.failed.message", value: "Failed to Export Logins", comment: "Alert title when exporting login data fails") + static let exportLoginsFailedMessage = NSLocalizedString("export.logins.failed.message", value: "Failed to Export Passwords", comment: "Alert title when exporting login data fails") static let exportLoginsFailedInformative = NSLocalizedString("export.logins.failed.informative", value: "Please check that no file exists at the location you selected.", comment: "Alert message when exporting login data fails") - static let exportBookmarksFailedMessage = NSLocalizedString("export.bookmarks.failed.message", value: "Failed to Export Bookmarks", comment: "Alert title when exporting login data fails") + static let exportBookmarksFailedMessage = NSLocalizedString("export.bookmarks.failed.message", value: "Failed to Export Bookmarks…", comment: "Alert title when exporting login data fails") static let exportBookmarksFailedInformative = NSLocalizedString("export.bookmarks.failed.informative", value: "Please check that no file exists at the location you selected.", comment: "Alert message when exporting bookmarks fails") - static let exportLoginsFileNameSuffix = NSLocalizedString("export.logins.file.name.suffix", value: "Logins", comment: "The last part of the suggested file name for exporting logins") + static let exportLoginsFileNameSuffix = NSLocalizedString("export.logins.file.name.suffix", value: "Passwords", comment: "The last part of the suggested file name for exporting logins") static let exportBookmarksFileNameSuffix = NSLocalizedString("export.bookmarks.file.name.suffix", value: "Bookmarks", comment: "The last part of the suggested file for exporting bookmarks") static let exportLoginsWarning = NSLocalizedString("export.logins.warning", value: "This file contains your passwords in plain text and should be saved in a secure location and deleted when you are done.\nAnyone with access to this file will be able to read your passwords.", comment: "Warning text presented when exporting logins.") diff --git a/DuckDuckGo/HomePage/View/HomePage.storyboard b/DuckDuckGo/HomePage/View/HomePage.storyboard index 7c0909b0db..fec70fd10a 100644 --- a/DuckDuckGo/HomePage/View/HomePage.storyboard +++ b/DuckDuckGo/HomePage/View/HomePage.storyboard @@ -1,7 +1,7 @@ - + - + diff --git a/DuckDuckGo/Menus/MainMenu.storyboard b/DuckDuckGo/Menus/MainMenu.storyboard index 5712a37f37..49c660ae00 100644 --- a/DuckDuckGo/Menus/MainMenu.storyboard +++ b/DuckDuckGo/Menus/MainMenu.storyboard @@ -7,7 +7,11 @@ - + + + + + @@ -126,7 +130,7 @@ - + @@ -544,12 +548,18 @@ CA - + + + + + + + @@ -823,6 +833,7 @@ CQ + diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index ac8453ca10..05bf853e67 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -65,6 +65,7 @@ final class MainMenu: NSMenu { @IBOutlet weak var manageBookmarksMenuItem: NSMenuItem! @IBOutlet weak var bookmarksMenuToggleBookmarksBarMenuItem: NSMenuItem? @IBOutlet weak var importBookmarksMenuItem: NSMenuItem! + @IBOutlet weak var exportBookmarksMenuItem: NSMenuItem! @IBOutlet weak var bookmarksMenuItem: NSMenuItem? @IBOutlet weak var bookmarkThisPageMenuItem: NSMenuItem? @IBOutlet weak var favoritesMenuItem: NSMenuItem? diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 5050760827..847b9db8a1 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -803,11 +803,43 @@ extension AppDelegate: NSMenuItemValidation { case #selector(AppDelegate.reopenAllWindowsFromLastSession(_:)): return stateRestorationManager.canRestoreLastSessionState + // Enables and disables export bookmarks itemz + case #selector(AppDelegate.openExportBookmarks(_:)): + return bookmarksManager.list?.totalBookmarks != 0 + + // Enables and disables export passwords items + case #selector(AppDelegate.openExportLogins(_:)): + return areTherePasswords + default: return true } } + private var areTherePasswords: Bool { + let vault = try? SecureVaultFactory.default.makeVault(errorReporter: SecureVaultErrorReporter.shared) + guard let vault else { + return false + } + let accounts = (try? vault.accounts()) ?? [] + if !accounts.isEmpty { + return true + } + let cards = (try? vault.creditCards()) ?? [] + if !cards.isEmpty { + return true + } + let notes = (try? vault.notes()) ?? [] + if !notes.isEmpty { + return true + } + let identities = (try? vault.identities()) ?? [] + if !identities.isEmpty { + return true + } + return false + } + } extension MainViewController: FindInPageDelegate { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index c1de8881f0..401010d566 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -28,6 +28,7 @@ protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedToggleBookmarksBar(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkImportInterface(_ menu: NSMenu) + func optionsButtonMenuRequestedBookmarkExportInterface(_ menu: NSMenu) func optionsButtonMenuRequestedLoginsPopover(_ menu: NSMenu, selectedCategory: SecureVaultSorting.Category) func optionsButtonMenuRequestedOpenExternalPasswordManager(_ menu: NSMenu) func optionsButtonMenuRequestedDownloadsPopover(_ menu: NSMenu) @@ -145,6 +146,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedBookmarkImportInterface(self) } + @objc func openBookmarkExportInterface(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedBookmarkExportInterface(self) + } + @objc func openDownloads(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedDownloadsPopover(self) } @@ -428,7 +433,8 @@ final class BookmarksSubMenu: NSMenu { addItem(NSMenuItem.separator()) } - guard let entities = LocalBookmarkManager.shared.list?.topLevelEntities else { + let bookmarkManager = LocalBookmarkManager.shared + guard let entities = bookmarkManager.list?.topLevelEntities else { return } @@ -439,8 +445,13 @@ final class BookmarksSubMenu: NSMenu { addItem(NSMenuItem.separator()) - addItem(withTitle: UserText.importBrowserData, action: #selector(MoreOptionsMenu.openBookmarkImportInterface(_:)), keyEquivalent: "") + addItem(withTitle: UserText.importBookmarks, action: #selector(MoreOptionsMenu.openBookmarkImportInterface(_:)), keyEquivalent: "") .targetting(target) + + let exportBookmarItem = NSMenuItem(title: UserText.exportBookmarks, action: #selector(MoreOptionsMenu.openBookmarkExportInterface(_:)), keyEquivalent: "") + exportBookmarItem.isEnabled = bookmarkManager.list?.totalBookmarks != 0 + addItem(exportBookmarItem) + } private func bookmarkMenuItems(from bookmarkViewModels: [BookmarkViewModel], topLevel: Bool = true) -> [NSMenuItem] { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index d90015d5c5..aca4a16d11 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -1,7 +1,7 @@ - + - + diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 16a24d44be..5f562905f4 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -703,6 +703,10 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { DataImportViewController.show() } + func optionsButtonMenuRequestedBookmarkExportInterface(_ menu: NSMenu) { + NSApp.sendAction(#selector(AppDelegate.openExportBookmarks(_:)), to: nil, from: nil) + } + func optionsButtonMenuRequestedLoginsPopover(_ menu: NSMenu, selectedCategory: SecureVaultSorting.Category) { popovers.showPasswordManagementPopover(selectedCategory: selectedCategory, usingView: passwordManagementButton, diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index 7e555894d7..2e0f8bfc40 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -102,6 +102,10 @@ final class AutofillPreferencesModel: ObservableObject { NSApp.sendAction(#selector(AppDelegate.openImportBrowserDataWindow(_:)), to: nil, from: nil) } + func openExportLogins() { + NSApp.sendAction(#selector(AppDelegate.openExportLogins(_:)), to: nil, from: nil) + } + @MainActor func showAutofillPopover() { guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController else { return } diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 8a85d80e8d..9fb9276f70 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -71,9 +71,12 @@ extension Preferences { model.showAutofillPopover() } #if APPSTORE - Button(UserText.importBrowserData) { + Button(UserText.importPasswords) { model.openImportBrowserDataWindow() } + Button(UserText.exportLogins) { + model.openExportLogins() + } #endif } @@ -94,9 +97,12 @@ extension Preferences { } } Spacer() - Button(UserText.importBrowserData) { + Button(UserText.importPasswords) { model.openImportBrowserDataWindow() } + Button(UserText.exportLogins) { + model.openExportLogins() + } } #endif diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index 9ec8cf7e66..1147ca4b03 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -53,7 +53,7 @@ final class PasswordManagementViewController: NSViewController { @IBOutlet var emptyStateTitle: NSTextField! @IBOutlet var emptyStateMessage: NSTextField! @IBOutlet var emptyStateButton: NSButton! - + @IBOutlet weak var exportLoginItem: NSMenuItem! @IBOutlet var lockScreen: NSView! @IBOutlet var lockScreenIconImageView: NSImageView! { didSet { @@ -161,6 +161,8 @@ final class PasswordManagementViewController: NSViewController { addVaultItemButton.sendAction(on: .leftMouseDown) moreButton.sendAction(on: .leftMouseDown) + exportLoginItem.title = UserText.exportLogins + NotificationCenter.default.addObserver(forName: .deviceBecameLocked, object: nil, queue: .main) { [weak self] _ in self?.displayLockScreen() } @@ -258,6 +260,11 @@ final class PasswordManagementViewController: NSViewController { NSApp.sendAction(#selector(openImportBrowserDataWindow(_:)), to: nil, from: sender) } + @IBAction func openExportLogins(_ sender: Any) { + self.dismiss() + NSApp.sendAction(#selector(AppDelegate.openExportLogins(_:)), to: nil, from: sender) + } + @IBAction func onImportClicked(_ sender: NSButton) { self.dismiss() DataImportViewController.show() diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index e84ffcd737..02099f612e 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -352,6 +352,7 @@ + @@ -371,12 +372,18 @@ - + + + + + + + diff --git a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift index 872a327c5b..f607b26560 100644 --- a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift +++ b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift @@ -20,6 +20,7 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { + var optionsButtonMenuRequestedPreferencesCalled = false var optionsButtonMenuRequestedAppearancePreferencesCalled = false @@ -67,4 +68,8 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { optionsButtonMenuRequestedAppearancePreferencesCalled = true } + func optionsButtonMenuRequestedBookmarkExportInterface(_ menu: NSMenu) { + + } + }