feat: added history 13977ba9
Steve · 2025-12-25 12:56 4 file(s) · +226 −4
Titan.xcodeproj/project.pbxproj +4 −4
400 400
				DEVELOPMENT_TEAM = W8QNM2N67P;
401 401
				ENABLE_PREVIEWS = YES;
402 402
				GENERATE_INFOPLIST_FILE = YES;
403 -
				INFOPLIST_KEY_CFBundleDisplayName = Titan;
403 +
				INFOPLIST_KEY_CFBundleDisplayName = "Titan II";
404 404
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
405 405
				INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Titan needs access to save images from Gemini pages";
406 406
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
413 413
					"@executable_path/Frameworks",
414 414
				);
415 415
				MARKETING_VERSION = 0.1;
416 -
				PRODUCT_BUNDLE_IDENTIFIER = com.stevedylandev.Titan;
416 +
				PRODUCT_BUNDLE_IDENTIFIER = com.stevedylandev.TitanII;
417 417
				PRODUCT_NAME = "$(TARGET_NAME)";
418 418
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
419 419
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
439 439
				DEVELOPMENT_TEAM = W8QNM2N67P;
440 440
				ENABLE_PREVIEWS = YES;
441 441
				GENERATE_INFOPLIST_FILE = YES;
442 -
				INFOPLIST_KEY_CFBundleDisplayName = Titan;
442 +
				INFOPLIST_KEY_CFBundleDisplayName = "Titan II";
443 443
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
444 444
				INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Titan needs access to save images from Gemini pages";
445 445
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
452 452
					"@executable_path/Frameworks",
453 453
				);
454 454
				MARKETING_VERSION = 0.1;
455 -
				PRODUCT_BUNDLE_IDENTIFIER = com.stevedylandev.Titan;
455 +
				PRODUCT_BUNDLE_IDENTIFIER = com.stevedylandev.TitanII;
456 456
				PRODUCT_NAME = "$(TARGET_NAME)";
457 457
				STRING_CATALOG_GENERATE_SYMBOLS = YES;
458 458
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Titan/Services/HistoryManager.swift (added) +67 −0
1 +
//
2 +
//  HistoryManager.swift
3 +
//  Titan
4 +
//
5 +
6 +
import Foundation
7 +
import SwiftUI
8 +
9 +
struct HistoryItem: Identifiable, Codable, Equatable {
10 +
    let id: UUID
11 +
    let url: String
12 +
    let title: String
13 +
    let visitedAt: Date
14 +
15 +
    init(id: UUID = UUID(), url: String, title: String, visitedAt: Date = Date()) {
16 +
        self.id = id
17 +
        self.url = url
18 +
        self.title = title
19 +
        self.visitedAt = visitedAt
20 +
    }
21 +
}
22 +
23 +
@Observable
24 +
class HistoryManager {
25 +
    private let storageKey = "titan_history"
26 +
27 +
    var items: [HistoryItem] = []
28 +
29 +
    init() {
30 +
        loadHistory()
31 +
    }
32 +
33 +
    func addToHistory(url: String, title: String) {
34 +
        let item = HistoryItem(url: url, title: title)
35 +
        items.insert(item, at: 0)
36 +
        saveHistory()
37 +
    }
38 +
39 +
    func removeItem(_ item: HistoryItem) {
40 +
        items.removeAll { $0.id == item.id }
41 +
        saveHistory()
42 +
    }
43 +
44 +
    func removeItems(at offsets: IndexSet) {
45 +
        items.remove(atOffsets: offsets)
46 +
        saveHistory()
47 +
    }
48 +
49 +
    func clearAll() {
50 +
        items.removeAll()
51 +
        saveHistory()
52 +
    }
53 +
54 +
    private func saveHistory() {
55 +
        if let data = try? JSONEncoder().encode(items) {
56 +
            UserDefaults.standard.set(data, forKey: storageKey)
57 +
        }
58 +
    }
59 +
60 +
    private func loadHistory() {
61 +
        guard let data = UserDefaults.standard.data(forKey: storageKey),
62 +
              let decoded = try? JSONDecoder().decode([HistoryItem].self, from: data) else {
63 +
            return
64 +
        }
65 +
        items = decoded
66 +
    }
67 +
}
Titan/Views/ContentView.swift +20 −0
55 55
    @State private var bookmarkManager = BookmarkManager()
56 56
    @State private var showBookmarks = false
57 57
58 +
    // History
59 +
    @State private var historyManager = HistoryManager()
60 +
    @State private var showHistory = false
61 +
58 62
    // Settings
59 63
    @State private var showSettings = false
60 64
148 152
                                    }
149 153
                                }
150 154
                                .disabled(urlText.isEmpty || bookmarkManager.isBookmarked(url: urlText))
155 +
156 +
                                Button {
157 +
                                    showHistory = true
158 +
                                } label: {
159 +
                                    Label("History", systemImage: "clock")
160 +
                                }
151 161
152 162
                                Divider()
153 163
                                
206 216
        .sheet(isPresented: $showSettings) {
207 217
            SettingsView()
208 218
        }
219 +
        .sheet(isPresented: $showHistory) {
220 +
            HistoryListView(historyManager: historyManager) { item in
221 +
                showHistory = false
222 +
                navigateTo(item.url)
223 +
            }
224 +
        }
209 225
    }
