feat: added bookmarks 182fdc78
Steve · 2025-12-24 16:51 3 file(s) · +185 −0
Titan/Services/BookmarkManager.swift (added) +69 −0
1 +
//
2 +
//  BookmarkManager.swift
3 +
//  Titan
4 +
//
5 +
6 +
import Foundation
7 +
import SwiftUI
8 +
9 +
struct Bookmark: Identifiable, Codable, Equatable {
10 +
    let id: UUID
11 +
    let url: String
12 +
    let title: String
13 +
    let dateAdded: Date
14 +
15 +
    init(id: UUID = UUID(), url: String, title: String, dateAdded: Date = Date()) {
16 +
        self.id = id
17 +
        self.url = url
18 +
        self.title = title
19 +
        self.dateAdded = dateAdded
20 +
    }
21 +
}
22 +
23 +
@Observable
24 +
class BookmarkManager {
25 +
    private let storageKey = "titan_bookmarks"
26 +
27 +
    var bookmarks: [Bookmark] = []
28 +
29 +
    init() {
30 +
        loadBookmarks()
31 +
    }
32 +
33 +
    func addBookmark(url: String, title: String) {
34 +
        // Don't add duplicate URLs
35 +
        guard !bookmarks.contains(where: { $0.url == url }) else { return }
36 +
37 +
        let bookmark = Bookmark(url: url, title: title)
38 +
        bookmarks.insert(bookmark, at: 0)
39 +
        saveBookmarks()
40 +
    }
41 +
42 +
    func removeBookmark(_ bookmark: Bookmark) {
43 +
        bookmarks.removeAll { $0.id == bookmark.id }
44 +
        saveBookmarks()
45 +
    }
46 +
47 +
    func removeBookmarks(at offsets: IndexSet) {
48 +
        bookmarks.remove(atOffsets: offsets)
49 +
        saveBookmarks()
50 +
    }
51 +
52 +
    func isBookmarked(url: String) -> Bool {
53 +
        bookmarks.contains { $0.url == url }
54 +
    }
55 +
56 +
    private func saveBookmarks() {
57 +
        if let data = try? JSONEncoder().encode(bookmarks) {
58 +
            UserDefaults.standard.set(data, forKey: storageKey)
59 +
        }
60 +
    }
61 +
62 +
    private func loadBookmarks() {
63 +
        guard let data = UserDefaults.standard.data(forKey: storageKey),
64 +
              let decoded = try? JSONDecoder().decode([Bookmark].self, from: data) else {
65 +
            return
66 +
        }
67 +
        bookmarks = decoded
68 +
    }
69 +
}
Titan/Views/BookmarksListView.swift (added) +66 −0
1 +
//
2 +
//  BookmarksListView.swift
3 +
//  Titan
4 +
//
5 +
6 +
import SwiftUI
7 +
8 +
struct BookmarksListView: View {
9 +
    @Bindable var bookmarkManager: BookmarkManager
10 +
    let onSelect: (Bookmark) -> Void
11 +
12 +
    @Environment(\.dismiss) private var dismiss
13 +
14 +
    var body: some View {
15 +
        NavigationStack {
16 +
            Group {
17 +
                if bookmarkManager.bookmarks.isEmpty {
18 +
                    ContentUnavailableView(
19 +
                        "No Bookmarks",
20 +
                        systemImage: "bookmark",
21 +
                        description: Text("Add bookmarks from the menu while browsing")
22 +
                    )
23 +
                } else {
24 +
                    List {
25 +
                        ForEach(bookmarkManager.bookmarks) { bookmark in
26 +
                            Button {
27 +
                                onSelect(bookmark)
28 +
                            } label: {
29 +
                                VStack(alignment: .leading, spacing: 4) {
30 +
                                    Text(bookmark.title)
31 +
                                        .font(.system(.body, design: .monospaced))
32 +
                                        .foregroundStyle(.primary)
33 +
                                        .lineLimit(2)
34 +
35 +
                                    Text(bookmark.url)
36 +
                                        .font(.system(.caption, design: .monospaced))
37 +
                                        .foregroundStyle(.secondary)
38 +
                                        .lineLimit(1)
39 +
                                }
40 +
                                .padding(.vertical, 4)
41 +
                            }
42 +
                        }
43 +
                        .onDelete { offsets in
44 +
                            bookmarkManager.removeBookmarks(at: offsets)
45 +
                        }
46 +
                    }
47 +
                }
48 +
            }
49 +
            .navigationTitle("Bookmarks")
50 +
            .navigationBarTitleDisplayMode(.inline)
51 +
            .toolbar {
52 +
                ToolbarItem(placement: .cancellationAction) {
53 +
                    Button("Done") {
54 +
                        dismiss()
55 +
                    }
56 +
                }
57 +
58 +
                if !bookmarkManager.bookmarks.isEmpty {
59 +
                    ToolbarItem(placement: .primaryAction) {
60 +
                        EditButton()
61 +
                    }
62 +
                }
63 +
            }
64 +
        }
65 +
    }
66 +
}
Titan/Views/ContentView.swift +50 −0
53 53
    // Current fetch task (for cancellation)
