Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ora/Common/Constants/KeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ enum KeyboardShortcuts {
category: "Address",
defaultChord: KeyChord(keyEquivalent: .init("l"), modifiers: [.command])
)
static let searchInCurrentTab = KeyboardShortcutDefinition(
id: "address.searchInCurrentTab",
name: "Search in Current Tab",
category: "Address",
defaultChord: KeyChord(keyEquivalent: .init("g"), modifiers: [.command, .shift])
)
}

// MARK: - Edit
Expand Down Expand Up @@ -329,7 +335,7 @@ enum KeyboardShortcuts {
Window.new, Window.newPrivate, Window.close, Window.fullscreen,

// Address
Address.copyURL, Address.focus,
Address.copyURL, Address.focus, Address.searchInCurrentTab,

// Edit
Edit.find, Edit.findNext, Edit.findPrevious,
Expand Down
31 changes: 27 additions & 4 deletions ora/Common/Constants/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@ import SwiftUI
// swiftlint:disable identifier_name
struct Theme: Equatable {
let colorScheme: ColorScheme
let customPrimaryColor: String?
let customAccentColor: String?

init(colorScheme: ColorScheme, customPrimaryColor: String? = nil, customAccentColor: String? = nil) {
self.colorScheme = colorScheme
self.customPrimaryColor = customPrimaryColor
self.customAccentColor = customAccentColor
}

var primary: Color {
Color(hex: "#f3e5d6")
if let hex = customPrimaryColor {
return Color(hex: hex)
}
return Color(hex: "#d6f3ea")
}

var primaryDark: Color {
Color(hex: "#63411D")
// Use the same color as primary for both light and dark modes
return primary
}
Comment on lines 23 to 26
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change to make primaryDark return the same color as primary breaks existing theme logic. Several computed properties depend on primaryDark having a different color value:

  • activeTabBackground (line 56): Uses self.primaryDark.opacity(0.8) in light mode
  • mutedBackground (line 60): Uses self.primaryDark.opacity(0.1) in light mode
  • invertedSolidWindowBackgroundColor (line 52): Swaps primary and primaryDark based on color scheme

With primaryDark now returning the same color as primary, these will produce unexpected visual results. For example, invertedSolidWindowBackgroundColor will always return the same color regardless of the color scheme.

If the intent is to use a single customizable color, these dependent properties should be updated to use different opacity levels or alternative color derivation methods to maintain visual distinction.

Copilot uses AI. Check for mistakes.

var accent: Color {
Color(hex: "#FF5F57")
if let hex = customAccentColor {
return Color(hex: hex)
}
return Color(hex: "#575dff")
}

var background: Color {
Expand Down Expand Up @@ -140,8 +155,16 @@ extension EnvironmentValues {

struct ThemeProvider: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var settings = SettingsStore.shared

func body(content: Content) -> some View {
content.environment(\.theme, Theme(colorScheme: colorScheme))
content.environment(
\.theme,
Theme(
colorScheme: colorScheme,
customPrimaryColor: settings.themePrimaryColor,
customAccentColor: settings.themeAccentColor
)
)
}
}
13 changes: 13 additions & 0 deletions ora/Common/Utils/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ class SettingsStore: ObservableObject {
private let tabRemovalTimeoutKey = "settings.tabRemovalTimeout"
private let maxRecentTabsKey = "settings.maxRecentTabs"
private let autoPiPEnabledKey = "settings.autoPiPEnabled"
private let themePrimaryColorKey = "settings.theme.primaryColor"
private let themeAccentColorKey = "settings.theme.accentColor"

// MARK: - Per-Container

Expand Down Expand Up @@ -217,6 +219,14 @@ class SettingsStore: ObservableObject {
didSet { defaults.set(autoPiPEnabled, forKey: autoPiPEnabledKey) }
}

@Published var themePrimaryColor: String? {
didSet { defaults.set(themePrimaryColor, forKey: themePrimaryColorKey) }
}

@Published var themeAccentColor: String? {
didSet { defaults.set(themeAccentColor, forKey: themeAccentColorKey) }
}

init() {
autoUpdateEnabled = defaults.bool(forKey: autoUpdateKey)
blockThirdPartyTrackers = defaults.bool(forKey: trackingThirdPartyKey)
Expand Down Expand Up @@ -271,6 +281,9 @@ class SettingsStore: ObservableObject {
maxRecentTabs = maxRecentTabsValue == 0 ? 5 : maxRecentTabsValue

autoPiPEnabled = defaults.object(forKey: autoPiPEnabledKey) as? Bool ?? true

themePrimaryColor = defaults.string(forKey: themePrimaryColorKey)
themeAccentColor = defaults.string(forKey: themeAccentColorKey)
}

// MARK: - Per-container helpers
Expand Down
23 changes: 5 additions & 18 deletions ora/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down Expand Up @@ -43,18 +41,6 @@
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -71,12 +57,13 @@
<string>Owner</string>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.web-browser</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deployment target has been lowered to macOS 14.0, but the LSMinimumSystemVersion in Info.plist is set to 13.0. These values should be aligned for consistency. Consider updating LSMinimumSystemVersion to match the deployment target of 14.0, or ensure there's a specific reason for the mismatch.

Suggested change
<string>13.0</string>
<string>14.0</string>

Copilot uses AI. Check for mistakes.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
Comment on lines +64 to +65
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting NSAllowsArbitraryLoads to true disables App Transport Security (ATS) and allows all HTTP connections, which poses significant security risks. This makes the app vulnerable to man-in-the-middle attacks and exposes user data to unencrypted transmission.

While the PR description mentions enabling "sites which don't support http", this setting is too permissive. Consider a more secure approach:

  • Use NSExceptionDomains to allow HTTP only for specific domains that require it
  • Or use NSAllowsLocalNetworking if the goal is to support local development/testing
  • Document which specific sites require this and why HTTPS alternatives aren't available

This is especially important for a browser application that handles user credentials and sensitive data.

Suggested change
<key>NSAllowsArbitraryLoads</key>
<true/>
<!--
For security, do not allow arbitrary HTTP loads.
Instead, specify only the domains that require HTTP below.
Replace "example.com" with the actual domain(s) that require HTTP,
and document the reason for each.
-->
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
<!-- Reason: [REPLACE WITH EXPLANATION WHY HTTPS IS NOT AVAILABLE] -->
</dict>
<!-- Add more domains as needed -->
</dict>

Copilot uses AI. Check for mistakes.
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
Expand Down
25 changes: 18 additions & 7 deletions ora/Modules/Launcher/LauncherView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,22 @@ struct LauncherView: View {
if let engine = engineToUse,
let url = searchEngineService.createSearchURL(for: engine, query: correctInput)
{
tabManager
.openTab(
url: url,
historyManager: historyManager,
downloadManager: downloadManager,
isPrivate: privacyMode.isPrivate
)
if appState.launcherSearchInCurrentTab, let activeTab = tabManager.activeTab {
// Search in current tab
activeTab.loadURL(url.absoluteString)
} else {
// Create new tab (default behavior)
tabManager
.openTab(
url: url,
historyManager: historyManager,
downloadManager: downloadManager,
isPrivate: privacyMode.isPrivate
)
}
}
appState.showLauncher = false
appState.launcherSearchInCurrentTab = false
}

var body: some View {
Expand Down Expand Up @@ -101,6 +108,10 @@ struct LauncherView: View {
}
.onChange(of: appState.showLauncher) { _, newValue in
isVisible = newValue
if !newValue {
// Reset the flag when launcher is closed
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] When the launcher closes, the search input field is not being cleared. The input state variable in LauncherView retains its value, so when the launcher is reopened, the previous search text will still be visible.

Consider clearing the input when the launcher is closed:

.onChange(of: appState.showLauncher) { _, newValue in
    isVisible = newValue
    if !newValue {
        input = ""  // Clear input when closing
        match = nil  // Also clear any matched engine
        appState.launcherSearchInCurrentTab = false
    }
}

This provides a cleaner UX where each launcher invocation starts fresh.

Suggested change
// Reset the flag when launcher is closed
// Clear input and match when launcher is closed for a fresh start
input = ""
match = nil

Copilot uses AI. Check for mistakes.
appState.launcherSearchInCurrentTab = false
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand Down
4 changes: 4 additions & 0 deletions ora/Modules/Settings/Sections/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ struct GeneralSettingsView: View {
}

AppearanceSelector(selection: $appearanceManager.appearance)

ThemeColorPicker()
.padding(.vertical, 8)

VStack(alignment: .leading, spacing: 12) {
Text("Tab Management")
.font(.headline)
Expand Down
118 changes: 118 additions & 0 deletions ora/Modules/Settings/Sections/ThemeColorPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import SwiftUI

struct ThemeColorPicker: View {
@StateObject private var settings = SettingsStore.shared
@Environment(\.theme) var theme

var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Theme Colors")
.font(.headline)

Text("Customize the primary colors used throughout the browser.")
.font(.caption)
.foregroundColor(.secondary)

VStack(alignment: .leading, spacing: 16) {
// Primary Color (used for both light and dark modes)
ColorPickerRow(
title: "Primary Color",
description: "Used for backgrounds and accents in both light and dark modes",
color: Binding(
get: {
if let hex = settings.themePrimaryColor {
return Color(hex: hex)
}
return Color(hex: "#d6f3ea")
},
set: { newColor in
settings.themePrimaryColor = newColor.toHex()
}
),
defaultColor: Color(hex: "#d6f3ea"),
onReset: {
settings.themePrimaryColor = nil
}
)

// Accent Color
ColorPickerRow(
title: "Accent Color",
description: "Used for interactive elements and highlights",
color: Binding(
get: {
if let hex = settings.themeAccentColor {
return Color(hex: hex)
}
return Color(hex: "#575dff")
},
set: { newColor in
settings.themeAccentColor = newColor.toHex()
}
),
defaultColor: Color(hex: "#575dff"),
onReset: {
settings.themeAccentColor = nil
}
)
}
}
}
}

private struct ColorPickerRow: View {
let title: String
let description: String
@Binding var color: Color
let defaultColor: Color
let onReset: () -> Void

@State private var isUsingCustom = false

var body: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
Text(description)
.font(.caption2)
.foregroundColor(.secondary)
}

Spacer()

ColorPicker("", selection: $color, supportsOpacity: false)
.labelsHidden()
.frame(width: 50)

Button {
onReset()
isUsingCustom = false
} label: {
Text("Reset")
.font(.caption)
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
.disabled(!isUsingCustom)
Comment on lines +70 to +98
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isUsingCustom state is a local variable in ColorPickerRow, but it should be initialized based on whether a custom color is set in settings. Currently, it's only updated in onAppear and onChange, but if the binding's initial value differs from default, there could be a brief moment where the Reset button state is incorrect.

Additionally, the disabled state logic doesn't account for the scenario where the user manually changes the color picker back to the default color - the Reset button should become disabled, but isUsingCustom will still be true until the onChange fires.

Consider using a computed property instead:

var isUsingCustom: Bool {
    let defaultHex = defaultColor.toHex() ?? ""
    let currentHex = color.toHex() ?? ""
    return defaultHex.lowercased() != currentHex.lowercased()
}

This ensures the state is always accurate without needing to manage it separately.

Copilot uses AI. Check for mistakes.
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(Color.secondary.opacity(0.1))
.cornerRadius(8)
.onChange(of: color) { _, newValue in
// Check if color differs from default (with small tolerance for floating point)
let defaultHex = defaultColor.toHex() ?? ""
let newHex = newValue.toHex() ?? ""
isUsingCustom = defaultHex.lowercased() != newHex.lowercased()
}
.onAppear {
// Check initial state
let defaultHex = defaultColor.toHex() ?? ""
let currentHex = color.toHex() ?? ""
isUsingCustom = defaultHex.lowercased() != currentHex.lowercased()
}
}
}

2 changes: 1 addition & 1 deletion ora/Modules/Sidebar/DownloadsWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct DownloadsWidget: View {
downloadManager.isDownloadsPopoverOpen.toggle()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.down")
Image(systemName: "arrow.down.circle")
.foregroundColor(downloadButtonColor)
.frame(width: 12, height: 12)

Expand Down
12 changes: 10 additions & 2 deletions ora/OraCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ struct OraCommands: Commands {
NotificationCenter.default.post(name: .showLauncher, object: NSApp.keyWindow)
}.keyboardShortcut(KeyboardShortcuts.Tabs.new.keyboardShortcut)

Button("Search in Current Tab") {
NotificationCenter.default.post(
name: .showLauncher,
object: NSApp.keyWindow,
userInfo: ["searchInCurrentTab": true]
)
}.keyboardShortcut(KeyboardShortcuts.Address.searchInCurrentTab.keyboardShortcut)

Divider()

ImportDataButton()
Expand Down Expand Up @@ -183,13 +191,13 @@ struct OraCommands: Commands {

private func showAboutWindow() {
let alert = NSAlert()
alert.messageText = "Ora Browser"
alert.messageText = "Ora"
alert.informativeText = """
Version \(getAppVersion())

Fast, secure, and beautiful browser built for macOS.

© 2025 Ora Browser
© 2025 Ora
"""
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
Expand Down
13 changes: 12 additions & 1 deletion ora/OraRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,18 @@ struct OraRoot: View {
NotificationCenter.default.addObserver(forName: .showLauncher, object: nil, queue: .main) { note in
guard note.object as? NSWindow === window ?? NSApp.keyWindow else { return }
if tabManager.activeTab != nil {
appState.showLauncher.toggle()
// Check if we should search in current tab
if let searchInCurrentTab = note.userInfo?["searchInCurrentTab"] as? Bool {
appState.launcherSearchInCurrentTab = searchInCurrentTab
} else {
appState.launcherSearchInCurrentTab = false
}
// Only open if not already open, or close if already open
if appState.showLauncher {
appState.showLauncher = false
} else {
appState.showLauncher = true
}
Comment on lines +130 to +135
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toggle logic for showLauncher has changed in a subtle but potentially problematic way. Previously, it used .toggle() which would automatically toggle the value. Now it explicitly checks if appState.showLauncher and sets it to false, or else sets it to true.

However, when the "Search in Current Tab" command is triggered while the launcher is already open, this logic will:

  1. Set launcherSearchInCurrentTab = true
  2. Close the launcher (because showLauncher was true)

This means the user would have to invoke the command twice - once to close the existing launcher, and again to open it with the search-in-current-tab mode. This is confusing UX.

Consider either:

  • Always opening the launcher when searchInCurrentTab is true (don't close if already open, just update the mode)
  • Or ensuring the launcher properly responds to the mode change without needing to close/reopen
Suggested change
// Only open if not already open, or close if already open
if appState.showLauncher {
appState.showLauncher = false
} else {
appState.showLauncher = true
}
// Always open the launcher and update the mode
appState.showLauncher = true

Copilot uses AI. Check for mistakes.
}
}
NotificationCenter.default.addObserver(forName: .closeActiveTab, object: nil, queue: .main) { note in
Expand Down
1 change: 1 addition & 0 deletions ora/UI/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct HomeView: View {
.zIndex(3)
.frame(maxWidth: .infinity, alignment: sidebarManager.sidebarPosition == .primary ? .leading : .trailing)
.padding(6)
.padding(.top, 6)
.ignoresSafeArea(.all)

VStack(alignment: .center, spacing: 16) {
Expand Down
Loading