210 226
211 227
    private func submitInput() {
319 335
                        if addToHistory {
320 336
                            history.append(finalURL)
321 337
                            historyIndex = history.count - 1
338 +
339 +
                            // Add to persistent history
340 +
                            let title = extractPageTitle() ?? finalURL
341 +
                            historyManager.addToHistory(url: finalURL, title: title)
322 342
                        }
323 343
                    }
324 344
                case .input:
Titan/Views/HistoryListView.swift (added) +135 −0
1 +
//
2 +
//  HistoryListView.swift
3 +
//  Titan
4 +
//
5 +
6 +
import SwiftUI
7 +
8 +
struct HistoryListView: View {
9 +
    @Bindable var historyManager: HistoryManager
10 +
    let onSelect: (HistoryItem) -> Void
11 +
12 +
    @Environment(\.dismiss) private var dismiss
13 +
    @State private var showClearConfirmation = false
14 +
15 +
    private var groupedHistory: [(String, [HistoryItem])] {
16 +
        let calendar = Calendar.current
17 +
        let now = Date()
18 +
19 +
        var groups: [String: [HistoryItem]] = [:]
20 +
21 +
        for item in historyManager.items {
22 +
            let key: String
23 +
            if calendar.isDateInToday(item.visitedAt) {
24 +
                key = "Today"
25 +
            } else if calendar.isDateInYesterday(item.visitedAt) {
26 +
                key = "Yesterday"
27 +
            } else if let weekAgo = calendar.date(byAdding: .day, value: -7, to: now),
28 +
                      item.visitedAt > weekAgo {
29 +
                key = "This Week"
30 +
            } else {
31 +
                key = "Older"
32 +
            }
33 +
34 +
            groups[key, default: []].append(item)
35 +
        }
36 +
37 +
        let order = ["Today", "Yesterday", "This Week", "Older"]
38 +
        return order.compactMap { key in
39 +
            guard let items = groups[key], !items.isEmpty else { return nil }
40 +
            return (key, items)
41 +
        }
42 +
    }
43 +
44 +
    var body: some View {
45 +
        NavigationStack {
46 +
            Group {
47 +
                if historyManager.items.isEmpty {
48 +
                    ContentUnavailableView(
49 +
                        "No History",
50 +
                        systemImage: "clock",
51 +
                        description: Text("Pages you visit will appear here")
52 +
                    )
53 +
                } else {
54 +
                    List {
55 +
                        ForEach(groupedHistory, id: \.0) { section, items in
56 +
                            Section(section) {
57 +
                                ForEach(items) { item in
58 +
                                    Button {
59 +
                                        onSelect(item)
60 +
                                    } label: {
61 +
                                        VStack(alignment: .leading, spacing: 4) {
62 +
                                            Text(item.title)
63 +
                                                .font(.system(.body, design: .monospaced))
64 +
                                                .foregroundStyle(.primary)
65 +
                                                .lineLimit(2)
66 +
67 +
                                            HStack {
68 +
                                                Text(item.url)
69 +
                                                    .font(.system(.caption, design: .monospaced))
70 +
                                                    .foregroundStyle(.secondary)
71 +
                                                    .lineLimit(1)
72 +
73 +
                                                Spacer()
74 +
75 +
                                                Text(item.visitedAt, style: .time)
76 +
                                                    .font(.system(.caption2, design: .monospaced))
77 +
                                                    .foregroundStyle(.tertiary)
78 +
                                            }
79 +
                                        }
80 +
                                        .padding(.vertical, 4)
81 +
                                    }
82 +
                                }
83 +
                                .onDelete { offsets in
84 +
                                    deleteItems(in: items, at: offsets)
85 +
                                }
86 +
                            }
87 +
                        }
88 +
                    }
89 +
                }
90 +
            }
91 +
            .navigationTitle("History")
92 +
            .navigationBarTitleDisplayMode(.inline)
93 +
            .toolbar {
94 +
                ToolbarItem(placement: .cancellationAction) {
95 +
                    Button("Done") {
96 +
                        dismiss()
97 +
                    }
98 +
                }
99 +
100 +
                if !historyManager.items.isEmpty {
101 +
                    ToolbarItem(placement: .primaryAction) {
102 +
                        Menu {
103 +
                            Button(role: .destructive) {
104 +
                                showClearConfirmation = true
105 +
                            } label: {
106 +
                                Label("Clear All History", systemImage: "trash")
107 +
                            }
108 +
                        } label: {
109 +
                            Image(systemName: "ellipsis.circle")
110 +
                        }
111 +
                    }
112 +
                }
113 +
            }
114 +
            .confirmationDialog(
115 +
                "Clear All History",
116 +
                isPresented: $showClearConfirmation,
117 +
                titleVisibility: .visible
118 +
            ) {
119 +
                Button("Clear All", role: .destructive) {
120 +
                    historyManager.clearAll()
121 +
                }
122 +
                Button("Cancel", role: .cancel) {}
123 +
            } message: {
124 +
                Text("This will permanently delete all browsing history.")
125 +
            }
126 +
        }
127 +
    }
128 +
129 +
    private func deleteItems(in sectionItems: [HistoryItem], at offsets: IndexSet) {
130 +
        for offset in offsets {
131 +
            let item = sectionItems[offset]
132 +
            historyManager.removeItem(item)
133 +
        }
134 +
    }
135 +
}