chore: refactored contentview fad3aaec
Steve · 2025-12-25 13:34 3 file(s) · +255 −164
Titan/Components/IndeterminateProgressBar.swift (added) +32 −0
1 +
//
2 +
//  IndeterminateProgressBar.swift
3 +
//  Titan
4 +
//
5 +
6 +
import SwiftUI
7 +
8 +
struct IndeterminateProgressBar: View {
9 +
    let color: Color
10 +
11 +
    @State private var animationOffset: CGFloat = -1.0
12 +
13 +
    var body: some View {
14 +
        GeometryReader { geometry in
15 +
            Rectangle()
16 +
                .fill(color)
17 +
                .frame(width: geometry.size.width * 0.3)
18 +
                .offset(x: animationOffset * geometry.size.width)
19 +
        }
20 +
        .frame(height: 3)
21 +
        .clipped()
22 +
        .onAppear {
23 +
            withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
24 +
                animationOffset = 1.0
25 +
            }
26 +
        }
27 +
    }
28 +
}
29 +
30 +
#Preview {
31 +
    IndeterminateProgressBar(color: .blue)
32 +
}
Titan/Views/BrowserToolbar.swift (added) +193 −0
1 +
//
2 +
//  BrowserToolbar.swift
3 +
//  Titan
4 +
//
5 +
6 +
import SwiftUI
7 +
8 +
struct BrowserToolbar: View {
9 +
    @EnvironmentObject private var themeSettings: ThemeSettings
10 +
11 +
    // State bindings
12 +
    @Binding var urlText: String
13 +
    var isURLFocused: FocusState<Bool>.Binding
14 +
    let isLoading: Bool
15 +
    let canGoBack: Bool
16 +
    let canGoForward: Bool
17 +
    let tabCount: Int
18 +
    let isBookmarked: Bool
19 +
    let canCloseTab: Bool
20 +
21 +
    // Callbacks
22 +
    let onBack: () -> Void
23 +
    let onForward: () -> Void
24 +
    let onSubmitURL: () -> Void
25 +
    let onDismissKeyboard: () -> Void
26 +
    let onShowTabs: () -> Void
27 +
    let onNewTab: () -> Void
28 +
    let onCloseTab: () -> Void
29 +
    let onShowSettings: () -> Void
30 +
    let onShowBookmarks: () -> Void
31 +
    let onAddBookmark: () -> Void
32 +
    let onShowHistory: () -> Void
33 +
    let onGoHome: () -> Void
34 +
35 +
    var body: some View {
36 +
        VStack(spacing: 0) {
37 +
            if isLoading {
38 +
                IndeterminateProgressBar(color: themeSettings.progressBarColor)
39 +
            } else {
40 +
                Color.clear
41 +
                    .frame(height: 3)
42 +
            }
43 +
44 +
            GlassEffectContainer {
45 +
                HStack(spacing: 12) {
46 +
                    // Navigation buttons in a single pill
47 +
                    if !isURLFocused.wrappedValue {
48 +
                        navigationButtons
49 +
                    }
50 +
51 +
                    urlTextField
52 +
53 +
                    if isURLFocused.wrappedValue {
54 +
                        dismissButton
55 +
                    } else {
56 +
                        menuButton
57 +
                    }
58 +
                }
59 +
                .animation(.easeInOut(duration: 0.25), value: isURLFocused.wrappedValue)
60 +
            }
61 +
            .padding(.top, 8)
62 +
        }
63 +
        .padding(.horizontal, 20)
64 +
        .padding(.bottom, 8)
65 +
    }
66 +
67 +
    // MARK: - Subviews
68 +
69 +
    private var navigationButtons: some View {
70 +
        HStack(spacing: 0) {
71 +
            Button(action: onBack) {
72 +
                Image(systemName: "chevron.left")
73 +
                    .font(.title2)
74 +
                    .foregroundStyle(canGoBack && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
75 +
                    .frame(width: 44, height: 44)
76 +
            }
77 +
            .disabled(!canGoBack || isLoading)
78 +
79 +
            Divider()
80 +
                .frame(height: 24)
81 +
82 +
            Button(action: onForward) {
83 +
                Image(systemName: "chevron.right")
84 +
                    .font(.title2)
85 +
                    .foregroundStyle(canGoForward && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
86 +
                    .frame(width: 44, height: 44)
87 +
            }
88 +
            .disabled(!canGoForward || isLoading)
89 +
        }
90 +
        .glassEffect(.regular.interactive())
91 +
        .transition(.opacity.combined(with: .scale(scale: 0.8)))
92 +
    }
93 +
94 +
    private var urlTextField: some View {
95 +
        TextField("Enter Gemini URL", text: $urlText)
96 +
            .focused(isURLFocused)
97 +
            .autocapitalization(.none)
98 +
            .disableAutocorrection(true)
99 +
            .keyboardType(.URL)
100 +
            .submitLabel(.go)
101 +
            .onSubmit {
102 +
                isURLFocused.wrappedValue = false
103 +
                onSubmitURL()
104 +
            }
105 +
            .padding(.horizontal, 12)
106 +
            .padding(.vertical, 12)
107 +
            .glassEffect(.regular, in: .capsule)
108 +
    }
109 +
110 +
    private var dismissButton: some View {
111 +
        Button {
112 +
            isURLFocused.wrappedValue = false
113 +
        } label: {
114 +
            Image(systemName: "xmark.circle.fill")
115 +
                .font(.title2)
116 +
                .foregroundStyle(themeSettings.toolbarButtonColor)
117 +
                .frame(width: 44, height: 44)
118 +
        }
119 +
        .glassEffect(.regular.interactive())
120 +
        .transition(.opacity.combined(with: .scale(scale: 0.8)))
121 +
    }
122 +
123 +
    private var menuButton: some View {
124 +
        Menu {
125 +
            Button {
126 +
                onShowTabs()
127 +
            } label: {
128 +
                Label("Tabs (\(tabCount))", systemImage: "square.on.square")
129 +
            }
130 +
131 +
            Button {
132 +
                onNewTab()
133 +
            } label: {
134 +
                Label("New Tab", systemImage: "plus")
135 +
            }
136 +
137 +
            Button {
138 +
                onCloseTab()
139 +
            } label: {
140 +
                Label("Close Tab", systemImage: "xmark")
141 +
            }
142 +
            .disabled(!canCloseTab)
143 +
144 +
            Divider()
145 +
146 +
            Button {
147 +
                onShowSettings()
148 +
            } label: {
149 +
                Label("Settings", systemImage: "gear")
150 +
            }
151 +
152 +
            Divider()
153 +
154 +
            Button {
155 +
                onShowBookmarks()
156 +
            } label: {
157 +
                Label("Bookmarks", systemImage: "book")
158 +
            }
159 +
160 +
            Button {
161 +
                onAddBookmark()
162 +
            } label: {
163 +
                if isBookmarked {
164 +
                    Label("Bookmarked", systemImage: "bookmark.fill")
165 +
                } else {
166 +
                    Label("Add Bookmark", systemImage: "bookmark")
167 +
                }
168 +
            }
169 +
            .disabled(urlText.isEmpty || isBookmarked)
170 +
171 +
            Button {
172 +
                onShowHistory()
173 +
            } label: {
174 +
                Label("History", systemImage: "clock")
175 +
            }
176 +
177 +
            Divider()
178 +
179 +
            Button {
180 +
                onGoHome()
181 +
            } label: {
182 +
                Label("Home", systemImage: "house")
183 +
            }
184 +
        } label: {
185 +
            Image(systemName: "ellipsis.circle")
186 +
                .font(.title2)
187 +
                .foregroundStyle(themeSettings.toolbarButtonColor)
188 +
                .frame(width: 44, height: 44)
189 +
        }
190 +
        .glassEffect(.regular.interactive())
191 +
        .transition(.opacity.combined(with: .scale(scale: 0.8)))
192 +
    }
193 +
}
Titan/Views/ContentView.swift +30 −164
5 5
6 6
import SwiftUI
7 7
8 -
struct IndeterminateProgressBar: View {
9 -
    let color: Color
10 -
11 -
    @State private var animationOffset: CGFloat = -1.0
12 -
13 -
    var body: some View {
14 -
        GeometryReader { geometry in
15 -
            Rectangle()
16 -
                .fill(color)
17 -
                .frame(width: geometry.size.width * 0.3)
18 -
                .offset(x: animationOffset * geometry.size.width)
19 -
        }
20 -
        .frame(height: 3)
21 -
        .clipped()
22 -
        .onAppear {
23 -
            withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
24 -
                animationOffset = 1.0
25 -
            }
26 -
        }
27 -
    }
28 -
}
29 -
30 8
struct ContentView: View {
31 9
    @EnvironmentObject private var themeSettings: ThemeSettings
32 10
    @State private var urlText = ""
88 66
            }
89 67
            .contentMargins(.top, 20, for: .scrollContent)
90 68
            .safeAreaInset(edge: .bottom) {
91 -
                VStack(spacing: 0) {
92 -
                    if isLoading {
93 -
                        IndeterminateProgressBar(color: themeSettings.progressBarColor)
94 -
                    } else {
95 -
                        Color.clear
96 -
                            .frame(height: 3)
97 -
                    }
98 -
99 -
                    GlassEffectContainer {
100 -
                        HStack(spacing: 12) {
101 -
                            // Navigation buttons in a single pill
102 -
                            if !isURLFocused {
103 -
                                HStack(spacing: 0) {
104 -
                                    Button(action: goBack) {
105 -
                                        Image(systemName: "chevron.left")
106 -
                                            .font(.title2)
107 -
                                            .foregroundStyle(canGoBack && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
108 -
                                            .frame(width: 44, height: 44)
109 -
                                    }
110 -
                                    .disabled(!canGoBack || isLoading)
111 -
112 -
                                    Divider()
113 -
                                        .frame(height: 24)
114 -
115 -
                                    Button(action: goForward) {
116 -
                                        Image(systemName: "chevron.right")
117 -
                                            .font(.title2)
118 -
                                            .foregroundStyle(canGoForward && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
119 -
                                            .frame(width: 44, height: 44)
120 -
                                    }
121 -
                                    .disabled(!canGoForward || isLoading)
122 -
                                }
123 -
                                .glassEffect(.regular.interactive())
124 -
                                .transition(.opacity.combined(with: .scale(scale: 0.8)))
125 -
                            }
126 -
127 -
                            TextField("Enter Gemini URL", text: $urlText)
128 -
                                .focused($isURLFocused)
129 -
                                .autocapitalization(.none)
130 -
                                .disableAutocorrection(true)
131 -
                                .keyboardType(.URL)
132 -
                                .submitLabel(.go)
133 -
                                .onSubmit {
134 -
                                    isURLFocused = false
135 -
                                    navigateTo(urlText)
136 -
                                }
137 -
                                .padding(.horizontal, 12)
138 -
                                .padding(.vertical, 12)
139 -
                                .glassEffect(.regular, in: .capsule)
140 -
141 -
                            if isURLFocused {
142 -
                                Button {
143 -
                                    isURLFocused = false
144 -
                                } label: {
145 -
                                    Image(systemName: "xmark.circle.fill")
146 -
                                        .font(.title2)
147 -
                                        .foregroundStyle(themeSettings.toolbarButtonColor)
148 -
                                        .frame(width: 44, height: 44)
149 -
                                }
150 -
                                .glassEffect(.regular.interactive())
151 -
                                .transition(.opacity.combined(with: .scale(scale: 0.8)))
152 -
                            } else {
153 -
                                Menu {
154 -
                                    Button {
155 -
                                        showTabs = true
156 -
                                    } label: {
157 -
                                        Label("Tabs (\(tabManager.tabs.count))", systemImage: "square.on.square")
158 -
                                    }
159 -
160 -
                                    Button {
161 -
                                        saveCurrentTabState()
162 -
                                        tabManager.createTab(url: themeSettings.homePage)
163 -
                                        loadActiveTabState()
164 -
                                        navigateTo(themeSettings.homePage)
165 -
                                    } label: {
166 -
                                        Label("New Tab", systemImage: "plus")
167 -
                                    }
168 -
169 -
                                    Button {
170 -
                                        tabManager.closeTab(id: tabManager.activeTabId)
171 -
                                        loadActiveTabState()
172 -
                                    } label: {
173 -
                                        Label("Close Tab", systemImage: "xmark")
174 -
                                    }
175 -
                                    .disabled(tabManager.tabs.count <= 1)
176 -
177 -
                                    Divider()
178 -
179 -
                                    Button {
180 -
                                        showSettings = true
181 -
                                    } label: {
182 -
                                        Label("Settings", systemImage: "gear")
183 -
                                    }
184 -
185 -
                                    Divider()
186 -
187 -
                                    Button {
188 -
                                        showBookmarks = true
189 -
                                    } label: {
190 -
                                        Label("Bookmarks", systemImage: "book")
191 -
                                    }
192 -
193 -
                                    Button {
194 -
                                        addCurrentPageToBookmarks()
195 -
                                    } label: {
196 -
                                        if bookmarkManager.isBookmarked(url: urlText) {
197 -
                                            Label("Bookmarked", systemImage: "bookmark.fill")
198 -
                                        } else {
199 -
                                            Label("Add Bookmark", systemImage: "bookmark")
200 -
                                        }
201 -
                                    }
202 -
                                    .disabled(urlText.isEmpty || bookmarkManager.isBookmarked(url: urlText))
203 -
204 -
                                    Button {
205 -
                                        showHistory = true
206 -
                                    } label: {
207 -
                                        Label("History", systemImage: "clock")
208 -
                                    }
209 -
210 -
                                    Divider()
211 -
212 -
                                    Button {
213 -
                                        navigateTo(themeSettings.homePage)
214 -
                                    } label: {
215 -
                                        Label("Home", systemImage: "house")
216 -
                                    }
217 -
                                } label: {
218 -
                                    Image(systemName: "ellipsis.circle")
219 -
                                        .font(.title2)
220 -
                                        .foregroundStyle(themeSettings.toolbarButtonColor)
221 -
                                        .frame(width: 44, height: 44)
222 -
                                }
223 -
                                .glassEffect(.regular.interactive())
224 -
                                .transition(.opacity.combined(with: .scale(scale: 0.8)))
225 -
                            }
226 -
                        }
227 -
                        .animation(.easeInOut(duration: 0.25), value: isURLFocused)
228 -
                    }
229 -
                    .padding(.top, 8)
230 -
                }
231 -
                .padding(.horizontal, 20)
232 -
                .padding(.bottom, 8)
69 +
                BrowserToolbar(
70 +
                    urlText: $urlText,
71 +
                    isURLFocused: $isURLFocused,
72 +
                    isLoading: isLoading,
73 +
                    canGoBack: canGoBack,
74 +
                    canGoForward: canGoForward,
75 +
                    tabCount: tabManager.tabs.count,
76 +
                    isBookmarked: bookmarkManager.isBookmarked(url: urlText),
77 +
                    canCloseTab: tabManager.tabs.count > 1,
78 +
                    onBack: goBack,
79 +
                    onForward: goForward,
80 +
                    onSubmitURL: { navigateTo(urlText) },
81 +
                    onDismissKeyboard: { isURLFocused = false },
82 +
                    onShowTabs: { showTabs = true },
83 +
                    onNewTab: {
84 +
                        saveCurrentTabState()
85 +
                        tabManager.createTab(url: themeSettings.homePage)
86 +
                        loadActiveTabState()
87 +
                        navigateTo(themeSettings.homePage)
88 +
                    },
89 +
                    onCloseTab: {
90 +
                        tabManager.closeTab(id: tabManager.activeTabId)
91 +
                        loadActiveTabState()
92 +
                    },
93 +
                    onShowSettings: { showSettings = true },
94 +
                    onShowBookmarks: { showBookmarks = true },
95 +
                    onAddBookmark: addCurrentPageToBookmarks,
96 +
                    onShowHistory: { showHistory = true },
97 +
                    onGoHome: { navigateTo(themeSettings.homePage) }
98 +
                )
233 99
            }
234 100
        }
235 101
        .onAppear {