// // BookmarksManagerView.swift // Zyquo Atlas // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Favorites manager (⌥⌘B): searchable list of bookmarks with edit (title/notes/ // tags/on-bar), delete, and HTML import/export. Folders and drag-organization // build on BookmarksService. // import SwiftUI import AppKit struct BookmarksManagerView: View { let onOpen: (String) -> Void @EnvironmentObject private var env: AppEnvironment @EnvironmentObject private var themeEngine: ThemeEngine @Environment(\.dismiss) private var dismiss private var theme: AtlasTheme { themeEngine.theme } @State private var query = "" @State private var editing: Bookmark? var body: some View { VStack(spacing: 0) { header Divider().overlay(theme.border) list } .frame(width: 640, height: 600) .background(theme.backgroundColor) .sheet(item: $editing) { bm in editor(bm) } } private var header: some View { HStack { Label("Favorites", systemImage: "star").font(ZyquoFont.title).foregroundStyle(theme.textPrimary) Spacer() HStack(spacing: ZyquoSpacing.xs) { Image(systemName: "magnifyingglass").font(.system(size: 11)).foregroundStyle(theme.textTertiary) TextField("Search favorites", text: $query).textFieldStyle(.plain).font(ZyquoFont.control) .foregroundStyle(theme.textPrimary).frame(width: 180) } .padding(.horizontal, ZyquoSpacing.sm).frame(height: 28) .background(RoundedRectangle(cornerRadius: ZyquoRadius.small).fill(theme.surfaceSecondary)) Button("Import") { importHTML() }.foregroundStyle(theme.accent) Button("Export") { exportHTML() }.foregroundStyle(theme.accent) Button("Done") { dismiss() }.foregroundStyle(theme.accent) } .padding(ZyquoSpacing.md).background(theme.surface) } private var list: some View { ScrollView { LazyVStack(spacing: 0) { ForEach(env.bookmarks.search(query)) { bm in row(bm) Divider().overlay(theme.border) } if env.bookmarks.bookmarks.isEmpty { Text("No favorites yet — bookmark a page with ⌘D") .font(ZyquoFont.caption).foregroundStyle(theme.textTertiary) .frame(maxWidth: .infinity).padding(ZyquoSpacing.xxl) } } } } private func row(_ bm: Bookmark) -> some View { HStack(spacing: ZyquoSpacing.sm) { Image(systemName: bm.onBar ? "star.fill" : "globe") .font(.system(size: 11)).foregroundStyle(bm.onBar ? theme.accent : theme.textTertiary) VStack(alignment: .leading, spacing: 1) { Text(bm.title).font(ZyquoFont.body(size: 12.5)).lineLimit(1).foregroundStyle(theme.textPrimary) Text(bm.url).font(ZyquoFont.caption).lineLimit(1).foregroundStyle(theme.textTertiary) } Spacer() if !bm.tags.isEmpty { Text(bm.tags.joined(separator: ", ")).font(ZyquoFont.caption).foregroundStyle(theme.accentIndigo) } Button { editing = bm } label: { Image(systemName: "pencil") } .buttonStyle(.plain).foregroundStyle(theme.textSecondary) Button { env.bookmarks.delete(bm.id) } label: { Image(systemName: "trash") } .buttonStyle(.plain).foregroundStyle(theme.textTertiary) } .padding(.horizontal, ZyquoSpacing.md).padding(.vertical, ZyquoSpacing.xs) .contentShape(Rectangle()) .onTapGesture { onOpen(bm.url); dismiss() } } private func editor(_ bm: Bookmark) -> some View { var draft = bm return VStack(alignment: .leading, spacing: ZyquoSpacing.sm) { Text("Edit Favorite").font(ZyquoFont.bodyEmphasis()).foregroundStyle(theme.textPrimary) EditorField(label: "Title", text: draft.title) { draft.title = $0 } EditorField(label: "URL", text: draft.url) { draft.url = $0 } EditorField(label: "Tags (comma-separated)", text: draft.tags.joined(separator: ", ")) { draft.tags = $0.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } } EditorField(label: "Notes", text: draft.notes) { draft.notes = $0 } Toggle("Show on bookmarks bar", isOn: Binding(get: { draft.onBar }, set: { draft.onBar = $0 })) .toggleStyle(.switch).font(ZyquoFont.body()).foregroundStyle(theme.textPrimary) HStack { Spacer() Button("Cancel") { editing = nil } Button("Save") { env.bookmarks.update(draft); editing = nil }.keyboardShortcut(.defaultAction) } } .padding(ZyquoSpacing.lg).frame(width: 420).background(theme.surface) } private func importHTML() { let p = NSOpenPanel(); p.allowedContentTypes = [.html]; p.allowsMultipleSelection = false if p.runModal() == .OK, let url = p.url { _ = try? env.bookmarks.importHTML(from: url) } } private func exportHTML() { let p = NSSavePanel(); p.allowedContentTypes = [.html]; p.nameFieldStringValue = "zyquo-atlas-bookmarks.html" if p.runModal() == .OK, let url = p.url { try? env.bookmarks.exportHTML(to: url) } } } /// A labeled single-line editor row that reports edits via a callback. private struct EditorField: View { let label: String @State var text: String let onChange: (String) -> Void @EnvironmentObject private var themeEngine: ThemeEngine var body: some View { VStack(alignment: .leading, spacing: 2) { Text(label).font(ZyquoFont.caption).foregroundStyle(themeEngine.theme.textTertiary) TextField("", text: $text) .textFieldStyle(.roundedBorder) .onChange(of: text) { onChange($0) } } } }