Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Preview bookmarks using Quick Look on macOS #478

Merged
merged 1 commit into from
May 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SelectableCollectionView
83 changes: 50 additions & 33 deletions core/Sources/BookmarksCore/Store/BookmarksView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,20 @@ public class BookmarksView: ObservableObject, Runnable {
@Published public var title: String
@Published public var subtitle: String = ""
@Published public var bookmarks: [Bookmark] = []
@Published public var urls: [URL] = []
@Published public var state: State = .loading
@Published public var filter: String = ""
@Published public var tokens: [String] = []
@Published public var suggestedTokens: [String] = []
@Published public var layoutMode: LayoutMode
@Published public var selection: Set<Bookmark.ID> = []
@Published public var previewURL: URL? = nil

@Published public var sheet: SheetType? = nil
@Published public var lastError: Error? = nil

@Published private var query: AnyQuery
@Published private var bookmarksLookup: [Bookmark.ID: Bookmark] = [:]

private let manager: BookmarksManager?
private let section: BookmarksSection
Expand Down Expand Up @@ -87,19 +90,24 @@ public class BookmarksView: ObservableObject, Runnable {
.receive(on: DispatchQueue.global())
.asyncMap { (_, query) in
do {
return (query, try await manager.database.bookmarks(query: query))
let bookmarks = try await manager.database.bookmarks(query: query)
let bookmarksLookup = bookmarks.reduce(into: [Bookmark.ID: Bookmark]()) { $0[$1.id] = $1 }
let urls = bookmarks.map { $0.url }
return (query, bookmarks, bookmarksLookup, urls)
} catch {
return (query, [])
return (query, [], [:], [])
}
}
.receive(on: DispatchQueue.main)
.sink { query, bookmarks in
.sink { query, bookmarks, bookmarksLookup, urls in
// Guard against updating the bookmarks with results from an old query.
guard self.query == query else {
print("Discarding query")
return
}
self.bookmarks = bookmarks
self.bookmarksLookup = bookmarksLookup
self.urls = urls
self.state = .ready
}
.store(in: &cancellables)
Expand Down Expand Up @@ -173,6 +181,19 @@ public class BookmarksView: ObservableObject, Runnable {
}
.store(in: &cancellables)

// Update the selection if the preview URL changes.
$previewURL
.compactMap { $0 }
.debounce(for: 0.2, scheduler: DispatchQueue.global(qos: .userInteractive))
.combineLatest($bookmarks)
.compactMap { url, bookmarks in
bookmarks.first { $0.url == url }?.id
}
.map { Set([$0]) }
.receive(on: DispatchQueue.main)
.assign(to: \.selection, on: self)
.store(in: &cancellables)

}

@MainActor public func stop() {
Expand All @@ -185,14 +206,9 @@ public class BookmarksView: ObservableObject, Runnable {
sheet = .addTags
}

@MainActor public func bookmarks(for ids: Set<Bookmark.ID>? = nil) async -> [Bookmark] {
@MainActor public func bookmarks(for ids: Set<Bookmark.ID>? = nil) -> [Bookmark] {
let ids = ids ?? self.selection
let bookmarks = bookmarks
return await withCheckedContinuation { continuation in
DispatchQueue.global().async {
continuation.resume(returning: bookmarks.filter { ids.contains($0.id) })
}
}
return ids.compactMap { bookmarksLookup[$0] }
}

@MainActor public func open(ids: Set<Bookmark.ID>? = nil, location: Bookmark.Location = .web) {
Expand All @@ -211,66 +227,62 @@ public class BookmarksView: ObservableObject, Runnable {
}
}

@MainActor public func update(ids: Set<Bookmark.ID>? = nil, toRead: Bool) async {
@MainActor public func update(ids: Set<Bookmark.ID>? = nil, toRead: Bool) {
guard let manager else {
return
}
let bookmarks = await bookmarks(for: ids)
let bookmarks = bookmarks(for: ids)
.map { $0.setting(toRead: toRead) }
manager.updateBookmarks(bookmarks, completion: errorHandler())
}

@MainActor public func update(ids: Set<Bookmark.ID>? = nil, shared: Bool) async {
@MainActor public func update(ids: Set<Bookmark.ID>? = nil, shared: Bool) {
guard let manager else {
return
}
let bookmarks = await bookmarks(for: ids)
let bookmarks = bookmarks(for: ids)
.map { $0.setting(shared: shared) }
manager.updateBookmarks(bookmarks, completion: errorHandler())
}

@MainActor public func delete(ids: Set<Bookmark.ID>? = nil) async {
@MainActor public func delete(ids: Set<Bookmark.ID>? = nil) {
guard let manager else {
return
}
let bookmarks = await bookmarks(for: ids)
let bookmarks = bookmarks(for: ids)
manager.deleteBookmarks(bookmarks, completion: errorHandler())
}

// TODO: Rethink the threading here.
@MainActor public func copy(ids: Set<Bookmark.ID>? = nil) async {
@MainActor public func copy(ids: Set<Bookmark.ID>? = nil) {
#if os(macOS)
let bookmarks = await bookmarks(for: ids)
DispatchQueue.main.async {
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects(bookmarks.map { $0.url.absoluteString as NSString })
NSPasteboard.general.writeObjects(bookmarks.map { $0.url as NSURL })
}
let bookmarks = bookmarks(for: ids)
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects(bookmarks.map { $0.url.absoluteString as NSString })
NSPasteboard.general.writeObjects(bookmarks.map { $0.url as NSURL })
#else
assertionFailure("Unsupported")
fatalError("Unsupported")
#endif
}

// TODO: Rethink the threading here.
@MainActor public func copyTags(ids: Set<Bookmark.ID>? = nil) async {
@MainActor public func copyTags(ids: Set<Bookmark.ID>? = nil) {
#if os(macOS)
let bookmarks = await bookmarks(for: ids)
let bookmarks = bookmarks(for: ids)
let tags = bookmarks.tags.sorted().joined(separator: " ")
DispatchQueue.main.async {
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects([tags as NSString])
}
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects([tags as NSString])
#else
assertionFailure("Unsupported")
fatalError("Unsupported")
#endif
}

// TODO: Consider whether we should pull this down into the manager.
@MainActor public func addTags(ids: Set<Bookmark.ID>? = nil, tags: Set<String>, markAsRead: Bool) async {
@MainActor public func addTags(ids: Set<Bookmark.ID>? = nil, tags: Set<String>, markAsRead: Bool) {
guard let manager else {
return
}
let bookmarks = await bookmarks(for: ids)
let bookmarks = bookmarks(for: ids)
.map { item in
item
.adding(tags: tags)
Expand All @@ -279,6 +291,11 @@ public class BookmarksView: ObservableObject, Runnable {
manager.updateBookmarks(bookmarks, completion: errorHandler({ _ in }))
}

@MainActor public func showPreview() {
let bookmarks = bookmarks()
previewURL = bookmarks.first?.url
}

// TODO: Set this asynchronously using combine.
@MainActor public var selectionContainsUnreadBookmarks: Bool {
let bookmarks = bookmarks.filter { selection.contains($0.id) }
Expand Down
2 changes: 1 addition & 1 deletion macos/Bookmarks/Commands/BookmarkDestructiveCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct BookmarkDesctructiveCommands: View {
var body: some View {
Button("Delete") {
Task {
await bookmarksView.delete()
bookmarksView.delete()
}
}
.keyboardShortcut(.delete, modifiers: [.command])
Expand Down
8 changes: 2 additions & 6 deletions macos/Bookmarks/Commands/BookmarkEditCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,13 @@ struct BookmarkEditCommands: View {
var body: some View {

Button(bookmarksView.selectionContainsUnreadBookmarks ? "Mark as Read" : "Mark as Unread") {
Task {
await bookmarksView.update(toRead: !bookmarksView.selectionContainsUnreadBookmarks)
}
bookmarksView.update(toRead: !bookmarksView.selectionContainsUnreadBookmarks)
}
.keyboardShortcut("U", modifiers: [.command, .shift])
.disabled(bookmarksView.selection.isEmpty)

Button(bookmarksView.selectionContainsPublicBookmark ? "Make Private" : "Make Public") {
Task {
await bookmarksView.update(shared: !bookmarksView.selectionContainsPublicBookmark)
}
bookmarksView.update(shared: !bookmarksView.selectionContainsPublicBookmark)
}
.disabled(bookmarksView.selection.isEmpty)

Expand Down
8 changes: 8 additions & 0 deletions macos/Bookmarks/Commands/BookmarkOpenCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ struct BookmarkOpenCommands: View {

var body: some View {

Button("Preview") {
bookmarksView.showPreview()
}
.keyboardShortcut(.space, modifiers: [])
.disabled(bookmarksView.selection.isEmpty)

Divider()

Button("Open") {
bookmarksView.open()
}
Expand Down
8 changes: 2 additions & 6 deletions macos/Bookmarks/Commands/BookmarkShareCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,13 @@ struct BookmarkShareCommands: View {

var body: some View {
Button("Copy") {
Task {
await bookmarksView.copy()
}
bookmarksView.copy()
}
.keyboardShortcut("c", modifiers: [.command])
.disabled(bookmarksView.selection.isEmpty)

Button("Copy Tags") {
Task {
await bookmarksView.copyTags()
}
bookmarksView.copyTags()
}
.keyboardShortcut("c", modifiers: [.command, .shift])
.disabled(bookmarksView.selection.isEmpty)
Expand Down
15 changes: 12 additions & 3 deletions macos/Bookmarks/Toolbars/SelectionToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ struct SelectionToolbar: CustomizableToolbarContent {

var body: some CustomizableToolbarContent {

ToolbarItem(id: "preview") {
Button {
bookmarksView.showPreview()
} label: {
Label("Preview", systemImage: "eye")
}
.help("Preview with Quick Look")
.keyboardShortcut(.space, modifiers: [])
.disabled(bookmarksView.selection.count != 1)
}

ToolbarItem(id: "open") {
Button {
bookmarksView.open(ids: bookmarksView.selection)
Expand All @@ -52,9 +63,7 @@ struct SelectionToolbar: CustomizableToolbarContent {

ToolbarItem(id: "delete") {
Button {
Task {
await bookmarksView.delete()
}
bookmarksView.delete()
} label: {
Label("Delete", systemImage: "trash")
}
Expand Down
4 changes: 1 addition & 3 deletions macos/Bookmarks/Views/AddTagsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ struct AddTagsView: View {
.keyboardShortcut(.cancelAction)
Button {
let tags = tokens.compactMap { $0.associatedValue }
Task {
await bookmarksView.addTags(tags: Set(tags), markAsRead: markAsRead)
}
bookmarksView.addTags(tags: Set(tags), markAsRead: markAsRead)
dismiss()
} label: {
Text("OK")
Expand Down
26 changes: 21 additions & 5 deletions macos/Bookmarks/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Carbon
import Combine
import QuickLook
import SwiftUI

import BookmarksCore
Expand Down Expand Up @@ -52,25 +54,25 @@ struct ContentView: View {
}
Separator()
MenuItem("Delete") {
await bookmarksView.delete(ids: selection)
bookmarksView.delete(ids: selection)
}
Separator()
MenuItem(bookmarksView.selectionContainsUnreadBookmarks ? "Mark as Read" : "Mark as Unread") {
await bookmarksView.update(toRead: !bookmarksView.selectionContainsUnreadBookmarks)
bookmarksView.update(toRead: !bookmarksView.selectionContainsUnreadBookmarks)
}
MenuItem(bookmarksView.selectionContainsPublicBookmark ? "Make Private" : "Make Public") {
await bookmarksView.update(shared: !bookmarksView.selectionContainsPublicBookmark)
bookmarksView.update(shared: !bookmarksView.selectionContainsPublicBookmark)
}
Separator()
MenuItem("Edit on Pinboard") {
bookmarksView.open(ids: selection, location: .pinboard)
}
Separator()
MenuItem("Copy") {
await bookmarksView.copy(ids: selection)
bookmarksView.copy(ids: selection)
}
MenuItem("Copy Tags") {
await bookmarksView.copyTags(ids: selection)
bookmarksView.copyTags(ids: selection)
}
}

Expand All @@ -83,6 +85,7 @@ struct ContentView: View {

switch bookmarksView.layoutMode {
case .grid:

SelectableCollectionView(bookmarksView.bookmarks,
selection: $bookmarksView.selection,
layout: layout) { bookmark in
Expand All @@ -97,7 +100,19 @@ struct ContentView: View {
contextMenu(selection)
} primaryAction: { selection in
primaryAction(selection)
} keyDown: { event in
if event.keyCode == kVK_Space {
bookmarksView.showPreview()
return true
}
return false
} keyUp: { event in
if event.keyCode == kVK_Space {
return true
}
return false
}

case .table:

Table(bookmarksView.bookmarks, selection: $bookmarksView.selection) {
Expand All @@ -116,6 +131,7 @@ struct ContentView: View {

}
.overlay(bookmarksView.state == .loading ? LoadingView() : nil)
.quickLookPreview($bookmarksView.previewURL, in: bookmarksView.urls)
.runs(bookmarksView)
.searchable(text: $bookmarksView.filter,
tokens: $bookmarksView.tokens,
Expand Down