Skip to content

Commit

Permalink
Bookmark All Tabs (#2683)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1206339769350174/f

Description:
This PR adds the capability to bookmark all tabs in one step.
  • Loading branch information
alessandroboron authored Apr 24, 2024
1 parent 99408c1 commit 4441fba
Show file tree
Hide file tree
Showing 50 changed files with 2,580 additions and 97 deletions.
100 changes: 100 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion DuckDuckGo/Bookmarks/Model/BookmarkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ protocol BookmarkManager: AnyObject {
func allHosts() -> Set<String>
func getBookmark(for url: URL) -> Bookmark?
func getBookmark(forUrl url: String) -> Bookmark?
func getBookmarkFolder(withId id: String) -> BookmarkFolder?
@discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark?
@discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool, index: Int?, parent: BookmarkFolder?) -> Bookmark?
func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType)
func makeFolder(for title: String, parent: BookmarkFolder?, completion: @escaping (BookmarkFolder) -> Void)
func remove(bookmark: Bookmark)
func remove(folder: BookmarkFolder)
Expand All @@ -46,7 +48,6 @@ protocol BookmarkManager: AnyObject {
func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void)
func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void)
func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary

func handleFavoritesAfterDisablingSync()

// Wrapper definition in a protocol is not supported yet
Expand Down Expand Up @@ -138,6 +139,10 @@ final class LocalBookmarkManager: BookmarkManager {
return list?[url]
}

func getBookmarkFolder(withId id: String) -> BookmarkFolder? {
bookmarkStore.bookmarkFolder(withId: id)
}

@discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? {
makeBookmark(for: url, title: title, isFavorite: isFavorite, index: nil, parent: nil)
}
Expand Down Expand Up @@ -167,6 +172,12 @@ final class LocalBookmarkManager: BookmarkManager {
return bookmark
}

func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) {
bookmarkStore.saveBookmarks(for: websitesInfo, inNewFolderNamed: folderName, withinParentFolder: parent)
loadBookmarks()
requestSync()
}

func remove(bookmark: Bookmark) {
guard list != nil else { return }
guard let latestBookmark = getBookmark(forUrl: bookmark.url) else {
Expand Down
8 changes: 5 additions & 3 deletions DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@

import Foundation

struct WebsiteInfo {
struct WebsiteInfo: Equatable {
let url: URL
let title: String?
/// Returns the title of the website if available, otherwise returns the domain of the URL.
/// If both title and and domain are nil, it returns the absolute string representation of the URL.
let title: String

init?(_ tab: Tab) {
guard case let .url(url, _, _) = tab.content else {
return nil
}
self.url = url
self.title = tab.title
self.title = tab.title ?? url.host ?? url.absoluteString
}
}
3 changes: 2 additions & 1 deletion DuckDuckGo/Bookmarks/Services/BookmarkStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ protocol BookmarkStore {

func loadAll(type: BookmarkStoreFetchPredicateType, completion: @escaping ([BaseBookmarkEntity]?, Error?) -> Void)
func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void)
func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType)
func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void)
func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void)
func update(bookmark: Bookmark)
func bookmarkFolder(withId id: String) -> BookmarkFolder?
func update(folder: BookmarkFolder)
func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType)
func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void)
Expand All @@ -59,6 +61,5 @@ protocol BookmarkStore {
func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void)
func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void)
func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary

func handleFavoritesAfterDisablingSync()
}
19 changes: 19 additions & 0 deletions DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ public final class BookmarkStoreMock: BookmarkStore {
capturedBookmark = bookmark
}

var bookmarkFolderWithIdCalled = false
var capturedFolderId: String?
var bookmarkFolder: BookmarkFolder?
func bookmarkFolder(withId id: String) -> BookmarkFolder? {
bookmarkFolderWithIdCalled = true
capturedFolderId = id
return bookmarkFolder
}

var updateFolderCalled = false
func update(folder: BookmarkFolder) {
updateFolderCalled = true
Expand Down Expand Up @@ -133,6 +142,16 @@ public final class BookmarkStoreMock: BookmarkStore {
return BookmarksImportSummary(successful: 0, duplicates: 0, failed: 0)
}

var saveBookmarksInNewFolderNamedCalled = false
var capturedWebsitesInfo: [WebsiteInfo]?
var capturedNewFolderName: String?
func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) {
saveBookmarksInNewFolderNamedCalled = true
capturedWebsitesInfo = websitesInfo
capturedNewFolderName = folderName
capturedParentFolderType = parent
}

var canMoveObjectWithUUIDCalled = false
func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool {
canMoveObjectWithUUIDCalled = true
Expand Down
87 changes: 72 additions & 15 deletions DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ final class LocalBookmarkStore: BookmarkStore {
case missingRoot
case missingFavoritesRoot
case saveLoopError(Error?)
case badModelMapping
}

private(set) var favoritesDisplayMode: FavoritesDisplayMode
Expand Down Expand Up @@ -339,6 +340,23 @@ final class LocalBookmarkStore: BookmarkStore {
})
}

func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) {
do {
try applyChangesAndSave { context in
// Fetch Parent folder
let parentFolder = try bookmarkEntity(for: parent, in: context)
// Create new Folder for all bookmarks
let newFolderMO = BookmarkEntity.makeFolder(title: folderName, parent: parentFolder, context: context)
// Save the bookmarks
websitesInfo.forEach { info in
_ = BookmarkEntity.makeBookmark(title: info.title, url: info.url.absoluteString, parent: newFolderMO, context: context)
}
}
} catch {
commonOnSaveErrorHandler(error)
}
}

