Skip to content
Open
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
17 changes: 17 additions & 0 deletions AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var clipboardWatcher: ClipboardWatcher?
private var historyWindowController: HistoryWindowController?
private var hotkeyManager: HotkeyManager?
private var webLinkWindowController: WebLinkWindowController?

let clipboardStore = ClipboardStore()
let webLinkStore = WebLinkStore()

func applicationDidFinishLaunching(_ notification: Notification) {
// Hide dock icon - we're menu bar only
Expand Down Expand Up @@ -37,9 +39,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
watcher: clipboardWatcher!,
onShowHistory: { [weak self] in
self?.showHistoryWindow()
},
onShowWebLinks: { [weak self] in
self?.showWebLinksWindow()
}
)

// Initialize web link store and window controller
webLinkWindowController = WebLinkWindowController(store: webLinkStore)

// Initialize history window controller
historyWindowController = HistoryWindowController(store: clipboardStore)

Expand Down Expand Up @@ -81,4 +89,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private func showHistoryWindow() {
historyWindowController?.showWindow(nil)
}

private func showWebLinksWindow() {
if let window = webLinkWindowController?.window, window.isVisible {
webLinkWindowController?.close()
} else {
webLinkWindowController?.showWindow(nil)
}
}

}
12 changes: 12 additions & 0 deletions Buffer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
AAA00017 /* UpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB00019 /* UpdateService.swift */; };
D87736122F8ECC6B00392A0B /* ClickDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87736112F8ECC6B00392A0B /* ClickDetector.swift */; };
AAA00020 /* PasteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB00020 /* PasteButton.swift */; };
AAA00021 /* WebLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB00021 /* WebLink.swift */; };
AAA00022 /* WebLinkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB00022 /* WebLinkStore.swift */; };
AAA00023 /* WebLinksWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB00023 /* WebLinksWindow.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -65,6 +68,9 @@
D57AA9E1A5E3E5C6ED5E9CDC /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; };
D87736112F8ECC6B00392A0B /* ClickDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickDetector.swift; sourceTree = "<group>"; };
BBB00020 /* PasteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteButton.swift; sourceTree = "<group>"; };
BBB00021 /* WebLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLink.swift; sourceTree = "<group>"; };
BBB00022 /* WebLinkStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLinkStore.swift; sourceTree = "<group>"; };
BBB00023 /* WebLinksWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLinksWindow.swift; sourceTree = "<group>"; };
EB5F7ACF8D07ACEDB0B7217E /* BufferTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BufferTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -141,6 +147,7 @@
isa = PBXGroup;
children = (
BBB00003 /* ClipboardItem.swift */,
BBB00021 /* WebLink.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -155,6 +162,7 @@
BBB00015 /* SettingsManager.swift */,
BBB00017 /* OCRService.swift */,
BBB00019 /* UpdateService.swift */,
BBB00022 /* WebLinkStore.swift */,
);
path = Services;
sourceTree = "<group>";
Expand All @@ -171,6 +179,7 @@
BBB00012 /* StatusBarController.swift */,
BBB00016 /* SettingsView.swift */,
BBB00020 /* PasteButton.swift */,
BBB00023 /* WebLinksWindow.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -305,6 +314,9 @@
AAA00016 /* TagChip.swift in Sources */,
AAA00017 /* UpdateService.swift in Sources */,
AAA00020 /* PasteButton.swift in Sources */,
AAA00021 /* WebLink.swift in Sources */,
AAA00022 /* WebLinkStore.swift in Sources */,
AAA00023 /* WebLinksWindow.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
42 changes: 42 additions & 0 deletions Models/WebLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

struct WebLink: Identifiable, Codable, Equatable {
let id: UUID
var name: String
var url: String
var sortOrder: Int
var createdAt: Date
var lastOpenedAt: Date?
var visitCount: Int

init(id: UUID = UUID(), name: String, url: String, sortOrder: Int = 0, createdAt: Date = Date(), lastOpenedAt: Date? = nil, visitCount: Int = 0) {
self.id = id
self.name = name
self.url = url
self.sortOrder = sortOrder
self.createdAt = createdAt
self.lastOpenedAt = lastOpenedAt
self.visitCount = visitCount
}

var displayURL: String {
if url.hasPrefix("http://") || url.hasPrefix("https://") {
return String(url.dropFirst(url.hasPrefix("https://") ? 8 : 7))
}
return url
}

var isValidURL: Bool {
guard !url.isEmpty else { return false }
let lower = url.lowercased()
if lower.hasPrefix("http://") || lower.hasPrefix("https://") { return true }
return url.contains(".")
}

var resolvedURL: String {
if url.lowercased().hasPrefix("http://") || url.lowercased().hasPrefix("https://") {
return url
}
return "https://" + url
}
}
84 changes: 84 additions & 0 deletions Services/WebLinkStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation
import Combine

class WebLinkStore: ObservableObject {
@Published var links: [WebLink] = []

private let fileManager = FileManager.default
private let saveQueue = DispatchQueue(label: "com.buffer.weblinks.save", qos: .utility)

private var storageDirectory: URL {
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return appSupport.appendingPathComponent("Buffer", isDirectory: true)
}

private var fileURL: URL {
storageDirectory.appendingPathComponent("weblinks.json")
}

init() {
ensureDirectoriesExist()
load()
}

func add(name: String, url: String) {
let maxOrder = links.map(\.sortOrder).max() ?? 0
let link = WebLink(name: name, url: url, sortOrder: maxOrder + 1)
DispatchQueue.main.async { [weak self] in
self?.links.append(link)
self?.persist()
}
}

func update(_ link: WebLink) {
DispatchQueue.main.async { [weak self] in
guard let self = self, let index = self.links.firstIndex(where: { $0.id == link.id }) else { return }
self.links[index] = link
self.persist()
}
}

func delete(_ link: WebLink) {
DispatchQueue.main.async { [weak self] in
self?.links.removeAll { $0.id == link.id }
self?.persist()
}
}

func recordOpen(_ link: WebLink) {
DispatchQueue.main.async { [weak self] in
guard let self = self, let index = self.links.firstIndex(where: { $0.id == link.id }) else { return }
self.links[index].lastOpenedAt = Date()
self.links[index].visitCount += 1
self.persist()
}
}

private func ensureDirectoriesExist() {
try? fileManager.createDirectory(at: storageDirectory, withIntermediateDirectories: true)
}

private func load() {
guard fileManager.fileExists(atPath: fileURL.path) else { return }
do {
let data = try Data(contentsOf: fileURL)
let loaded = try JSONDecoder().decode([WebLink].self, from: data)
self.links = loaded
} catch {
print("[Buffer] Failed to load web links: \(error)")
}
}

private func persist() {
let items = links
saveQueue.async { [weak self] in
guard let self = self else { return }
do {
let data = try JSONEncoder().encode(items)
try data.write(to: self.fileURL)
} catch {
print("[Buffer] Failed to save web links: \(error)")
}
}
}
}
34 changes: 34 additions & 0 deletions Views/HistoryWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ struct HistoryContentView: View {

@FocusState private var isSearchFocused: Bool
@State private var searchText = ""
@State private var statusBarHidden = SettingsManager.shared.hideStatusBar
@State private var debouncedSearchText = ""
@State private var searchDebounceTask: Task<Void, Never>? = nil
@State private var selectedIndex = 0
Expand Down Expand Up @@ -782,6 +783,14 @@ struct HistoryContentView: View {
guard isSearchFocused, searchText.isEmpty, activeTagFilter != nil else { return false }
activeTagFilter = nil
return true
},
onSelectAll: {
guard !isEditing else { return }
guard !filteredItems.isEmpty else { return }
selectedIDs = Set(filteredItems.map(\.id))
selectionAnchor = filteredItems.first?.id
selectedID = filteredItems.first?.id
selectedIndex = 0
}
))
}
Expand Down Expand Up @@ -888,6 +897,7 @@ struct HistoryContentView: View {
Text("\(filteredItems.count) items")
.font(.system(size: 11, weight: .regular))
.foregroundColor(.secondary.opacity(0.6))

}
.padding(.horizontal, 14)
.padding(.vertical, 10)
Expand Down Expand Up @@ -1561,6 +1571,20 @@ struct HistoryContentView: View {
}

Spacer()

// Status bar visibility toggle
Button(action: {
statusBarHidden.toggle()
SettingsManager.shared.hideStatusBar = statusBarHidden
SettingsManager.shared.save()
NotificationCenter.default.post(name: .bufferStatusBarVisibilityChanged, object: nil)
}) {
Text(statusBarHidden ? "πŸ™ˆ" : "πŸ’")
.font(.system(size: 14))
}
.buttonStyle(.plain)
.help(statusBarHidden ? "Show menu bar icon" : "Hide menu bar icon")
.padding(.trailing, 4)

PasteButton(action: { if let item = selectedItem { onPaste(item) } })
}
Expand Down Expand Up @@ -1705,6 +1729,7 @@ struct GlobalKeyMonitor: NSViewRepresentable {
let onEdit: () -> Void
let onTabComplete: () -> Void
let onBackspace: () -> Bool
let onSelectAll: () -> Void

func makeNSView(context: Context) -> NSView {
let view = NSView()
Expand Down Expand Up @@ -1800,6 +1825,13 @@ struct GlobalKeyMonitor: NSViewRepresentable {
if isEditing { return event }
context.coordinator.onTabComplete?()
return nil
case 0: // A
if event.modifierFlags.contains(.command) {
if isEditing { return event }
context.coordinator.onSelectAll?()
return nil
}
return event
default:
return event
}
Expand Down Expand Up @@ -1827,6 +1859,7 @@ struct GlobalKeyMonitor: NSViewRepresentable {
context.coordinator.onEdit = onEdit
context.coordinator.onTabComplete = onTabComplete
context.coordinator.onBackspace = onBackspace
context.coordinator.onSelectAll = onSelectAll
}

func makeCoordinator() -> Coordinator {
Expand All @@ -1851,6 +1884,7 @@ struct GlobalKeyMonitor: NSViewRepresentable {
var onEdit: (() -> Void)?
var onTabComplete: (() -> Void)?
var onBackspace: (() -> Bool)?
var onSelectAll: (() -> Void)?

deinit {
if let monitor = monitor {
Expand Down
15 changes: 14 additions & 1 deletion Views/StatusBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ class StatusBarController {
private let store: ClipboardStore
private let watcher: ClipboardWatcher
private let onToggleHistory: () -> Void
private let onShowWebLinks: () -> Void
private var settingsWindowController: NSWindowController?
private var activeAlert: NSAlert?

init(store: ClipboardStore, watcher: ClipboardWatcher, onShowHistory: @escaping () -> Void) {
init(store: ClipboardStore, watcher: ClipboardWatcher, onShowHistory: @escaping () -> Void, onShowWebLinks: @escaping () -> Void) {
self.store = store
self.watcher = watcher
self.onToggleHistory = onShowHistory
self.onShowWebLinks = onShowWebLinks

if !SettingsManager.shared.hideStatusBar {
createStatusItem()
Expand Down Expand Up @@ -94,6 +96,13 @@ class StatusBarController {
pauseItem.target = self
menu.addItem(pauseItem)

menu.addItem(NSMenuItem.separator())

// Quick Web Links
let webLinksItem = NSMenuItem(title: "Quick Web Links…", action: #selector(showWebLinks), keyEquivalent: "u")
webLinksItem.target = self
menu.addItem(webLinksItem)

menu.addItem(NSMenuItem.separator())

// Clear History
Expand Down Expand Up @@ -134,6 +143,10 @@ class StatusBarController {
NSApp.activate(ignoringOtherApps: true)
}

@objc private func showWebLinks() {
onShowWebLinks()
}

@objc private func togglePause() {
if watcher.isPaused {
watcher.resume()
Expand Down
Loading