Skip to content

Commit

Permalink
Feature: Search and sort bookmarks in bookmarks panel (#3022)
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira authored Jul 30, 2024
1 parent 13a0783 commit b1f3b21
Show file tree
Hide file tree
Showing 40 changed files with 2,255 additions and 260 deletions.
46 changes: 34 additions & 12 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Bookmarks-Search-Empty.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "sort-asc.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "sort-desc.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "search_bookmarks.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions DuckDuckGo/Bookmarks/Model/Bookmark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,16 @@ final class Bookmark: BaseBookmarkEntity {
}

}

extension Array where Element == BaseBookmarkEntity {
func sorted(by sortMode: BookmarksSortMode) -> [BaseBookmarkEntity] {
switch sortMode {
case .manual:
return self
case .nameAscending:
return self.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
case .nameDescending:
return self.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedDescending }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,37 @@ final class BookmarkListTreeControllerDataSource: BookmarkTreeControllerDataSour
self.bookmarkManager = bookmarkManager
}

func treeController(treeController: BookmarkTreeController, childNodesFor node: BookmarkNode) -> [BookmarkNode] {
return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
func treeController(childNodesFor node: BookmarkNode, sortMode: BookmarksSortMode) -> [BookmarkNode] {
return node.isRoot ? childNodesForRootNode(node, for: sortMode) : childNodes(node, for: sortMode)
}

// MARK: - Private

private func childNodesForRootNode(_ node: BookmarkNode) -> [BookmarkNode] {
let topLevelNodes = bookmarkManager.list?.topLevelEntities.compactMap { (item) -> BookmarkNode? in
if let folder = item as? BookmarkFolder {
let itemNode = node.createChildNode(item)
itemNode.canHaveChildNodes = !folder.children.isEmpty

return itemNode
} else if item is Bookmark {
let itemNode = node.findOrCreateChildNode(with: item)
itemNode.canHaveChildNodes = false
return itemNode
} else {
assertionFailure("\(#file): Tried to display non-bookmark type in bookmark list")
return nil
}
} ?? []
private func childNodesForRootNode(_ node: BookmarkNode, for sortMode: BookmarksSortMode) -> [BookmarkNode] {
let topLevelNodes = bookmarkManager.list?.topLevelEntities
.sorted(by: sortMode)
.compactMap { (item) -> BookmarkNode? in
if let folder = item as? BookmarkFolder {
let itemNode = node.createChildNode(item)
itemNode.canHaveChildNodes = !folder.children.isEmpty

return itemNode
} else if item is Bookmark {
let itemNode = node.findOrCreateChildNode(with: item)
itemNode.canHaveChildNodes = false
return itemNode
} else {
assertionFailure("\(#file): Tried to display non-bookmark type in bookmark list")
return nil
}
} ?? []

return topLevelNodes
}

private func childNodes(_ node: BookmarkNode) -> [BookmarkNode] {
private func childNodes(_ node: BookmarkNode, for sortMode: BookmarksSortMode) -> [BookmarkNode] {
if let folder = node.representedObject as? BookmarkFolder {
return childNodes(for: folder, parentNode: node)
return childNodes(for: folder, parentNode: node, sortMode: sortMode)
}

return []
Expand All @@ -72,20 +74,22 @@ final class BookmarkListTreeControllerDataSource: BookmarkTreeControllerDataSour
return node
}

private func childNodes(for folder: BookmarkFolder, parentNode: BookmarkNode) -> [BookmarkNode] {
private func childNodes(for folder: BookmarkFolder, parentNode: BookmarkNode, sortMode: BookmarksSortMode) -> [BookmarkNode] {
var updatedChildNodes = [BookmarkNode]()

folder.children.forEach { representedObject in
if let existingNode = parentNode.childNodeRepresenting(object: representedObject) {
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
return
folder.children
.sorted(by: sortMode)
.forEach { representedObject in
if let existingNode = parentNode.childNodeRepresenting(object: representedObject) {
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
return
}
}
}

let newNode = self.createNode(representedObject, parent: parentNode)
updatedChildNodes += [newNode]
}
let newNode = self.createNode(representedObject, parent: parentNode)
updatedChildNodes += [newNode]
}

return updatedChildNodes
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// BookmarkListTreeControllerSearchDataSource.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

final class BookmarkListTreeControllerSearchDataSource: BookmarkTreeControllerSearchDataSource {
private let bookmarkManager: BookmarkManager

init(bookmarkManager: BookmarkManager) {
self.bookmarkManager = bookmarkManager
}

func nodes(for searchQuery: String, sortMode: BookmarksSortMode) -> [BookmarkNode] {
let searchResults = bookmarkManager.search(by: searchQuery)

return rebuildChildNodes(for: searchResults.sorted(by: sortMode))
}

private func rebuildChildNodes(for results: [BaseBookmarkEntity]) -> [BookmarkNode] {
let rootNode = BookmarkNode.genericRootNode()
let nodes = results.compactMap { (item) -> BookmarkNode in
let itemNode = rootNode.createChildNode(item)
itemNode.canHaveChildNodes = false
return itemNode
}

return nodes
}
}
2 changes: 1 addition & 1 deletion DuckDuckGo/Bookmarks/Model/BookmarkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ final class LocalBookmarkManager: BookmarkManager {
while !queue.isEmpty {
let current = queue.removeFirst()

if current.title.lowercased().contains(query) {
if current.title.lowercased().contains(query.lowercased()) {
result.append(current)
}

Expand Down
11 changes: 11 additions & 0 deletions DuckDuckGo/Bookmarks/Model/BookmarkNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ final class BookmarkNode: Hashable {
return false
}

/// Checks if two nodes represent the same base bookmark entity based only on their ID
func representedObjectHasSameId(_ otherRepresentedObject: AnyObject) -> Bool {
if let entity = otherRepresentedObject as? BaseBookmarkEntity,
let nodeEntity = self.representedObject as? BaseBookmarkEntity,
entity.id == nodeEntity.id {
return true
}

return false
}

func findOrCreateChildNode(with representedObject: AnyObject) -> BookmarkNode {
if let node = childNodeRepresenting(object: representedObject) {
return node
Expand Down
42 changes: 35 additions & 7 deletions DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
@Published var selectedFolders: [BookmarkFolder] = []

let treeController: BookmarkTreeController
private(set) var expandedNodesIDs = Set<String>()

private let contentMode: ContentMode
private(set) var expandedNodesIDs = Set<String>()
private(set) var isSearching = false

/// When a drag and drop to a folder happens while in search, we need to stor the destination folder
/// so we can expand the tree to the destination folder once the drop finishes.
private(set) var dragDestinationFolderInSearchMode: BookmarkFolder?

private let bookmarkManager: BookmarkManager
private let showMenuButtonOnHover: Bool
private let onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)?
Expand All @@ -45,6 +51,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
contentMode: ContentMode,
bookmarkManager: BookmarkManager,
treeController: BookmarkTreeController,
sortMode: BookmarksSortMode,
showMenuButtonOnHover: Bool = true,
onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? = nil,
presentFaviconsFetcherOnboarding: (() -> Void)? = nil
Expand All @@ -58,13 +65,25 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS

super.init()

reloadData()
reloadData(with: sortMode)
}

func reloadData(with sortMode: BookmarksSortMode) {
isSearching = false
dragDestinationFolderInSearchMode = nil
setFolderCount()
treeController.rebuild(for: sortMode)
}

func reloadData(for searchQuery: String, and sortMode: BookmarksSortMode) {
isSearching = true
setFolderCount()
treeController.rebuild(for: searchQuery, sortMode: sortMode)
}

func reloadData() {
private func setFolderCount() {
favoritesPseudoFolder.count = bookmarkManager.list?.favoriteBookmarks.count ?? 0
bookmarksPseudoFolder.count = bookmarkManager.list?.totalBookmarks ?? 0
treeController.rebuild()
}

// MARK: - Private
Expand Down Expand Up @@ -133,7 +152,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
cell.delegate = self

if let bookmark = node.representedObject as? Bookmark {
cell.update(from: bookmark)
cell.update(from: bookmark, isSearch: isSearching)

if bookmark.favicon(.small) == nil {
presentFaviconsFetcherOnboarding?()
Expand All @@ -142,7 +161,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
}

if let folder = node.representedObject as? BookmarkFolder {
cell.update(from: folder)
cell.update(from: folder, isSearch: isSearching)
return cell
}

Expand Down Expand Up @@ -181,6 +200,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
return .none
}

if isSearching {
if let destinationFolder = destinationNode.representedObject as? BookmarkFolder {
self.dragDestinationFolderInSearchMode = destinationFolder
return .move
}

return .none
}

let bookmarks = PasteboardBookmark.pasteboardBookmarks(with: info.draggingPasteboard)
let folders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)

Expand Down Expand Up @@ -243,7 +271,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
let containsDescendantOfDestination = draggedFolders.contains { draggedFolder in
let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name, parentFolderUUID: draggedFolder.parentFolderUUID, children: draggedFolder.children)

guard let draggedNode = treeController.node(representing: folder) else {
guard let draggedNode = treeController.findNodeWithId(representing: folder) else {
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation

final class BookmarkSidebarTreeController: BookmarkTreeControllerDataSource {

func treeController(treeController: BookmarkTreeController, childNodesFor node: BookmarkNode) -> [BookmarkNode] {
func treeController(childNodesFor node: BookmarkNode, sortMode: BookmarksSortMode) -> [BookmarkNode] {
return node.isRoot ? childNodesForRootNode(node) : childNodes(for: node)
}

Expand Down
Loading

0 comments on commit b1f3b21

Please sign in to comment.