func remove(objectsWithUUIDs identifiers: [String], completion: @escaping (Bool, Error?) -> Void) {

applyChangesAndSave(changes: { [weak self] context in
Expand Down Expand Up @@ -390,6 +408,38 @@ final class LocalBookmarkStore: BookmarkStore {
}
}

func bookmarkFolder(withId id: String) -> BookmarkFolder? {
let context = makeContext()

var bookmarkFolderToReturn: BookmarkFolder?
let favoritesDisplayMode = self.favoritesDisplayMode

context.performAndWait {
let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: id)
do {
let folderFetchRequestResult = try context.fetch(folderFetchRequest)
guard let bookmarkFolderManagedObject = folderFetchRequestResult.first else { return }

guard let bookmarkFolder = BaseBookmarkEntity.from(
managedObject: bookmarkFolderManagedObject,
parentFolderUUID: bookmarkFolderManagedObject.parent?.uuid,
favoritesDisplayMode: favoritesDisplayMode
) as? BookmarkFolder
else {
throw BookmarkStoreError.badModelMapping
}
bookmarkFolderToReturn = bookmarkFolder

} catch BookmarkStoreError.badModelMapping {
os_log("Failed to map BookmarkEntity to BookmarkFolder, with error: %s", log: .bookmarks, type: .error)
} catch {
os_log("Failed to fetch last saved folder for bookmarks all tabs, with error: %s", log: .bookmarks, type: .error, error.localizedDescription)
}
}

return bookmarkFolderToReturn
}

func update(folder: BookmarkFolder) {
do {
_ = try applyChangesAndSave(changes: { [weak self] context in
Expand Down Expand Up @@ -998,32 +1048,38 @@ private extension LocalBookmarkStore {
}

func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws {
let newParentFolder = try bookmarkEntity(for: type, in: context)

if let index = index, index < newParentFolder.childrenArray.count {
self.move(entities: entities, to: index, within: newParentFolder)
} else {
for bookmarkManagedObject in entities {
bookmarkManagedObject.parent = nil
newParentFolder.addToChildren(bookmarkManagedObject)
}
}
}

func bookmarkEntity(for parentFolderType: ParentFolderType, in context: NSManagedObjectContext) throws -> BookmarkEntity {
guard let rootFolder = bookmarksRoot(in: context) else {
throw BookmarkStoreError.missingRoot
}

let newParentFolder: BookmarkEntity
let parentFolder: BookmarkEntity

switch type {
case .root: newParentFolder = rootFolder
case .parent(let newParentUUID):
let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID)
switch parentFolderType {
case .root:
parentFolder = rootFolder
case let .parent(parentUUID):
let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: parentUUID)

if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder {
newParentFolder = fetchedParent
parentFolder = fetchedParent
} else {
throw BookmarkStoreError.missingEntity
}
}

if let index = index, index < newParentFolder.childrenArray.count {
self.move(entities: entities, to: index, within: newParentFolder)
} else {
for bookmarkManagedObject in entities {
bookmarkManagedObject.parent = nil
newParentFolder.addToChildren(bookmarkManagedObject)
}
}
return parentFolder
}

}
Expand All @@ -1041,6 +1097,7 @@ extension LocalBookmarkStore.BookmarkStoreError: CustomNSError {
case .missingRoot: return 7
case .missingFavoritesRoot: return 8
case .saveLoopError: return 9
case .badModelMapping: return 10
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// UserDefaultsBookmarkFoldersStore.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

/// A type used to provide the ID of the folder where all tabs were last saved.
protocol BookmarkFoldersStore: AnyObject {
/// The ID of the folder where all bookmarks from the last session were saved.
var lastBookmarkAllTabsFolderIdUsed: String? { get set }
}

final class UserDefaultsBookmarkFoldersStore: BookmarkFoldersStore {

enum Keys {
static let bookmarkAllTabsFolderUsedKey = "bookmarks.all-tabs.last-used-folder"
}

private let userDefaults: UserDefaults

init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}

var lastBookmarkAllTabsFolderIdUsed: String? {
get {
userDefaults.string(forKey: Keys.bookmarkAllTabsFolderUsedKey)
}
set {
userDefaults.set(newValue, forKey: Keys.bookmarkAllTabsFolderUsedKey)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ struct AddBookmarkFolderPopoverView: ModalView {
isDefaultActionDisabled: model.isDefaultActionButtonDisabled,
defaultAction: { _ in model.addFolder() }
)
.padding(.vertical, 16.0)
.font(.system(size: 13))
.frame(width: 320)
}
Expand Down
1 change: 0 additions & 1 deletion DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ struct AddBookmarkPopoverView: View {
isDefaultActionDisabled: model.isDefaultActionButtonDisabled,
defaultAction: model.doneButtonAction
)
.padding(.vertical, 16.0)
.font(.system(size: 13))
.frame(width: 320)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct AddEditBookmarkDialogView: ModalView {
isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled,
defaultAction: viewModel.bookmarkModel.addOrSave
)
.frame(width: 448, height: 288)
.frame(width: 448)
}

private var addFolderView: some View {
Expand Down
Loading

0 comments on commit 4441fba

Please sign in to comment.