54 54
    @State private var currentFetchTask: Task<Void, Never>?
55 55
56 +
    // Bookmarks
57 +
    @State private var bookmarkManager = BookmarkManager()
58 +
    @State private var showBookmarks = false
59 +
56 60
    private let maxRedirects = 5
57 61
58 62
    var body: some View {
123 127
                                } label: {
124 128
                                    Label("Home", systemImage: "house")
125 129
                                }
130 +
131 +
                                Divider()
132 +
133 +
                                Button {
134 +
                                    addCurrentPageToBookmarks()
135 +
                                } label: {
136 +
                                    if bookmarkManager.isBookmarked(url: urlText) {
137 +
                                        Label("Bookmarked", systemImage: "bookmark.fill")
138 +
                                    } else {
139 +
                                        Label("Add Bookmark", systemImage: "bookmark")
140 +
                                    }
141 +
                                }
142 +
                                .disabled(urlText.isEmpty || bookmarkManager.isBookmarked(url: urlText))
143 +
144 +
                                Button {
145 +
                                    showBookmarks = true
146 +
                                } label: {
147 +
                                    Label("Bookmarks", systemImage: "book")
148 +
                                }
126 149
                            } label: {
127 150
                                Image(systemName: "ellipsis.circle")
128 151
                                    .font(.title2)
164 187
                }
165 188
            }
166 189
        }
190 +
        .sheet(isPresented: $showBookmarks) {
191 +
            BookmarksListView(bookmarkManager: bookmarkManager) { bookmark in
192 +
                showBookmarks = false
193 +
                navigateTo(bookmark.url)
194 +
            }
195 +
        }
167 196
    }
168 197
169 198
    private func submitInput() {
174 203
        inputValue = ""
175 204
176 205
        navigateTo(urlWithQuery)
206 +
    }
207 +
208 +
    // MARK: - Bookmarks
209 +
210 +
    private func addCurrentPageToBookmarks() {
211 +
        guard !urlText.isEmpty else { return }
212 +
        let title = extractPageTitle() ?? urlText
213 +
        bookmarkManager.addBookmark(url: urlText, title: title)
214 +
    }
215 +
216 +
    private func extractPageTitle() -> String? {
217 +
        let lines = TitanParser.parse(responseText, baseURL: urlText)
218 +
        for line in lines {
219 +
            switch line {
220 +
            case .heading1(let text), .heading2(let text), .heading3(let text):
221 +
                return text
222 +
            default:
223 +
                continue
224 +
            }
225 +
        }
226 +
        return nil
177 227
    }
178 228
179 229
    // MARK: - Navigation History