SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
6.0 KB · 140 lines swift
Raw Blame History
1//2//  BookmarksManagerView.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Favorites manager (⌥⌘B): searchable list of bookmarks with edit (title/notes/9//  tags/on-bar), delete, and HTML import/export. Folders and drag-organization10//  build on BookmarksService.11//1213import SwiftUI14import AppKit1516struct BookmarksManagerView: View {17    let onOpen: (String) -> Void18    @EnvironmentObject private var env: AppEnvironment19    @EnvironmentObject private var themeEngine: ThemeEngine20    @Environment(\.dismiss) private var dismiss21    private var theme: AtlasTheme { themeEngine.theme }2223    @State private var query = ""24    @State private var editing: Bookmark?2526    var body: some View {27        VStack(spacing: 0) {28            header29            Divider().overlay(theme.border)30            list31        }32        .frame(width: 640, height: 600)33        .background(theme.backgroundColor)34        .sheet(item: $editing) { bm in editor(bm) }35    }3637    private var header: some View {38        HStack {39            Label("Favorites", systemImage: "star").font(ZyquoFont.title).foregroundStyle(theme.textPrimary)40            Spacer()41            HStack(spacing: ZyquoSpacing.xs) {42                Image(systemName: "magnifyingglass").font(.system(size: 11)).foregroundStyle(theme.textTertiary)43                TextField("Search favorites", text: $query).textFieldStyle(.plain).font(ZyquoFont.control)44                    .foregroundStyle(theme.textPrimary).frame(width: 180)45            }46            .padding(.horizontal, ZyquoSpacing.sm).frame(height: 28)47            .background(RoundedRectangle(cornerRadius: ZyquoRadius.small).fill(theme.surfaceSecondary))48            Button("Import") { importHTML() }.foregroundStyle(theme.accent)49            Button("Export") { exportHTML() }.foregroundStyle(theme.accent)50            Button("Done") { dismiss() }.foregroundStyle(theme.accent)51        }52        .padding(ZyquoSpacing.md).background(theme.surface)53    }5455    private var list: some View {56        ScrollView {57            LazyVStack(spacing: 0) {58                ForEach(env.bookmarks.search(query)) { bm in59                    row(bm)60                    Divider().overlay(theme.border)61                }62                if env.bookmarks.bookmarks.isEmpty {63                    Text("No favorites yet — bookmark a page with ⌘D")64                        .font(ZyquoFont.caption).foregroundStyle(theme.textTertiary)65                        .frame(maxWidth: .infinity).padding(ZyquoSpacing.xxl)66                }67            }68        }69    }7071    private func row(_ bm: Bookmark) -> some View {72        HStack(spacing: ZyquoSpacing.sm) {73            Image(systemName: bm.onBar ? "star.fill" : "globe")74                .font(.system(size: 11)).foregroundStyle(bm.onBar ? theme.accent : theme.textTertiary)75            VStack(alignment: .leading, spacing: 1) {76                Text(bm.title).font(ZyquoFont.body(size: 12.5)).lineLimit(1).foregroundStyle(theme.textPrimary)77                Text(bm.url).font(ZyquoFont.caption).lineLimit(1).foregroundStyle(theme.textTertiary)78            }79            Spacer()80            if !bm.tags.isEmpty {81                Text(bm.tags.joined(separator: ", ")).font(ZyquoFont.caption).foregroundStyle(theme.accentIndigo)82            }83            Button { editing = bm } label: { Image(systemName: "pencil") }84                .buttonStyle(.plain).foregroundStyle(theme.textSecondary)85            Button { env.bookmarks.delete(bm.id) } label: { Image(systemName: "trash") }86                .buttonStyle(.plain).foregroundStyle(theme.textTertiary)87        }88        .padding(.horizontal, ZyquoSpacing.md).padding(.vertical, ZyquoSpacing.xs)89        .contentShape(Rectangle())90        .onTapGesture { onOpen(bm.url); dismiss() }91    }9293    private func editor(_ bm: Bookmark) -> some View {94        var draft = bm95        return VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {96            Text("Edit Favorite").font(ZyquoFont.bodyEmphasis()).foregroundStyle(theme.textPrimary)97            EditorField(label: "Title", text: draft.title) { draft.title = $0 }98            EditorField(label: "URL", text: draft.url) { draft.url = $0 }99            EditorField(label: "Tags (comma-separated)", text: draft.tags.joined(separator: ", ")) {100                draft.tags = $0.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }101            }102            EditorField(label: "Notes", text: draft.notes) { draft.notes = $0 }103            Toggle("Show on bookmarks bar", isOn: Binding(get: { draft.onBar }, set: { draft.onBar = $0 }))104                .toggleStyle(.switch).font(ZyquoFont.body()).foregroundStyle(theme.textPrimary)105            HStack {106                Spacer()107                Button("Cancel") { editing = nil }108                Button("Save") { env.bookmarks.update(draft); editing = nil }.keyboardShortcut(.defaultAction)109            }110        }111        .padding(ZyquoSpacing.lg).frame(width: 420).background(theme.surface)112    }113114    private func importHTML() {115        let p = NSOpenPanel(); p.allowedContentTypes = [.html]; p.allowsMultipleSelection = false116        if p.runModal() == .OK, let url = p.url { _ = try? env.bookmarks.importHTML(from: url) }117    }118    private func exportHTML() {119        let p = NSSavePanel(); p.allowedContentTypes = [.html]; p.nameFieldStringValue = "zyquo-atlas-bookmarks.html"120        if p.runModal() == .OK, let url = p.url { try? env.bookmarks.exportHTML(to: url) }121    }122}123124/// A labeled single-line editor row that reports edits via a callback.125private struct EditorField: View {126    let label: String127    @State var text: String128    let onChange: (String) -> Void129    @EnvironmentObject private var themeEngine: ThemeEngine130131    var body: some View {132        VStack(alignment: .leading, spacing: 2) {133            Text(label).font(ZyquoFont.caption).foregroundStyle(themeEngine.theme.textTertiary)134            TextField("", text: $text)135                .textFieldStyle(.roundedBorder)136                .onChange(of: text) { onChange($0) }137        }138    }139}140