├── ora ├── UI │ ├── Toast │ │ ├── ToastModel.swift │ │ ├── ToastManager.swift │ │ └── ToastView.swift │ ├── CopiedURLOverlay.swift │ ├── EmptyPinnedTabs.swift │ ├── Buttons │ │ └── URLBarButton.swift │ ├── EmptyFavTabItem.swift │ ├── ShareLinkButton.swift │ ├── HomeView.swift │ ├── WindowControls.swift │ └── LinkPreview.swift ├── Assets.xcassets │ ├── Contents.json │ ├── OraIcon.appiconset │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-256.png │ │ ├── Icon-32 1.png │ │ ├── Icon-32.png │ │ ├── Icon-512.png │ │ ├── Icon-64.png │ │ ├── Icon-256 1.png │ │ ├── Icon-512 1.png │ │ ├── ora-white-macos-icon.png │ │ └── Contents.json │ ├── OraIconDev.appiconset │ │ ├── Icon.png │ │ ├── Icon-128.png │ │ ├── Icon-256.png │ │ ├── Icon-32.png │ │ ├── Icon-512.png │ │ ├── Icon-64.png │ │ ├── Icon-1024.png │ │ ├── Icon-256 1.png │ │ ├── Icon-32 1.png │ │ ├── Icon-512 1.png │ │ └── Contents.json │ ├── appearance-dark.imageset │ │ ├── dark.png │ │ └── Contents.json │ ├── appearance-light.imageset │ │ ├── light.png │ │ └── Contents.json │ ├── appearance-system.imageset │ │ ├── Ora Browser System.png │ │ └── Contents.json │ ├── ora-logo-plain.imageset │ │ ├── Contents.json │ │ └── Ora Browser Logo.svg │ ├── ora-logo-outline.imageset │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Capsule.xcassets │ ├── Contents.json │ ├── t3chat-capsule-logo.imageset │ │ ├── t3chat-capsule-logo.png │ │ ├── t3chat-capsule-logo 1.png │ │ └── Contents.json │ ├── perplexity-capsule-logo.imageset │ │ ├── perplexity-capsule-logo.png │ │ ├── perplexity-capsule-logo 1.png │ │ └── Contents.json │ ├── reddit-capsule-logo.imageset │ │ ├── Contents.json │ │ ├── reddit-capsule-logo.svg │ │ └── reddit-capsule-logo 1.svg │ ├── grok-capsule-logo.imageset │ │ ├── Contents.json │ │ ├── grok-black-capsule-logo.svg │ │ └── grok-white-capsule-logo.svg │ ├── openai-capsule-logo.imageset │ │ ├── Contents.json │ │ ├── opeai-black-capsule-logo.svg │ │ └── openai-white-capsule-logo 1.svg │ ├── grok-capsule-logo-inverted.imageset │ │ ├── Contents.json │ │ ├── grok-black-capsule-logo.svg │ │ └── grok-white-capsule-logo.svg │ └── openai-capsule-logo-inverted.imageset │ │ ├── Contents.json │ │ ├── opeai-black-capsule-logo.svg │ │ └── openai-white-capsule-logo.svg ├── WindowControls.xcassets │ ├── Contents.json │ ├── no-focus.imageset │ │ ├── Contents.json │ │ └── no-focus.svg │ ├── close-normal.imageset │ │ ├── Contents.json │ │ └── close-normal.svg │ ├── close-hover.imageset │ │ ├── Contents.json │ │ └── Close Hover Icon.svg │ ├── maximize-hover.imageset │ │ ├── Contents.json │ │ └── maximize-hover.svg │ ├── maximize-normal.imageset │ │ ├── Contents.json │ │ └── maximize-normal.svg │ ├── minimize-hover.imageset │ │ ├── Contents.json │ │ └── minimize-hover.svg │ └── minimize-normal.imageset │ │ ├── Contents.json │ │ └── minimize-normal.svg ├── Icons │ ├── OraIconDev.icon │ │ ├── Assets │ │ │ ├── Grid.png │ │ │ └── OraLogo.svg │ │ └── icon.json │ └── OraIcon.icon │ │ ├── Assets │ │ └── OraLogo.svg │ │ └── icon.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Services │ ├── Haptic.swift │ ├── ToolbarManager.swift │ ├── AppearanceManager.swift │ ├── CustomKeyboardShortcutManager.swift │ ├── DefaultBrowserManager.swift │ ├── SidebarManager.swift │ ├── SectionDropDelegate.swift │ ├── KeyModifierListener.swift │ ├── PrivacyService.swift │ └── TabDropDelegate.swift ├── Common │ ├── Extensions │ │ ├── EnvironmentValues+Window.swift │ │ ├── View+Modifiers.swift │ │ ├── ModelConfiguration+Shared.swift │ │ ├── View+Shortcuts.swift │ │ └── Color+Hex.swift │ ├── Representables │ │ ├── KeyCaptureView.swift │ │ ├── WindowReader.swift │ │ ├── BlurEffectView.swift │ │ └── WindowAccessor.swift │ ├── Utils │ │ ├── WindowFactory.swift │ │ ├── TabUtils.swift │ │ ├── Utils.swift │ │ └── ClipboardUtils.swift │ ├── Constants │ │ ├── ContainerConstants.swift │ │ └── AppEvents.swift │ └── Shapes │ │ └── ConditionallyConcentricRectangle.swift ├── Models │ ├── Folder.swift │ ├── History.swift │ ├── TabContainer.swift │ ├── SearchEngine.swift │ └── Download.swift ├── Modules │ ├── Settings │ │ ├── Sections │ │ │ ├── SettingsContainer.swift │ │ │ ├── PrivacySecuritySettingsView.swift │ │ │ └── AppearanceSelector.swift │ │ └── SettingsContentView.swift │ ├── Sidebar │ │ ├── FloatingSidebar.swift │ │ ├── TabList │ │ │ ├── NewTabButton.swift │ │ │ ├── PinnedTabsList.swift │ │ │ ├── FavTabsList.swift │ │ │ └── NormalTabsList.swift │ │ ├── BottomOption │ │ │ ├── EditContainerModal.swift │ │ │ ├── NewContainerButton.swift │ │ │ └── ContainerForm.swift │ │ └── DownloadsWidget.swift │ ├── SplitView │ │ ├── CursorModifier.swift │ │ ├── Splitter+Extensions.swift │ │ ├── SplitEnums.swift │ │ ├── SplitConstraints.swift │ │ └── SplitStyling.swift │ ├── Player │ │ └── PlayerIconButtonStyle.swift │ ├── Launcher │ │ ├── Suggestions │ │ │ └── LauncherSuggestionsView.swift │ │ └── Main │ │ │ ├── SearchCapsule.swift │ │ │ └── LauncherTextField.swift │ ├── Browser │ │ ├── BrowserContentContainer.swift │ │ ├── BrowserWebContentView.swift │ │ └── BrowserSplitView.swift │ └── URLBar │ │ └── FloatingURLBar.swift ├── ora.entitlements └── Info.plist ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── PULL_REQUEST_TEMPLATE │ ├── minimal_pr.md │ └── pull_request_template.md └── workflows │ ├── deploy-appcast.yml │ ├── build-and-test.yml │ ├── release.yml │ └── brew-release.yml ├── ora_public_key.pem ├── assets └── icon.png ├── .gitmodules ├── .githooks ├── pre-push └── pre-commit ├── scripts ├── xcbuild-debug.sh ├── check-security.sh ├── setup.sh ├── upload-dmg.sh └── setup-sparkle.sh ├── oraTests └── oraTests.swift ├── .swiftformat ├── oraUITests ├── oraUITestsLaunchTests.swift └── oraUITests.swift ├── appcast.xml ├── .cursor-rules.yaml ├── .swiftlint.yml ├── .gitignore ├── SECURITY.md ├── CODE_OF_CONDUCT.md └── ROADMAP.md /ora/UI/Toast/ToastModel.swift: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [the-ora] -------------------------------------------------------------------------------- /ora_public_key.pem: -------------------------------------------------------------------------------- 1 | Ozj+rezzbJAD76RfajtfQ7rFojJbpFSCl/0DcFSBCTI= 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/assets/icon.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "browser.wiki"] 2 | path = browser.wiki 3 | url = https://github.com/the-ora/browser.wiki.git 4 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ora/Icons/OraIconDev.icon/Assets/Grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Icons/OraIconDev.icon/Assets/Grid.png -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | 3 | * @yonaries @kenenisa -------------------------------------------------------------------------------- /ora/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-32 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-64.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-256 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Icon-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/Icon-512 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-128.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-256.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-32.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-512.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-64.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-dark.imageset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/appearance-dark.imageset/dark.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-256 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-32 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Icon-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIconDev.appiconset/Icon-512 1.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-light.imageset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/appearance-light.imageset/light.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/ora-white-macos-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/OraIcon.appiconset/ora-white-macos-icon.png -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-system.imageset/Ora Browser System.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Assets.xcassets/appearance-system.imageset/Ora Browser System.png -------------------------------------------------------------------------------- /ora/Capsule.xcassets/t3chat-capsule-logo.imageset/t3chat-capsule-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Capsule.xcassets/t3chat-capsule-logo.imageset/t3chat-capsule-logo.png -------------------------------------------------------------------------------- /ora/Capsule.xcassets/t3chat-capsule-logo.imageset/t3chat-capsule-logo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Capsule.xcassets/t3chat-capsule-logo.imageset/t3chat-capsule-logo 1.png -------------------------------------------------------------------------------- /ora/Capsule.xcassets/perplexity-capsule-logo.imageset/perplexity-capsule-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Capsule.xcassets/perplexity-capsule-logo.imageset/perplexity-capsule-logo.png -------------------------------------------------------------------------------- /ora/Capsule.xcassets/perplexity-capsule-logo.imageset/perplexity-capsule-logo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuilenren/browser1/main/ora/Capsule.xcassets/perplexity-capsule-logo.imageset/perplexity-capsule-logo 1.png -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building debug build..." 4 | ./scripts/xcbuild-debug.sh 5 | 6 | if [ $? -ne 0 ]; then 7 | echo "❌ Build failed." 8 | exit 1 9 | fi 10 | 11 | echo "✅ Build completed successfully." 12 | -------------------------------------------------------------------------------- /scripts/xcbuild-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o pipefail && xcodebuild build \ 3 | -scheme ora \ 4 | -destination "platform=macOS" \ 5 | -configuration Debug \ 6 | CODE_SIGN_IDENTITY="" \ 7 | CODE_SIGNING_REQUIRED=NO | xcbeautify 8 | -------------------------------------------------------------------------------- /ora/Services/Haptic.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | func performHapticFeedback(pattern: NSHapticFeedbackManager.FeedbackPattern) { 4 | let manager = NSHapticFeedbackManager.defaultPerformer 5 | manager.perform(pattern, performanceTime: .drawCompleted) 6 | } 7 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dark.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-light.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "light.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/no-focus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "no-focus.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/Services/ToolbarManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | class ToolbarManager: ObservableObject { 5 | @AppStorage("ui.toolbar.hidden") var isToolbarHidden: Bool = false 6 | @AppStorage("ui.toolbar.showfullurl") var showFullURL: Bool = true 7 | } 8 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/ora-logo-plain.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Ora Browser Logo.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/close-normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "close-normal.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/appearance-system.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Ora Browser System.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/ora-logo-outline.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Ora Browser Logo (1).svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/close-hover.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Close Hover Icon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/maximize-hover.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "maximize-hover.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/maximize-normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "maximize-normal.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/minimize-hover.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "minimize-hover.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/minimize-normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "minimize-normal.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | }, 11 | "properties" : { 12 | "localizable" : true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /oraTests/oraTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // oraTests.swift 3 | // oraTests 4 | // 5 | // Created by keni on 6/21/25. 6 | // 7 | 8 | @testable import ora 9 | import Testing 10 | 11 | struct OraTests { 12 | @Test func example() async throws { 13 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📖 Contribution Guide 4 | url: https://github.com/the-ora/browser/blob/main/CONTRIBUTING.md 5 | about: Read this before opening a new issue or PR. 6 | 7 | - name: 💬 Discussions 8 | url: https://discord.gg/9aZWH52Zjm 9 | about: Start a discussion, ask a question, or share ideas that don’t fit as an issue. -------------------------------------------------------------------------------- /ora/Common/Extensions/EnvironmentValues+Window.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | private struct WindowEnvironmentKey: EnvironmentKey { 5 | static let defaultValue: NSWindow? = nil 6 | } 7 | 8 | extension EnvironmentValues { 9 | var window: NSWindow? { 10 | get { self[WindowEnvironmentKey.self] } 11 | set { self[WindowEnvironmentKey.self] = newValue } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ora/Common/Extensions/View+Modifiers.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func gradientAnimatingBorder(color: Color, trigger: Bool) -> some View { 5 | modifier( 6 | GradientAnimatingBorder( 7 | color: color, 8 | trigger: trigger 9 | ) 10 | ) 11 | } 12 | 13 | func withTheme() -> some View { 14 | self.modifier(ThemeProvider()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/no-focus.imageset/no-focus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/close-normal.imageset/close-normal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/maximize-normal.imageset/maximize-normal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/minimize-normal.imageset/minimize-normal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ora/Common/Representables/KeyCaptureView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct KeyCaptureView: NSViewRepresentable { 4 | var onKeyDown: (NSEvent) -> Void 5 | 6 | func makeNSView(context: Context) -> NSView { 7 | let view = NSView() 8 | NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { event in 9 | onKeyDown(event) 10 | return event 11 | } 12 | return view 13 | } 14 | 15 | func updateNSView(_ nsView: NSView, context: Context) {} 16 | } 17 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/reddit-capsule-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "reddit-capsule-logo.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "reddit-capsule-logo 1.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/t3chat-capsule-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "t3chat-capsule-logo.png", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "t3chat-capsule-logo 1.png", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "grok-white-capsule-logo.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "grok-black-capsule-logo.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "openai-white-capsule-logo 1.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "opeai-black-capsule-logo.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/perplexity-capsule-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "perplexity-capsule-logo.png", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "perplexity-capsule-logo 1.png", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo-inverted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "grok-black-capsule-logo.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "grok-white-capsule-logo.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo-inverted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "opeai-black-capsule-logo.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "openai-white-capsule-logo.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.9 2 | --indent 4 3 | --maxwidth 120 4 | --allman false 5 | --semicolons never 6 | --stripunusedargs closure-only 7 | --wraparguments before-first 8 | --wrapcollections before-first 9 | --commas inline 10 | --closingparen balanced 11 | --self remove 12 | --header ignore 13 | --insertlines disabled 14 | --trimwhitespace always 15 | --ranges spaced 16 | --hexgrouping none 17 | --enable indent 18 | --enable trailingClosures 19 | --disable redundantSelf 20 | --disable sortedImports 21 | --disable consecutiveSpaces 22 | --disable redundantReturn 23 | --exclude Pods 24 | --exclude Carthage 25 | --exclude .build -------------------------------------------------------------------------------- /ora/Models/Folder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | // MARK: - Folder 5 | 6 | @Model 7 | class Folder: ObservableObject, Identifiable { 8 | var id: UUID 9 | var name: String 10 | var isOpened: Bool 11 | 12 | @Relationship(inverse: \TabContainer.folders) var container: TabContainer 13 | init( 14 | id: UUID = UUID(), 15 | name: String, 16 | isOpened: Bool = false, 17 | container: TabContainer 18 | ) { 19 | self.id = UUID() 20 | self.name = name 21 | self.isOpened = isOpened 22 | self.container = container 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ora/Common/Representables/WindowReader.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct WindowReader: NSViewRepresentable { 5 | @Binding var window: NSWindow? 6 | 7 | func makeNSView(context: Context) -> NSView { 8 | let view = NSView() 9 | DispatchQueue.main.async { [weak view] in 10 | if let win = view?.window { self.window = win } 11 | } 12 | return view 13 | } 14 | 15 | func updateNSView(_ nsView: NSView, context: Context) { 16 | DispatchQueue.main.async { [weak nsView] in 17 | if let win = nsView?.window { self.window = win } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ora/UI/CopiedURLOverlay.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CopiedURLOverlay: View { 4 | let foregroundColor: Color 5 | @Binding var showCopiedAnimation: Bool 6 | @Binding var startWheelAnimation: Bool 7 | 8 | var body: some View { 9 | HStack { 10 | Image(systemName: "link") 11 | Text("Copied Current URL") 12 | } 13 | .font(.system(size: 14)) 14 | .foregroundColor(foregroundColor) 15 | .opacity(showCopiedAnimation ? 1 : 0) 16 | .offset(y: showCopiedAnimation ? 0 : (startWheelAnimation ? -12 : 12)) 17 | .animation(.easeOut(duration: 0.3), value: showCopiedAnimation) 18 | .animation(.easeOut(duration: 0.3), value: startWheelAnimation) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ora/Common/Representables/BlurEffectView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BlurEffectView: NSViewRepresentable { 4 | let material: NSVisualEffectView.Material 5 | let blendingMode: NSVisualEffectView.BlendingMode 6 | 7 | func makeNSView(context: Context) -> NSVisualEffectView { 8 | let visualEffectView = NSVisualEffectView() 9 | visualEffectView.material = material 10 | visualEffectView.blendingMode = blendingMode 11 | visualEffectView.state = .active 12 | return visualEffectView 13 | } 14 | 15 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) { 16 | nsView.material = material 17 | nsView.blendingMode = blendingMode 18 | nsView.state = .active 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🎨 Running SwiftFormat..." 4 | if ! command -v swiftformat >/dev/null 2>&1; then 5 | echo "⚠️ SwiftFormat not installed. Install with 'brew install swiftformat' or execute './setup.sh.'" 6 | exit 1 7 | fi 8 | swiftformat . --quiet 9 | if [ $? -ne 0 ]; then 10 | echo "❌ SwiftFormat failed." 11 | exit 1 12 | fi 13 | 14 | echo "🔍 Running SwiftLint..." 15 | if ! command -v swiftlint >/dev/null 2>&1; then 16 | echo "⚠️ SwiftLint not installed. Install with 'brew install swiftlint' or execute './setup.sh.'" 17 | exit 1 18 | fi 19 | swiftlint --quiet 20 | swiftlint lint --fix --strict 21 | if [ $? -ne 0 ]; then 22 | echo "❌ SwiftLint failed." 23 | exit 1 24 | fi 25 | 26 | git add . 27 | 28 | echo "✅ Pre-commit checks passed!" 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/minimal_pr.md: -------------------------------------------------------------------------------- 1 | ### Minimal Pull Request – Ora Browser 2 | 3 | For small fixes, dependency updates, or minor changes that do not require a full PR description. 4 | 5 | --- 6 | 7 | ### Type of Change 8 | 9 | - [ ] Chore / maintenance 10 | - [ ] Bug fix (small) 11 | - [ ] Documentation / comments 12 | 13 | --- 14 | 15 | ### Quick Testing 16 | 17 | Optional: if no testing is needed, leave blank. 18 | 19 | --- 20 | 21 | ### AI Assistance Disclosure 22 | 23 | If AI was used for any part of this PR, please disclose it here: 24 | 25 | > Example: "Used Claude Code for writing ." 26 | 27 | --- 28 | 29 | ### Checklist 30 | 31 | - [ ] Code builds successfully 32 | - [ ] No new warnings or errors 33 | - [ ] Relevant dependencies updated (if applicable) 34 | 35 | --- 36 | 37 | ### Related Issues 38 | 39 | Closes # -------------------------------------------------------------------------------- /ora/Modules/Settings/Sections/SettingsContainer.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsContainer: View { 4 | let maxContentWidth: CGFloat 5 | var usesScrollView: Bool = true 6 | @ViewBuilder var content: () -> Content 7 | 8 | var body: some View { 9 | ZStack { 10 | if usesScrollView { 11 | ScrollView { inner } 12 | } else { 13 | inner 14 | } 15 | } 16 | } 17 | 18 | @ViewBuilder 19 | private var inner: some View { 20 | HStack(alignment: .top) { 21 | Spacer(minLength: 0) 22 | VStack(alignment: .leading, spacing: 24) { 23 | content() 24 | } 25 | .frame(maxWidth: maxContentWidth, alignment: .leading) 26 | Spacer(minLength: 0) 27 | } 28 | .padding(.horizontal, 20) 29 | .padding(.vertical, 16) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ora/UI/Toast/ToastManager.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | final class ToastManager: ObservableObject { 4 | @Published var toasts: [Toast] = [] 5 | 6 | @discardableResult 7 | func show(message: String, systemImage: String? = "checkmark.circle.fill") -> String { 8 | let toast = Toast { id in 9 | ToastView( 10 | id: id, message: message, systemImage: systemImage, 11 | action: { [weak self] in self?.dismiss(id: id) } 12 | ) 13 | } 14 | 15 | withAnimation(.bouncy) { 16 | toasts.append(toast) 17 | } 18 | 19 | return toast.id 20 | } 21 | 22 | func dismiss(id: String) { 23 | withAnimation(.bouncy) { 24 | toasts.removeAll(where: { $0.id == id }) 25 | } 26 | } 27 | 28 | func dismissAll() { 29 | withAnimation(.bouncy) { 30 | toasts.removeAll() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ora/Common/Utils/WindowFactory.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | enum WindowFactory { 5 | static func makeMainWindow(rootView: some View, size: CGSize = CGSize(width: 1440, height: 900)) -> NSWindow { 6 | let window = NSWindow( 7 | contentRect: NSRect(x: 0, y: 0, width: size.width, height: size.height), 8 | styleMask: [.titled, .closable, .miniaturizable, .resizable], 9 | backing: .buffered, 10 | defer: false 11 | ) 12 | window.titleVisibility = .hidden 13 | window.titlebarAppearsTransparent = true 14 | window.isReleasedWhenClosed = false 15 | 16 | let hostingController = NSHostingController(rootView: rootView) 17 | window.contentViewController = hostingController 18 | window.center() 19 | window.makeKeyAndOrderFront(nil) 20 | NSApp.activate(ignoringOtherApps: true) 21 | return window 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/FloatingSidebar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct FloatingSidebar: View { 4 | @Environment(\.theme) var theme 5 | 6 | let sidebarCornerRadius: CGFloat = { 7 | if #available(macOS 26, *) { 8 | return 13 9 | } else { 10 | return 6 11 | } 12 | }() 13 | 14 | var body: some View { 15 | let clipShape = ConditionallyConcentricRectangle(cornerRadius: sidebarCornerRadius) 16 | 17 | ZStack(alignment: .leading) { 18 | SidebarView() 19 | .background(theme.subtleWindowBackgroundColor) 20 | .background(BlurEffectView(material: .popover, blendingMode: .withinWindow)) 21 | .clipShape(clipShape) 22 | .overlay(clipShape 23 | .stroke(theme.invertedSolidWindowBackgroundColor.opacity(0.3), lineWidth: 1) 24 | ) 25 | } 26 | .padding(6) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/maximize-hover.imageset/maximize-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /oraUITests/oraUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // oraUITestsLaunchTests.swift 3 | // oraUITests 4 | // 5 | // Created by keni on 6/21/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class OraUITestsLaunchTests: XCTestCase { 11 | override static var runsForEachTargetApplicationUIConfiguration: Bool { 12 | true 13 | } 14 | 15 | override func setUpWithError() throws { 16 | continueAfterFailure = false 17 | } 18 | 19 | @MainActor 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ora/ora.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-only 8 | 9 | com.apple.security.assets.music.read-only 10 | 11 | com.apple.security.assets.pictures.read-only 12 | 13 | com.apple.security.cs.allow-jit 14 | 15 | com.apple.security.device.audio-input 16 | 17 | com.apple.security.device.camera 18 | 19 | com.apple.security.files.downloads.read-write 20 | 21 | com.apple.security.files.user-selected.read-write 22 | 23 | com.apple.security.network.client 24 | 25 | com.apple.security.print 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ora/Services/AppearanceManager.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum AppAppearance: String, CaseIterable, Identifiable { 4 | case system = "System" 5 | case light = "Light" 6 | case dark = "Dark" 7 | 8 | var id: String { rawValue } 9 | } 10 | 11 | class AppearanceManager: ObservableObject { 12 | static let shared = AppearanceManager() 13 | @AppStorage("ui.app.appearance") var appearance: AppAppearance = .system { 14 | didSet { 15 | updateAppearance() 16 | } 17 | } 18 | 19 | func updateAppearance() { 20 | guard NSApp != nil else { 21 | print("NSApp is nil, skipping appearance update") 22 | return 23 | } 24 | switch appearance { 25 | case .system: 26 | NSApp.appearance = nil 27 | case .light: 28 | NSApp.appearance = NSAppearance(named: .aqua) 29 | case .dark: 30 | NSApp.appearance = NSAppearance(named: .darkAqua) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/TabList/NewTabButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct NewTabButton: View { 4 | let addNewTab: () -> Void 5 | 6 | @State private var isHovering = false 7 | @Environment(\.theme) private var theme 8 | 9 | var body: some View { 10 | Button(action: addNewTab) { 11 | HStack(spacing: 8) { 12 | Image(systemName: "plus") 13 | .frame(width: 12, height: 12) 14 | 15 | Text("New Tab") 16 | .font(.system(size: 13, weight: .medium)) 17 | } 18 | .foregroundColor(.secondary) 19 | .padding(8) 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | .background(isHovering ? theme.activeTabBackground.opacity(0.3) : .clear, in: .rect(cornerRadius: 10)) 22 | .contentShape(ConditionallyConcentricRectangle(cornerRadius: 10)) 23 | .geometryGroup() 24 | } 25 | .buttonStyle(.plain) 26 | .onHover { isHovering = $0 } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy-appcast.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Appcast 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to deploy' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | deploy-appcast: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Download appcast from release 20 | run: | 21 | # Download the appcast.xml from the latest release 22 | curl -L -o appcast.xml "https://raw.githubusercontent.com/${{ github.repository }}/release/appcast.xml" 23 | 24 | - name: Deploy to GitHub Pages 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: . 29 | publish_branch: gh-pages 30 | keep_files: true 31 | exclude_assets: 'build/**,*.sh,*.yml,*.md,*.json,*.xcodeproj/**,ora/**,docs/**,SECURITY.md,check-security.sh,.github/**,.gitignore,README.md,LICENSE.md' 32 | destination_dir: . -------------------------------------------------------------------------------- /ora/Common/Extensions/ModelConfiguration+Shared.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | extension ModelConfiguration { 5 | /// Shared model configuration for the main Ora database 6 | static func oraDatabase(isPrivate: Bool = false) -> ModelConfiguration { 7 | if isPrivate { 8 | return ModelConfiguration(isStoredInMemoryOnly: true) 9 | } else { 10 | return ModelConfiguration( 11 | "OraData", 12 | schema: Schema([TabContainer.self, History.self, Download.self]), 13 | url: URL.applicationSupportDirectory.appending(path: "Ora/OraData.sqlite") 14 | ) 15 | } 16 | } 17 | 18 | /// Creates a ModelContainer using the standard Ora database configuration 19 | static func createOraContainer(isPrivate: Bool = false) throws -> ModelContainer { 20 | return try ModelContainer( 21 | for: TabContainer.self, History.self, Download.self, 22 | configurations: oraDatabase(isPrivate: isPrivate) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/minimize-hover.imageset/minimize-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ora/Common/Utils/TabUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum TabSection { 4 | case fav 5 | case pinned 6 | case normal 7 | } 8 | 9 | // MARK: - Tab Utility Functions 10 | 11 | /// Determines if two tabs are in the same section 12 | func isInSameSection(from: Tab, to: Tab) -> Bool { 13 | return section(for: from) == section(for: to) 14 | } 15 | 16 | /// Gets the section for a given tab based on its type 17 | func section(for tab: Tab) -> TabSection { 18 | switch tab.type { 19 | case .fav: return .fav 20 | case .pinned: return .pinned 21 | case .normal: return .normal 22 | } 23 | } 24 | 25 | /// Converts a TabSection to corresponding TabType 26 | func tabType(for section: TabSection) -> TabType { 27 | switch section { 28 | case .fav: return .fav 29 | case .pinned: return .pinned 30 | case .normal: return .normal 31 | } 32 | } 33 | 34 | /// Moves a tab between different sections 35 | func moveTabBetweenSections(from: Tab, to: Tab) { 36 | from.switchSections(from: from, to: to) 37 | from.container.reorderTabs(from: from, to: to) 38 | } 39 | -------------------------------------------------------------------------------- /ora/UI/EmptyPinnedTabs.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EmptyPinnedTabs: View { 4 | @Environment(\.theme) var theme 5 | @State private var isTargeted = false 6 | 7 | var body: some View { 8 | HStack(spacing: 8) { 9 | Image(systemName: "pin") 10 | .font(.system(size: 12)) 11 | .foregroundColor(theme.mutedForeground) 12 | 13 | Text("Drop here to pin a tab") 14 | .font(.system(size: 12, weight: .medium)) 15 | .foregroundColor(theme.mutedForeground) 16 | } 17 | .frame(maxWidth: .infinity) 18 | .padding(8) 19 | .background(theme.invertedSolidWindowBackgroundColor.opacity(0.07)) 20 | .cornerRadius(10) 21 | .overlay( 22 | RoundedRectangle(cornerRadius: 10, style: .continuous) 23 | .stroke( 24 | theme.invertedSolidWindowBackgroundColor.opacity(0.25), 25 | style: StrokeStyle(lineWidth: 1, dash: [5, 5]) 26 | ) 27 | ) 28 | .onHover { isTargeted = $0 } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ora/UI/Buttons/URLBarButton.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct URLBarButton: View { 5 | let systemName: String 6 | let isEnabled: Bool 7 | let foregroundColor: Color 8 | let action: () -> Void 9 | @State private var isHovering = false 10 | 11 | var body: some View { 12 | Button(action: action) { 13 | Image(systemName: systemName) 14 | .font(.system(size: 14, weight: .medium)) 15 | .foregroundColor(isEnabled ? (isHovering ? foregroundColor.opacity(0.8) : foregroundColor) : 16 | foregroundColor.opacity(0.5) 17 | ) 18 | .frame(width: 30, height: 30) 19 | .background( 20 | ConditionallyConcentricRectangle(cornerRadius: 6) 21 | .fill(isHovering && isEnabled ? foregroundColor.opacity(0.2) : Color.clear) 22 | ) 23 | } 24 | .buttonStyle(PlainButtonStyle()) 25 | .disabled(!isEnabled) 26 | .onHover { hovering in 27 | isHovering = hovering 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ora/Common/Constants/ContainerConstants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Constants related to container functionality 4 | enum ContainerConstants { 5 | /// Default emoji used when no emoji is selected for a container 6 | static let defaultEmoji = "•" 7 | 8 | /// Default time in seconds after which a tab is no longer considered alive 9 | static let defaultTabAliveTimeout: TimeInterval = 60 * 60 // 1 hour 10 | 11 | /// Default time in seconds after which normal tabs are completely removed 12 | static let defaultTabRemovalTimeout: TimeInterval = 24 * 60 * 60 // 1 day 13 | 14 | /// UI constants for container forms and displays 15 | enum UI { 16 | static let normalButtonWidth: CGFloat = 28 17 | static let compactButtonWidth: CGFloat = 12 18 | static let popoverWidth: CGFloat = 300 19 | static let emojiButtonSize: CGFloat = 32 20 | static let cornerRadius: CGFloat = 10 21 | } 22 | 23 | /// Animation constants for container interactions 24 | enum Animation { 25 | static let hoverDuration: Double = 0.15 26 | static let emojiPickerDuration: Double = 0.1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/check-security.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🔒 Checking Ora Browser Security..." 4 | 5 | # Check if private key exists 6 | if [ -f "build/dsa_priv.pem" ]; then 7 | echo "✅ DSA private key found in build/dsa_priv.pem" 8 | 9 | # Check if it's in git (it shouldn't be) 10 | if git ls-files | grep -q "dsa_priv.pem"; then 11 | echo "❌ SECURITY ISSUE: Private key is tracked by git!" 12 | echo " Run: git rm --cached build/dsa_priv.pem" 13 | exit 1 14 | else 15 | echo "✅ Private key is not tracked by git" 16 | fi 17 | else 18 | echo "⚠️ DSA private key not found - will be generated on next release" 19 | fi 20 | 21 | # Check if public key exists 22 | if [ -f "build/dsa_pub.pem" ]; then 23 | echo "✅ DSA public key found in build/dsa_pub.pem" 24 | else 25 | echo "⚠️ DSA public key not found" 26 | fi 27 | 28 | # Check .gitignore 29 | if grep -q "dsa_priv.pem" .gitignore; then 30 | echo "✅ Private key is properly ignored in .gitignore" 31 | else 32 | echo "❌ SECURITY ISSUE: Private key not in .gitignore!" 33 | exit 1 34 | fi 35 | 36 | echo "" 37 | echo "🔐 Security check complete!" 38 | echo "Remember: Never commit build/dsa_priv.pem to version control!" -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo.imageset/grok-black-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo.imageset/grok-white-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo-inverted.imageset/grok-black-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/grok-capsule-logo-inverted.imageset/grok-white-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ora/Common/Utils/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func extractDomainOrIP(from text: String) -> String? { 4 | guard let url = URL(string: text.hasPrefix("http") ? text : "https://\(text)") else { 5 | return nil 6 | } 7 | 8 | guard let host = url.host else { 9 | return nil 10 | } 11 | 12 | return host 13 | } 14 | 15 | func isValidURL(_ text: String) -> Bool { 16 | guard let host = extractDomainOrIP(from: text) else { return false } 17 | 18 | let ipPattern = #"^(\d{1,3}\.){3}\d{1,3}$"# 19 | if host.range(of: ipPattern, options: .regularExpression) != nil { 20 | return true 21 | } 22 | 23 | let domainPattern = 24 | #"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)+$"# 25 | 26 | return host.range(of: domainPattern, options: .regularExpression) != nil 27 | } 28 | 29 | func constructURL(from text: String) -> URL? { 30 | let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) 31 | if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { 32 | return URL(string: trimmed) 33 | } 34 | if isValidURL(trimmed) { 35 | return URL(string: "https://\(trimmed)") 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /ora/Modules/SplitView/CursorModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func cursor(_ cursor: NSCursor) -> some View { 5 | modifier(CursorModifier(cursor: cursor)) 6 | } 7 | } 8 | 9 | struct CursorModifier: ViewModifier { 10 | let cursor: NSCursor 11 | 12 | func body(content: Content) -> some View { 13 | content.overlay( 14 | GeometryReader { proxy in 15 | Representable( 16 | cursor: cursor, 17 | frame: proxy.frame(in: .global) 18 | ) 19 | } 20 | ) 21 | } 22 | 23 | private class CustomCursorView: NSView { 24 | var cursor: NSCursor! 25 | override func resetCursorRects() { 26 | addCursorRect(bounds, cursor: cursor) 27 | } 28 | } 29 | 30 | private struct Representable: NSViewRepresentable { 31 | let cursor: NSCursor 32 | let frame: NSRect 33 | 34 | func makeNSView(context: Context) -> NSView { 35 | let cursorView = CustomCursorView(frame: frame) 36 | cursorView.cursor = cursor 37 | return cursorView 38 | } 39 | 40 | func updateNSView(_ nsView: NSView, context: Context) {} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/ora-logo-plain.imageset/Ora Browser Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/UI/EmptyFavTabItem.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EmptyFavTabItem: View { 4 | @Environment(\.theme) var theme 5 | @State private var isTargeted = false 6 | 7 | let cornerRadius: CGFloat = 8 8 | 9 | var body: some View { 10 | VStack(spacing: 8) { 11 | Image(systemName: "star") 12 | .font(.system(size: 16)) 13 | .foregroundColor(theme.mutedForeground) 14 | 15 | Text("Drag a tab here to \n add it to your favorites") 16 | .font(.system(size: 12, weight: .medium)) 17 | .foregroundColor(theme.mutedForeground) 18 | .multilineTextAlignment(.center) 19 | } 20 | .frame(maxWidth: .infinity, alignment: .center) 21 | .frame(height: 96) 22 | .background(theme.invertedSolidWindowBackgroundColor.opacity(0.07)) 23 | .cornerRadius(cornerRadius) 24 | .overlay( 25 | ConditionallyConcentricRectangle(cornerRadius: cornerRadius) 26 | .stroke( 27 | theme.invertedSolidWindowBackgroundColor.opacity(0.25), 28 | style: StrokeStyle(lineWidth: 1, dash: [5, 5]) 29 | ) 30 | ) 31 | .onHover { isTargeted = $0 } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ora/Models/History.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | // SwiftData model for a browsing history entry 5 | @Model 6 | final class History { 7 | @Attribute(.unique) var id: UUID // Unique identifier 8 | var url: URL 9 | var urlString: String 10 | var title: String 11 | var faviconURL: URL 12 | var faviconLocalFile: URL? 13 | var createdAt: Date 14 | var visitCount: Int 15 | var lastAccessedAt: Date 16 | 17 | @Relationship(inverse: \TabContainer.history) var container: TabContainer? 18 | 19 | init( 20 | id: UUID = UUID(), 21 | url: URL, 22 | title: String, 23 | faviconURL: URL, 24 | faviconLocalFile: URL? = nil, 25 | createdAt: Date, 26 | lastAccessedAt: Date, 27 | visitCount: Int, 28 | container: TabContainer? = nil 29 | ) { 30 | let now = Date() 31 | self.id = id 32 | self.url = url 33 | self.urlString = url.absoluteString 34 | self.title = title 35 | self.faviconURL = faviconURL 36 | self.createdAt = now 37 | self.lastAccessedAt = now 38 | self.visitCount = visitCount 39 | self.faviconLocalFile = faviconLocalFile 40 | self.container = container 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ora/Icons/OraIcon.icon/Assets/OraLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Icons/OraIconDev.icon/Assets/OraLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Modules/Player/PlayerIconButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PlayerIconButtonStyle: ButtonStyle { 4 | let isEnabled: Bool 5 | @State private var isHovering = false 6 | 7 | func makeBody(configuration: Configuration) -> some View { 8 | configuration.label 9 | .foregroundStyle(isEnabled ? Color.white.opacity(isHovering || configuration.isPressed ? 0.95 : 0.82) 10 | : Color.white.opacity(0.35) 11 | ) 12 | .padding(.vertical, 4) 13 | .padding(.horizontal, 6) 14 | .background( 15 | RoundedRectangle(cornerRadius: 8, style: .continuous) 16 | .fill( 17 | isEnabled 18 | ? (configuration.isPressed ? Color.white.opacity(0.18) 19 | : (isHovering ? Color.white.opacity(0.10) : Color.clear) 20 | ) 21 | : Color.clear 22 | ) 23 | ) 24 | .scaleEffect(configuration.isPressed && isEnabled ? 0.97 : 1.0) 25 | .animation(.easeOut(duration: 0.15), value: isHovering || configuration.isPressed) 26 | .onHover { hovering in 27 | if isEnabled { isHovering = hovering } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ora Browser Changelog 5 | Most recent changes with links to updates. 6 | en 7 | 8 | Version 0.2.9 9 | Ora Browser v0.2.9 11 | Changes since last release: 12 | 13 | Other 14 | 15 | Set Favicons on MainThread (#162) — Aarav Gupta 16 | Roadmap (#164) — Yonathan Dejene 17 | 18 | 19 | ]]> 20 | Wed, 29 Oct 2025 16:25:16 +0000 21 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ora/Common/Shapes/ConditionallyConcentricRectangle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Uses ConcentricRectangle on 26.0+, falls back to RoundedRectangle otherwise. 4 | struct ConditionallyConcentricRectangle: Shape { 5 | var cornerRadius: CGFloat 6 | var style: RoundedCornerStyle = .continuous 7 | 8 | // This is equivilent to building with Xcode 26.0+ 9 | #if compiler(>=6.2) 10 | func path(in rect: CGRect) -> Path { 11 | if #available(macOS 26.0, *) { 12 | return ConcentricRectangle( 13 | corners: .concentric( 14 | minimum: .fixed( 15 | cornerRadius 16 | ) 17 | ), 18 | isUniform: true 19 | ) 20 | .path(in: rect) 21 | } else { 22 | return RoundedRectangle( 23 | cornerRadius: cornerRadius, 24 | style: style 25 | ) 26 | .path(in: rect) 27 | } 28 | } 29 | #else 30 | func path(in rect: CGRect) -> Path { 31 | return RoundedRectangle( 32 | cornerRadius: cornerRadius, 33 | style: style 34 | ) 35 | .path(in: rect) 36 | } 37 | #endif 38 | } 39 | -------------------------------------------------------------------------------- /ora/Modules/SplitView/Splitter+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Splitter+Extensions.swift 3 | // SplitView 4 | // 5 | // This file is a place to hold generally useful extensions of Splitter. If you create one, 6 | // please add your name below to identify your extension, add it to this file, and submit 7 | // a pull request. Note that your custom Splitter should probably conform to SplitDivider. 8 | // Your custom splitter can get its layout from the LayoutHolder in the Environment or 9 | // directly as part of its initialization. 10 | // 11 | // Created by Steven Harris on 2/16/23. 12 | // 13 | // Extension authors: 14 | // 15 | // Steven G. Harris created `line` and `invisible` extensions. 16 | // 17 | 18 | import SwiftUI 19 | 20 | public extension Splitter { 21 | /// A Splitter (that responds to changes in layout) that is a line across the full breadth of the view, by default 22 | /// gray and visibleThickness of 1 23 | static func line(color: Color? = nil, visibleThickness: CGFloat? = nil) -> Splitter { 24 | return Splitter(color: color, inset: 0, visibleThickness: visibleThickness ?? 1) 25 | } 26 | 27 | /// An invisible Splitter (that responds to changes in layout) that is a line across the full breadth of the view 28 | static func invisible() -> Splitter { 29 | Splitter.line(visibleThickness: 0) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ora/Services/CustomKeyboardShortcutManager.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | class CustomKeyboardShortcutManager: ObservableObject { 5 | static let shared = CustomKeyboardShortcutManager() 6 | 7 | @Published private(set) var customShortcuts: [String: KeyChord] = [:] 8 | 9 | private let settingsStore = SettingsStore.shared 10 | 11 | private init() { 12 | loadCustomShortcuts() 13 | } 14 | 15 | private func loadCustomShortcuts() { 16 | customShortcuts = settingsStore.customKeyboardShortcuts 17 | } 18 | 19 | func setCustomShortcut(for shortcut: KeyboardShortcutDefinition, event: NSEvent) { 20 | if let keyChord = KeyChord(fromEvent: event) { 21 | customShortcuts[shortcut.id] = keyChord 22 | settingsStore.setCustomKeyboardShortcut(id: shortcut.id, keyChord: keyChord) 23 | } 24 | } 25 | 26 | func removeCustomShortcut(for shortcut: KeyboardShortcutDefinition) { 27 | customShortcuts.removeValue(forKey: shortcut.id) 28 | settingsStore.removeCustomKeyboardShortcut(id: shortcut.id) 29 | } 30 | 31 | func getShortcut(id: String) -> KeyChord? { 32 | return customShortcuts[id] 33 | } 34 | 35 | func hasCustomShortcut(for shortcut: KeyboardShortcutDefinition) -> Bool { 36 | return customShortcuts[shortcut.id] != nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ora/Modules/Launcher/Suggestions/LauncherSuggestionsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum SuggestionFocus: Hashable { 4 | case suggestion(id: UUID) 5 | } 6 | 7 | struct LauncherSuggestionsView: View { 8 | @Environment(\.theme) private var theme 9 | @Binding var text: String 10 | @StateObject private var searchEngineService = SearchEngineService() 11 | @Binding var suggestions: [LauncherSuggestion] 12 | @Binding var focusedElement: UUID 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 8) { 16 | ForEach(suggestions) { suggestion in 17 | LauncherSuggestionItem( 18 | suggestion: suggestion, 19 | defaultAI: searchEngineService.getDefaultAIChat(), 20 | focusedElement: $focusedElement 21 | ) 22 | } 23 | } 24 | .frame(maxWidth: .infinity) 25 | .padding(.top, 4) 26 | .overlay( 27 | Rectangle() 28 | .frame(height: 1) 29 | .foregroundColor(theme.border.opacity(0.5)), 30 | alignment: .top 31 | ) 32 | .onAppear { 33 | searchEngineService.setTheme(theme) 34 | } 35 | // .onChange(of: theme) { _, newValue in 36 | // searchEngineService.setTheme(newValue) 37 | // } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ora/Modules/Launcher/Main/SearchCapsule.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct SearchEngineCapsule: View { 5 | let text: String 6 | let color: Color 7 | let foregroundColor: Color 8 | let icon: String 9 | let favicon: NSImage? 10 | let faviconBackgroundColor: Color? 11 | 12 | var body: some View { 13 | HStack(alignment: .center, spacing: 8) { 14 | if let favicon { 15 | Image(nsImage: favicon) 16 | .resizable() 17 | .frame(width: 16, height: 16) 18 | } else if icon.isEmpty { 19 | Image(systemName: "magnifyingglass") 20 | .resizable() 21 | .frame(width: 16, height: 16) 22 | .foregroundStyle(foregroundColor) 23 | } else { 24 | Image(icon) 25 | .resizable() 26 | .frame(width: 16, height: 16) 27 | .foregroundStyle(foregroundColor) 28 | } 29 | Text(text) 30 | .font(.callout) 31 | .bold() 32 | .foregroundStyle(foregroundColor) 33 | } 34 | .padding(.vertical, 6) 35 | .padding(.horizontal, 12) 36 | .frame(alignment: .leading) 37 | .background(faviconBackgroundColor ?? color) 38 | .cornerRadius(99) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | REPO_DIR="$(cd "$(dirname "$0")" && pwd)" 6 | cd "$REPO_DIR" 7 | 8 | echo "🔍 Checking dependencies..." 9 | 10 | ensure_formula() { 11 | local cmd_name="$1" 12 | local formula_name="$2" 13 | 14 | if command -v "$cmd_name" >/dev/null 2>&1; then 15 | echo "✅ $cmd_name already installed" 16 | return 0 17 | fi 18 | 19 | echo "⬇️ Installing $cmd_name..." 20 | 21 | if ! command -v brew >/dev/null 2>&1; then 22 | echo "❌ Homebrew is not installed. Please install Homebrew first to proceed: https://brew.sh" 23 | exit 1 24 | fi 25 | 26 | # Install the formula if it's not already present 27 | if ! brew list --formula "$formula_name" >/dev/null 2>&1; then 28 | brew install "$formula_name" 29 | fi 30 | 31 | if command -v "$cmd_name" >/dev/null 2>&1; then 32 | echo "✅ $cmd_name installed" 33 | else 34 | echo "❌ Failed to install $cmd_name" 35 | exit 1 36 | fi 37 | } 38 | 39 | ensure_formula Xcodegen xcodegen 40 | ensure_formula Swiftlint swiftlint 41 | ensure_formula Swiftformat swiftformat 42 | ensure_formula Xcbeautify xcbeautify 43 | 44 | git config core.hooksPath .githooks 45 | if [ -d .githooks ]; then 46 | chmod -R +x .githooks || true 47 | fi 48 | echo "✅ Git hooks installed!" 49 | 50 | cd .. 51 | xcodegen 52 | echo "✅ Xcodegen generated successfully!" 53 | 54 | echo "🎉 Setup complete." 55 | -------------------------------------------------------------------------------- /ora/UI/ShareLinkButton.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct ShareLinkButton: View { 5 | let isEnabled: Bool 6 | let foregroundColor: Color 7 | let onShare: (NSView, NSRect) -> Void 8 | 9 | @State private var shareSourceView: NSView? 10 | 11 | var body: some View { 12 | URLBarButton( 13 | systemName: "square.and.arrow.up", 14 | isEnabled: isEnabled, 15 | foregroundColor: foregroundColor, 16 | action: { 17 | if let sourceView = shareSourceView { 18 | let rect = sourceView.bounds 19 | onShare(sourceView, rect) 20 | } 21 | } 22 | ) 23 | .background( 24 | ShareSourceView { nsView in 25 | shareSourceView = nsView 26 | } 27 | ) 28 | } 29 | } 30 | 31 | private struct ShareSourceView: NSViewRepresentable { 32 | let onViewCreated: (NSView) -> Void 33 | 34 | func makeNSView(context: Context) -> NSView { 35 | let view = NSView() 36 | view.wantsLayer = true 37 | view.layer?.backgroundColor = NSColor.clear.cgColor 38 | DispatchQueue.main.async { 39 | onViewCreated(view) 40 | } 41 | return view 42 | } 43 | 44 | func updateNSView(_ nsView: NSView, context: Context) { 45 | // Nothing to update 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ora/Common/Utils/ClipboardUtils.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | /// Utility functions for clipboard operations 5 | enum ClipboardUtils { 6 | /// Copies the given text to the system clipboard 7 | static func copyToClipboard(_ text: String) { 8 | let pasteboard = NSPasteboard.general 9 | pasteboard.clearContents() 10 | pasteboard.setString(text, forType: .string) 11 | } 12 | 13 | /// Triggers copy with animation states 14 | /// - Parameters: 15 | /// - text: The text to copy 16 | /// - showCopiedAnimation: Binding to control animation visibility 17 | /// - startWheelAnimation: Binding to control wheel animation 18 | static func triggerCopy( 19 | _ text: String, 20 | showCopiedAnimation: Binding, 21 | startWheelAnimation: Binding 22 | ) { 23 | // Prevent double-trigger if both Command and view shortcut fire 24 | if showCopiedAnimation.wrappedValue { return } 25 | copyToClipboard(text) 26 | withAnimation { 27 | showCopiedAnimation.wrappedValue = true 28 | startWheelAnimation.wrappedValue = true 29 | } 30 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 31 | withAnimation { 32 | showCopiedAnimation.wrappedValue = false 33 | startWheelAnimation.wrappedValue = false 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/upload-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Upload DMG to GitHub Releases Script 5 | # This script uploads the built DMG to GitHub releases 6 | 7 | if [ $# -lt 1 ]; then 8 | echo "Usage: $0 [dmg_file]" 9 | echo "Example: $0 0.0.18 build/Ora-Browser.dmg" 10 | exit 1 11 | fi 12 | 13 | VERSION=$1 14 | DMG_FILE=${2:-"build/Ora-Browser.dmg"} 15 | REPO="the-ora/browser" 16 | 17 | echo "📤 Uploading Ora Browser v$VERSION DMG to GitHub..." 18 | 19 | # Check if gh CLI is installed 20 | if ! command -v gh &> /dev/null; then 21 | echo "❌ GitHub CLI (gh) not found. Please install it first:" 22 | echo " brew install gh" 23 | echo " gh auth login" 24 | exit 1 25 | fi 26 | 27 | # Check if DMG file exists 28 | if [ ! -f "$DMG_FILE" ]; then 29 | echo "❌ DMG file not found: $DMG_FILE" 30 | exit 1 31 | fi 32 | 33 | # Check if release already exists 34 | if gh release view "v$VERSION" --repo "$REPO" &> /dev/null; then 35 | echo "📋 Release v$VERSION already exists. Uploading DMG to existing release..." 36 | gh release upload "v$VERSION" "$DMG_FILE" --repo "$REPO" --clobber 37 | else 38 | echo "📋 Creating new release v$VERSION..." 39 | gh release create "v$VERSION" "$DMG_FILE" \ 40 | --repo "$REPO" \ 41 | --title "Ora Browser v$VERSION" \ 42 | --notes "Release v$VERSION of Ora Browser" \ 43 | --generate-notes 44 | fi 45 | 46 | echo "✅ Successfully uploaded $DMG_FILE to GitHub release v$VERSION" 47 | echo "🔗 Release URL: https://github.com/$REPO/releases/tag/v$VERSION" -------------------------------------------------------------------------------- /ora/Common/Extensions/View+Shortcuts.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // Attach app shortcuts that update as overrides change. 4 | private struct OraKeyboardShortcutModifier: ViewModifier { 5 | let shortcut: KeyboardShortcutDefinition 6 | @EnvironmentObject private var shortcutManager: CustomKeyboardShortcutManager 7 | 8 | func body(content: Content) -> some View { 9 | content 10 | .keyboardShortcut(shortcut.keyboardShortcut) 11 | } 12 | } 13 | 14 | // Attach a tooltip that includes the current shortcut display. 15 | private struct OraShortcutHelpModifier: ViewModifier { 16 | let helpText: String 17 | let shortcut: KeyboardShortcutDefinition 18 | @EnvironmentObject private var shortcutManager: CustomKeyboardShortcutManager 19 | 20 | func body(content: Content) -> some View { 21 | content 22 | .help("\(helpText) (\(shortcut.currentChord.display))") 23 | } 24 | } 25 | 26 | extension View { 27 | /// Use in place of `.keyboardShortcut` to auto-update on custom shortcut changes. 28 | func oraShortcut(_ shortcut: KeyboardShortcutDefinition) -> some View { 29 | modifier(OraKeyboardShortcutModifier(shortcut: shortcut)) 30 | } 31 | 32 | /// Helper to keep tooltips in sync with the current shortcut mapping. 33 | /// Results in a tooltip like: "Copy URL (⇧⌘C)" 34 | func oraShortcutHelp(_ helpText: String, for shortcut: KeyboardShortcutDefinition) -> some View { 35 | modifier(OraShortcutHelpModifier(helpText: helpText, shortcut: shortcut)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ora/WindowControls.xcassets/close-hover.imageset/Close Hover Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ora/Modules/Settings/Sections/PrivacySecuritySettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PrivacySecuritySettingsView: View { 4 | @StateObject private var settings = SettingsStore.shared 5 | 6 | var body: some View { 7 | SettingsContainer(maxContentWidth: 760) { 8 | Form { 9 | VStack(alignment: .leading, spacing: 32) { 10 | VStack(alignment: .leading, spacing: 8) { 11 | Section { 12 | Text("Tracking Prevention").foregroundStyle(.secondary) 13 | Toggle("Block third-party trackers", isOn: $settings.blockThirdPartyTrackers) 14 | Toggle("Block fingerprinting", isOn: $settings.blockFingerprinting) 15 | Toggle("Ad Blocking", isOn: $settings.adBlocking) 16 | } 17 | } 18 | 19 | VStack(alignment: .leading, spacing: 8) { 20 | Section { 21 | Text("Cookies").foregroundStyle(.secondary) 22 | Picker("", selection: $settings.cookiesPolicy) { 23 | ForEach(CookiesPolicy.allCases) { policy in 24 | Text(policy.rawValue).tag(policy) 25 | } 26 | } 27 | .pickerStyle(.radioGroup) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ora/Modules/SplitView/SplitEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitEnums.swift 3 | // SplitView 4 | // 5 | // Created by Steven Harris on 1/31/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The orientation of the `primary` and `secondary` views (e.g., Vertical = VStack, Horizontal = HStack) 11 | public enum SplitLayout: String, CaseIterable { 12 | case horizontal 13 | case vertical 14 | } 15 | 16 | /// The two sides of a SplitView. 17 | /// 18 | /// Use `isPrimary` and `isSecondary` rather than accessing the cases directly. 19 | /// 20 | /// For `SplitLayout.horizontal`, `primary` is left, `secondary` is right. 21 | /// For `SplitLayout.vertical`, `primary` is top, `secondary` is bottom. 22 | /// 23 | /// For convenience and clarity when creating and constraining an HSplit view, you can use 24 | /// `left` and `right` instead of `primary` and `secondary`. Similarly you can 25 | /// use `top` and `bottom` when creating and constraining a VSplit view. 26 | public enum SplitSide: String { 27 | case primary 28 | case secondary 29 | case left 30 | case right 31 | case top 32 | case bottom 33 | 34 | public var isPrimary: Bool { self == .primary || self == .left || self == .top } 35 | public var isSecondary: Bool { self == .secondary || self == .right || self == .bottom } 36 | } 37 | 38 | /// A SplitSide is generally optional. If so, then if nil, it is neither primary nor secondary. 39 | public extension SplitSide? { 40 | var isPrimary: Bool { self == nil ? false : self!.isPrimary } 41 | var isSecondary: Bool { self == nil ? false : self!.isSecondary } 42 | } 43 | -------------------------------------------------------------------------------- /oraUITests/oraUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // oraUITests.swift 3 | // oraUITests 4 | // 5 | // Created by keni on 6/21/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class OraUITests: XCTestCase { 11 | override func setUpWithError() throws { 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | 14 | // In UI tests it is usually best to stop immediately when a failure occurs. 15 | continueAfterFailure = false 16 | 17 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests 18 | // before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTApplicationLaunchMetric()]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ora/Modules/SplitView/SplitConstraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitConstraints.swift 3 | // SplitView 4 | // 5 | // Created by Steven Harris on 2/13/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct SplitConstraints { 11 | /// The minimum fraction that the primary view will be constrained within. A value of `nil` means unconstrained. 12 | var minPFraction: CGFloat? 13 | /// The minimum fraction that the secondary view will be constrained within. A value of `nil` means unconstrained. 14 | var minSFraction: CGFloat? 15 | /// The side that should have sizing priority (i.e., stay fixed) as the containing view is resized. A value of `nil` 16 | /// means the fraction remains unchanged. 17 | var priority: SplitSide? 18 | /// Whether to hide the primary side when dragging stops past minPFraction 19 | var dragToHideP: Bool 20 | /// Whether to hide the secondary side when dragging stops past minSFraction 21 | var dragToHideS: Bool 22 | 23 | public init( 24 | minPFraction: CGFloat? = nil, 25 | minSFraction: CGFloat? = nil, 26 | priority: SplitSide? = nil, 27 | dragToHideP: Bool = false, 28 | dragToHideS: Bool = false 29 | ) { 30 | self.minPFraction = minPFraction 31 | self.minSFraction = minSFraction 32 | self.priority = priority 33 | // Note: minPFraction/minSFraction must be specified if dragToHideP/dragToHideS is true, 34 | // else dragToHideP/dragToHideS are ignored. 35 | self.dragToHideP = dragToHideP 36 | self.dragToHideS = dragToHideS 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.cursor-rules.yaml: -------------------------------------------------------------------------------- 1 | # .cursor-rules.yaml 2 | 3 | # This Cursor rules file enforces the macOS project structure. 4 | # It helps keep the project organized by modules, shared components, services, and common resources. 5 | 6 | folders: 7 | - path: Modules 8 | required: true 9 | description: > 10 | Contains feature modules. Each module (screen/feature) should have its own folder. 11 | Place all files related to a module (e.g., views, view models, subviews) inside its respective folder. 12 | 13 | - path: UI 14 | required: false 15 | description: > 16 | Shared, reusable UI components. 17 | For components with multiple variants (e.g., Buttons), create a subfolder (e.g., Buttons) and place each variant (e.g., Button, IconButton) as a separate file within that folder. 18 | For components that typically have a single variant (e.g., TextField), place the file directly under UI without a subfolder. 19 | 20 | - path: Services 21 | required: false 22 | description: > 23 | Application-wide services and managers. 24 | Each service can have its own folder if it consists of multiple related files. 25 | 26 | - path: Common 27 | required: false 28 | description: > 29 | Shared resources, extensions, and miscellaneous files. 30 | Organize resources (e.g., colors, fonts, strings), extensions, and data files in subfolders as needed. 31 | 32 | files: 33 | - pattern: oraApp.swift 34 | required: true 35 | description: "App file." 36 | - pattern: ContentView.swift 37 | required: false 38 | description: "Main view file." 39 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIconDev.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ora/Assets.xcassets/OraIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ora-white-macos-icon.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Pull Request – Ora Browser 2 | 3 | Thank you for contributing to Ora. Please fill out this template to help us review your PR efficiently. 4 | 5 | --- 6 | 7 | ### Summary 8 | 9 | Describe what this PR does and why it is needed. 10 | If it fixes a bug, link the related issue (e.g., Closes #123). 11 | If it introduces a new feature, explain the motivation behind it. 12 | 13 | --- 14 | 15 | ### Type of Change 16 | 17 | Select all that apply: 18 | 19 | - [ ] Bug fix 20 | - [ ] New feature 21 | - [ ] Code cleanup / refactor 22 | - [ ] Design/UX improvement 23 | - [ ] Documentation update 24 | - [ ] Build / tooling change 25 | 26 | --- 27 | 28 | ### Screenshots / Recordings (if applicable) 29 | 30 | Attach screenshots or recordings if your changes modify the UI or user experience. 31 | 32 | --- 33 | 34 | ### AI Assistance Disclosure 35 | 36 | If AI assistance was used for any part of this PR (code, docs, commit messages, or descriptions), please disclose it here: 37 | 38 | Example: “Used Claude Code for writing .” 39 | 40 | --- 41 | 42 | ### Checklist 43 | 44 | Before submitting, ensure the following: 45 | • Code builds successfully on macOS 14+ 46 | • All tests pass locally 47 | • Code follows Ora’s formatting and linting standards 48 | • No new warnings or errors 49 | • Descriptive title and summary provided 50 | • Screenshots attached if applicable 51 | • Related issues linked below 52 | 53 | --- 54 | 55 | ### Related Issues 56 | 57 | Closes # 58 | 59 | --- 60 | 61 | ### Additional Notes 62 | 63 | Add any relevant context, trade-offs, or future follow-up plans here. -------------------------------------------------------------------------------- /ora/Models/TabContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | // MARK: - TabContainer 5 | 6 | @Model 7 | class TabContainer: ObservableObject, Identifiable { 8 | var id: UUID 9 | var name: String 10 | var emoji: String 11 | var createdAt: Date 12 | var lastAccessedAt: Date 13 | 14 | @Relationship(deleteRule: .cascade) var tabs: [Tab] = [] 15 | @Relationship(deleteRule: .cascade) var folders: [Folder] = [] 16 | @Relationship() var history: [History] = [] 17 | 18 | init( 19 | id: UUID = UUID(), 20 | name: String = "Default", 21 | isActive: Bool = true, 22 | emoji: String = "💩" 23 | ) { 24 | let nowDate = Date() 25 | self.id = id 26 | self.name = name 27 | self.emoji = emoji 28 | self.createdAt = nowDate 29 | self.lastAccessedAt = nowDate 30 | } 31 | 32 | func reorderTabs(from: Tab, to: Tab) { 33 | let dir = from.order - to.order > 0 ? -1 : 1 34 | 35 | let tabOrder = self.tabs.sorted { dir == -1 ? $0.order > $1.order : $0.order < $1.order } 36 | 37 | var started = false 38 | for (index, tab) in tabOrder.enumerated() { 39 | if tab.id == from.id { 40 | started = true 41 | } 42 | if tab.id == to.id { 43 | break 44 | } 45 | if started { 46 | let currentTab = tab 47 | let nextTab = tabOrder[index + 1] 48 | 49 | let tempOrder = currentTab.order 50 | currentTab.order = nextTab.order 51 | nextTab.order = tempOrder 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Pods 3 | - Carthage 4 | - .build 5 | - DerivedData 6 | - build 7 | - Build 8 | - "*.xcworkspace" 9 | - "*.xcodeproj" 10 | - fastlane/report.xml 11 | - fastlane/Preview.html 12 | - fastlane/screenshots 13 | - fastlane/test_output 14 | 15 | opt_in_rules: 16 | - empty_count 17 | - force_unwrapping 18 | - implicitly_unwrapped_optional 19 | - weak_delegate 20 | 21 | # Disabled rules to avoid conflicts with SwiftFormat 22 | disabled_rules: 23 | - trailing_comma # SwiftFormat handles this with --commas inline 24 | - vertical_whitespace # SwiftFormat handles whitespace 25 | - opening_brace # SwiftFormat handles brace style with --allman false 26 | - statement_position # SwiftFormat handles statement positioning 27 | - redundant_string_enum_value # Let SwiftFormat handle redundancy 28 | - sorted_imports # SwiftFormat has this disabled 29 | 30 | # Rule configurations 31 | line_length: 32 | warning: 120 33 | # error: 140 34 | 35 | file_length: 36 | warning: 500 37 | error: 800 38 | 39 | function_body_length: 40 | warning: 50 41 | error: 100 42 | 43 | type_body_length: 44 | warning: 300 45 | error: 500 46 | 47 | cyclomatic_complexity: 48 | warning: 10 49 | error: 20 50 | 51 | nesting: 52 | type_level: 53 | warning: 2 54 | error: 3 55 | function_level: 56 | warning: 2 57 | error: 3 58 | 59 | force_unwrapping: 60 | severity: warning 61 | 62 | implicitly_unwrapped_optional: 63 | severity: warning 64 | 65 | custom_rules: 66 | no_print_statements: 67 | name: "No Print Statements" 68 | regex: "print\\(" 69 | message: "Use logger instead of print statements" 70 | severity: warning 71 | -------------------------------------------------------------------------------- /ora/Services/DefaultBrowserManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBrowserManager.swift 3 | // ora 4 | // 5 | // Created by keni on 9/30/25. 6 | // 7 | 8 | import AppKit 9 | import Combine 10 | import CoreServices 11 | 12 | class DefaultBrowserManager: ObservableObject { 13 | static let shared = DefaultBrowserManager() 14 | 15 | @Published var isDefault: Bool = false 16 | 17 | private var cancellables = Set() 18 | 19 | private init() { 20 | updateIsDefault() 21 | // Periodically check if default browser status changed. I couldn't find another way. 22 | Timer.publish(every: 1.0, on: .main, in: .common) 23 | .autoconnect() 24 | .sink { [weak self] _ in 25 | self?.updateIsDefault() 26 | } 27 | .store(in: &cancellables) 28 | } 29 | 30 | private func updateIsDefault() { 31 | let newValue = Self.checkIsDefault() 32 | if newValue != isDefault { 33 | isDefault = newValue 34 | } 35 | } 36 | 37 | static func checkIsDefault() -> Bool { 38 | guard let testURL = URL(string: "http://example.com"), 39 | let appURL = NSWorkspace.shared.urlForApplication(toOpen: testURL), 40 | let appBundle = Bundle(url: appURL) 41 | else { 42 | return false 43 | } 44 | 45 | return appBundle.bundleIdentifier == Bundle.main.bundleIdentifier 46 | } 47 | 48 | static func requestSetAsDefault() { 49 | let bundleID = Bundle.main.bundleIdentifier! as CFString 50 | LSSetDefaultHandlerForURLScheme("http" as CFString, bundleID) 51 | LSSetDefaultHandlerForURLScheme("https" as CFString, bundleID) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/reddit-capsule-logo.imageset/reddit-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/reddit-capsule-logo.imageset/reddit-capsule-logo 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Services/SidebarManager.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum SidebarPosition: String, Hashable { 4 | case primary 5 | case secondary 6 | } 7 | 8 | @MainActor 9 | class SidebarManager: ObservableObject { 10 | @AppStorage("ui.sidebar.hidden") var isSidebarHidden: Bool = false 11 | @AppStorage("ui.sidebar.position") var sidebarPosition: SidebarPosition = .primary 12 | 13 | @Published var primaryFraction = FractionHolder.usingUserDefaults(0.2, key: "ui.sidebar.fraction.primary") 14 | @Published var secondaryFraction = FractionHolder.usingUserDefaults(0.2, key: "ui.sidebar.fraction.secondary") 15 | @Published var hiddenSidebar = SideHolder.usingUserDefaults(key: "ui.sidebar.visibility") 16 | 17 | var currentFraction: FractionHolder { 18 | sidebarPosition == .primary ? primaryFraction : secondaryFraction 19 | } 20 | 21 | func updateSidebarHidden() { 22 | isSidebarHidden = hiddenSidebar.side == .primary || hiddenSidebar.side == .secondary 23 | } 24 | 25 | func toggleSidebar() { 26 | let targetSide = sidebarPosition == .primary ? SplitSide.primary : .secondary 27 | withAnimation(.spring(response: 0.2, dampingFraction: 1.0)) { 28 | hiddenSidebar.side = (hiddenSidebar.side == targetSide) ? nil : targetSide 29 | updateSidebarHidden() 30 | } 31 | } 32 | 33 | func toggleSidebarPosition() { 34 | let isCurrentSidebarHidden = hiddenSidebar.side == (sidebarPosition == .primary ? .primary : .secondary) 35 | sidebarPosition = sidebarPosition == .primary ? .secondary : .primary 36 | if isCurrentSidebarHidden { 37 | hiddenSidebar.side = sidebarPosition == .primary ? .primary : .secondary 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ora/Common/Constants/AppEvents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Notification.Name { 4 | static let toggleSidebar = Notification.Name("ToggleSidebar") 5 | static let toggleSidebarPosition = Notification.Name("ToggleSidebarPosition") 6 | static let copyAddressURL = Notification.Name("CopyAddressURL") 7 | 8 | static let showLauncher = Notification.Name("ShowLauncher") 9 | static let closeActiveTab = Notification.Name("CloseActiveTab") 10 | static let restoreLastTab = Notification.Name("RestoreLastTab") 11 | static let findInPage = Notification.Name("FindInPage") 12 | static let toggleFullURL = Notification.Name("ToggleFullURL") 13 | static let reloadPage = Notification.Name("ReloadPage") 14 | static let goBack = Notification.Name("GoBack") 15 | static let goForward = Notification.Name("GoForward") 16 | static let togglePinTab = Notification.Name("TogglePinTab") 17 | static let nextTab = Notification.Name("NextTab") 18 | static let previousTab = Notification.Name("PreviousTab") 19 | static let toggleToolbar = Notification.Name("ToggleToolbar") 20 | static let selectTabAtIndex = Notification.Name("SelectTabAtIndex") // userInfo: ["index": Int] 21 | 22 | // Per-window settings/events 23 | static let setAppearance = Notification.Name("SetAppearance") // userInfo: ["appearance": String] 24 | static let checkForUpdates = Notification.Name("CheckForUpdates") 25 | 26 | // AppDelegate → UI routing 27 | static let openURL = Notification.Name("OpenURL") // userInfo: ["url": URL] 28 | 29 | // Cache and cookies 30 | static let clearCacheAndReload = Notification.Name("ClearCacheAndReload") 31 | static let clearCookiesAndReload = Notification.Name("ClearCookiesAndReload") 32 | } 33 | -------------------------------------------------------------------------------- /ora/Common/Extensions/Color+Hex.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // swiftlint:disable identifier_name 4 | extension Color { 5 | init(hex: String) { 6 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 7 | var int: UInt64 = 0 8 | Scanner(string: hex).scanHexInt64(&int) 9 | let a: UInt64, r: UInt64, g: UInt64, b: UInt64 10 | switch hex.count { 11 | case 3: // RGB (12-bit) 12 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 13 | case 6: // RGB (24-bit) 14 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 15 | case 8: // ARGB (32-bit) 16 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 17 | default: 18 | (a, r, g, b) = (1, 1, 1, 0) 19 | } 20 | 21 | self.init( 22 | .sRGB, 23 | red: Double(r) / 255, 24 | green: Double(g) / 255, 25 | blue: Double(b) / 255, 26 | opacity: Double(a) / 255 27 | ) 28 | } 29 | 30 | /// Converts the Color to a hex string in `#RRGGBB` or `#RRGGBBAA` format 31 | func toHex(includeAlpha: Bool = false) -> String? { 32 | let nsColor = NSColor(self) 33 | guard let rgbColor = nsColor.usingColorSpace(.sRGB) else { 34 | return nil 35 | } 36 | 37 | let r = Int(rgbColor.redComponent * 255) 38 | let g = Int(rgbColor.greenComponent * 255) 39 | let b = Int(rgbColor.blueComponent * 255) 40 | let a = Int(rgbColor.alphaComponent * 255) 41 | 42 | return includeAlpha 43 | ? String(format: "#%02X%02X%02X%02X", r, g, b, a) 44 | : String(format: "#%02X%02X%02X", r, g, b) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Formatting 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main, develop] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: macos-14 12 | 13 | strategy: 14 | matrix: 15 | xcode: ['15.0'] 16 | 17 | steps: 18 | - name: Check architecture 19 | run: uname -m 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Select Xcode version 25 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 26 | 27 | - name: Show Xcode version 28 | run: xcodebuild -version 29 | 30 | - name: Show Swift version 31 | run: swift --version 32 | 33 | - name: Install Xcodegen 34 | run: brew install xcodegen 35 | 36 | - name: Generate Xcode project 37 | run: xcodegen 38 | 39 | - name: Install SwiftLint 40 | run: brew install swiftlint 41 | 42 | - name: Run SwiftLint 43 | run: swiftlint lint 44 | 45 | - name: Install SwiftFormat 46 | run: brew install swiftformat 47 | 48 | - name: Check code formatting 49 | run: swiftformat --lint . 50 | 51 | - name: Install Xcbeautify 52 | run: brew install xcbeautify 53 | 54 | - name: Clean build folder 55 | run: xcodebuild clean -scheme ora -project Ora.xcodeproj 56 | 57 | - name: Cache SPM dependencies 58 | uses: actions/cache@v3 59 | with: 60 | path: | 61 | ~/Library/Caches/org.swift.swiftpm/ 62 | ~/.swiftpm/ 63 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.swift') }} 64 | restore-keys: | 65 | ${{ runner.os }}-spm- 66 | 67 | - name: Resolve dependencies 68 | run: xcodebuild -resolvePackageDependencies -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | *.xcodeproj 3 | *.xcuserstate 4 | *.xcuserdatad/ 5 | *.xcworkspace 6 | DerivedData/ 7 | build/ 8 | *.ipa 9 | *.dSYM.zip 10 | *.dSYM 11 | 12 | # XcodeGen 13 | project.yml.lock 14 | 15 | # Xcode Build Server 16 | buildServer.json 17 | 18 | # Swift Package Manager 19 | .swiftpm/ 20 | Package.resolved 21 | 22 | # macOS 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | # IDE 47 | .vscode/ 48 | .idea/ 49 | .zed/ 50 | 51 | # Build directory (allow directory but ignore contents) 52 | build/* 53 | !build/ 54 | !build/.gitkeep 55 | 56 | # Build artifacts and releases 57 | *.app 58 | *.dmg 59 | *.pkg 60 | *.zip 61 | *.tar.gz 62 | appcast.xml 63 | appcast.xml.bak 64 | dsa_priv.pem 65 | dsa_pub.pem 66 | exportOptions.plist 67 | Ora-Browser.dmg 68 | rw.*.Ora-Browser.dmg 69 | 70 | # Sparkle and signing (SECURITY: Never commit private keys!) 71 | *_private.pem 72 | *_public.pem 73 | # *.pem 74 | dsa_priv.pem # CRITICAL: Never commit this file! 75 | dsa_pub.pem # Public key (safe to commit if needed) 76 | 77 | # Environment files (contains private keys!) 78 | .env 79 | .env.local 80 | .env.*.local 81 | 82 | # Archives and temporary files 83 | *.xcarchive 84 | *.xctimeline 85 | *.trace 86 | *.log 87 | codesign* 88 | *.bak 89 | *.backup 90 | *.provisionprofile 91 | 92 | # Release artifacts (keep in build/ folder) 93 | # Uncomment these if you want to commit release artifacts: 94 | # !build/*.dmg 95 | # !build/appcast.xml 96 | # !build/dsa_pub.pem 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Xcode 17 | uses: maxim-lobanov/setup-xcode@v1 18 | with: 19 | xcode-version: latest-stable 20 | 21 | - name: Install dependencies 22 | run: | 23 | brew install xcodegen create-dmg 24 | ./setup.sh 25 | 26 | - name: Build release 27 | run: | 28 | chmod +x build-release.sh 29 | ./build-release.sh 30 | 31 | - name: Sign app (if certificates available) 32 | run: | 33 | # Note: For proper signing, you'll need to set up code signing certificates 34 | # This is a placeholder for the signing step 35 | echo "App built successfully" 36 | 37 | - name: Generate appcast signature 38 | run: | 39 | # This would use your private DSA key to sign the release 40 | # For now, we'll skip this step 41 | echo "Signature generation would happen here" 42 | 43 | - name: Update appcast.xml 44 | run: | 45 | # This would update the appcast.xml with the new release info 46 | # For now, we'll use the template 47 | cp appcast.xml build/ 48 | 49 | - name: Create GitHub Release 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | files: | 53 | build/Ora.app 54 | Ora-Browser.dmg 55 | build/appcast.xml 56 | generate_release_notes: true 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Upload appcast to GitHub Pages 61 | run: | 62 | # This would upload the appcast.xml to GitHub Pages 63 | # For now, you can manually host it or use GitHub Pages 64 | echo "Upload appcast.xml to your hosting location" -------------------------------------------------------------------------------- /ora/UI/HomeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct HomeView: View { 4 | @Environment(\.theme) var theme 5 | @EnvironmentObject private var sidebarManager: SidebarManager 6 | 7 | var body: some View { 8 | ZStack(alignment: .top) { 9 | Color.clear 10 | .ignoresSafeArea(.all) 11 | .frame(maxWidth: .infinity, maxHeight: .infinity) 12 | .contentShape(Rectangle()) 13 | .background(theme.background.opacity(0.65)) 14 | .background( 15 | BlurEffectView(material: .underWindowBackground, blendingMode: .behindWindow) 16 | ) 17 | 18 | HStack { 19 | URLBarButton( 20 | systemName: "sidebar.left", 21 | isEnabled: true, 22 | foregroundColor: theme.foreground.opacity(0.3), 23 | action: { sidebarManager.toggleSidebar() } 24 | ) 25 | .oraShortcut(KeyboardShortcuts.App.toggleSidebar) 26 | } 27 | .zIndex(3) 28 | .frame(maxWidth: .infinity, alignment: sidebarManager.sidebarPosition == .primary ? .leading : .trailing) 29 | .padding(6) 30 | .ignoresSafeArea(.all) 31 | 32 | VStack(alignment: .center, spacing: 16) { 33 | Image("ora-logo-plain") 34 | .resizable() 35 | .renderingMode(.template) 36 | .frame(width: 50, height: 50) 37 | .foregroundColor(theme.foreground.opacity(0.3)) 38 | 39 | Text("Less noise, more browsing.") 40 | .font(.system(size: 16, weight: .semibold)) 41 | .foregroundColor(theme.foreground.opacity(0.3)) 42 | } 43 | .offset(x: -10, y: 120) 44 | .zIndex(2) 45 | 46 | LauncherView(clearOverlay: true) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/BottomOption/EditContainerModal.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EditContainerModal: View { 4 | let container: TabContainer 5 | @Binding var isPresented: Bool 6 | 7 | @Environment(\.theme) private var theme 8 | @EnvironmentObject var tabManager: TabManager 9 | 10 | @State private var name: String = "" 11 | @State private var emoji: String = "" 12 | @State private var isEmojiPickerOpen = false 13 | @FocusState private var isTextFieldFocused: Bool 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 10) { 17 | headerView 18 | containerForm 19 | actionButtons 20 | } 21 | .frame(width: ContainerConstants.UI.popoverWidth) 22 | .padding() 23 | .onAppear { 24 | setupInitialValues() 25 | } 26 | } 27 | 28 | private var headerView: some View { 29 | Text("Edit Container") 30 | .font(.headline) 31 | } 32 | 33 | private var containerForm: some View { 34 | ContainerForm( 35 | name: $name, 36 | emoji: $emoji, 37 | isEmojiPickerOpen: $isEmojiPickerOpen, 38 | isTextFieldFocused: $isTextFieldFocused, 39 | onSubmit: saveContainer, 40 | defaultEmoji: ContainerConstants.defaultEmoji 41 | ) 42 | } 43 | 44 | private var actionButtons: some View { 45 | Button("Save") { 46 | saveContainer() 47 | } 48 | .disabled(name.isEmpty) 49 | } 50 | 51 | private func setupInitialValues() { 52 | name = container.name 53 | emoji = container.emoji 54 | } 55 | 56 | private func saveContainer() { 57 | guard !name.isEmpty else { return } 58 | 59 | let finalEmoji = emoji.isEmpty ? ContainerConstants.defaultEmoji : emoji 60 | tabManager.renameContainer(container, name: name, emoji: finalEmoji) 61 | isPresented = false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Ora Browser Security Guide 2 | 3 | ## 🔐 Update Key Management 4 | 5 | Ora Browser uses Ed25519 cryptographic keys to sign and verify app updates for security. 6 | 7 | ### Public Key (Committed to Git) 8 | - **File**: `ora_public_key.pem` 9 | - **Purpose**: Verifies update signatures in the app 10 | - **Status**: Committed to git repository 11 | - **Safety**: Public keys are safe to share 12 | 13 | ### Private Key (Never Commit!) 14 | - **File**: `.env` (contains `ORA_PRIVATE_KEY`) 15 | - **Purpose**: Signs app updates during release 16 | - **Status**: Never committed to git 17 | - **Safety**: Keep secure and private 18 | 19 | ### Setup Process 20 | 1. **First machine**: Keys auto-generated and saved appropriately 21 | 2. **Additional machines**: Copy `.env` file from first machine 22 | 3. **Release process**: `./scripts/create-release.sh` handles key management automatically 23 | 24 | ### Security Notes 25 | - `.env` is in `.gitignore` - it will never be committed 26 | - Public key is committed - this is safe and required 27 | - Never share your private key with anyone 28 | - If private key is lost, you'll need to regenerate keys (breaks update chain) 29 | 30 | ## 🔍 Security Checks 31 | 32 | Run `./scripts/check-security.sh` to verify: 33 | - Private key exists but is not tracked by git 34 | - Public key is available for app integration 35 | - `.gitignore` properly excludes sensitive files 36 | 37 | ## 🚨 Security Best Practices 38 | 39 | - **NEVER** commit private keys to version control 40 | - **NEVER** share private keys with anyone 41 | - **NEVER** delete private keys once you've published releases (breaks update chain) 42 | - Use secure methods to transfer keys between machines 43 | - Regularly audit what's in your git staging area before committing 44 | 45 | ## 🚨 Security Violations 46 | 47 | If you see any of these, stop immediately: 48 | - Private key files appear in `git status` 49 | - Private keys are committed to repository 50 | - Private keys are shared or transmitted insecurely -------------------------------------------------------------------------------- /ora/Modules/Browser/BrowserContentContainer.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BrowserContentContainer: View { 4 | @EnvironmentObject var tabManager: TabManager 5 | @EnvironmentObject var appState: AppState 6 | @EnvironmentObject var sidebarManager: SidebarManager 7 | 8 | let content: () -> Content 9 | 10 | private var isCompleteFullscreen: Bool { 11 | appState.isFullscreen && sidebarManager.isSidebarHidden 12 | } 13 | 14 | private var cornerRadius: CGFloat { 15 | if #available(macOS 26, *) { 16 | return 13 17 | } else { 18 | return 6 19 | } 20 | } 21 | 22 | init( 23 | @ViewBuilder content: @escaping () -> Content 24 | ) { 25 | self.content = content 26 | } 27 | 28 | var body: some View { 29 | content() 30 | .frame(maxWidth: .infinity, maxHeight: .infinity) 31 | .clipShape(RoundedRectangle(cornerRadius: isCompleteFullscreen ? 0 : cornerRadius, style: .continuous)) 32 | .padding( 33 | isCompleteFullscreen 34 | ? EdgeInsets( 35 | top: 0, 36 | leading: 0, 37 | bottom: 0, 38 | trailing: 0 39 | ) 40 | : EdgeInsets( 41 | top: 6, 42 | leading: sidebarManager.sidebarPosition != .primary || sidebarManager.hiddenSidebar 43 | .side == .primary ? 6 : 0, 44 | bottom: 6, 45 | trailing: sidebarManager.sidebarPosition != .secondary || sidebarManager.hiddenSidebar 46 | .side == .secondary ? 6 : 0 47 | ) 48 | ) 49 | .animation(.easeInOut(duration: 0.3), value: appState.isFullscreen) 50 | .shadow(color: .black.opacity(0.15), radius: isCompleteFullscreen ? 0 : cornerRadius, x: 0, y: 2) 51 | .ignoresSafeArea(.all) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ora/Models/SearchEngine.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SearchEngine { 4 | let name: String 5 | let color: Color 6 | let icon: String 7 | let aliases: [String] 8 | let searchURL: String 9 | let isAIChat: Bool 10 | let foregroundColor: Color? 11 | let autoSuggestions: ((String) async -> [String])? 12 | 13 | init( 14 | name: String, 15 | color: Color, 16 | icon: String, 17 | aliases: [String], 18 | searchURL: String, 19 | isAIChat: Bool, 20 | foregroundColor: Color? = nil, 21 | autoSuggestions: ((String) async -> [String])? = nil 22 | ) { 23 | self.name = name 24 | self.color = color 25 | self.icon = icon 26 | self.aliases = aliases 27 | self.searchURL = searchURL 28 | self.isAIChat = isAIChat 29 | self.foregroundColor = foregroundColor 30 | self.autoSuggestions = autoSuggestions 31 | } 32 | } 33 | 34 | extension SearchEngine { 35 | func toLauncherMatch( 36 | originalAlias: String, 37 | customEngine: CustomSearchEngine? = nil 38 | ) -> LauncherMain.Match { 39 | var favicon: NSImage? 40 | var faviconColor: Color? 41 | 42 | // Use cached favicon data from custom engine if available 43 | if let customEngine { 44 | favicon = customEngine.favicon 45 | faviconColor = customEngine.faviconBackgroundColor 46 | } else { 47 | // For built-in engines, use favicon service 48 | favicon = FaviconService.shared.getFavicon(for: searchURL) 49 | faviconColor = FaviconService.shared.getFaviconColor(for: searchURL) 50 | } 51 | 52 | return LauncherMain.Match( 53 | text: name, 54 | color: color, 55 | foregroundColor: foregroundColor ?? .white, 56 | icon: icon, 57 | originalAlias: originalAlias, 58 | searchURL: searchURL, 59 | favicon: favicon, 60 | faviconBackgroundColor: faviconColor 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ora/UI/WindowControls.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | enum WindowControlType { 5 | case close, minimize, zoom 6 | } 7 | 8 | struct WindowControls: View { 9 | @State private var isHovered = false 10 | let isFullscreen: Bool 11 | 12 | var body: some View { 13 | if !isFullscreen { 14 | HStack(spacing: 9) { 15 | WindowControlButton(type: .close, isHovered: $isHovered) 16 | WindowControlButton(type: .minimize, isHovered: $isHovered) 17 | WindowControlButton(type: .zoom, isHovered: $isHovered) 18 | } 19 | .padding(.horizontal, 8) 20 | .onHover { hovering in 21 | withAnimation(.easeInOut(duration: 0.1)) { 22 | isHovered = hovering 23 | } 24 | } 25 | } else { 26 | EmptyView() 27 | } 28 | } 29 | } 30 | 31 | struct WindowControlButton: View { 32 | let type: WindowControlType 33 | @Binding var isHovered: Bool 34 | 35 | private var buttonSize: CGFloat { 36 | if #available(macOS 26.0, *) { 37 | return 14 38 | } else { 39 | return 12 40 | } 41 | } 42 | 43 | private var assetBaseName: String { 44 | switch type { 45 | case .close: return "close" 46 | case .minimize: return "minimize" 47 | case .zoom: return "maximize" 48 | } 49 | } 50 | 51 | var body: some View { 52 | Image(isHovered ? "\(assetBaseName)-hover" : "\(assetBaseName)-normal") 53 | .resizable() 54 | .frame(width: buttonSize, height: buttonSize) 55 | .onTapGesture { 56 | performAction() 57 | } 58 | } 59 | 60 | private func performAction() { 61 | guard let window = NSApp.keyWindow else { return } 62 | switch type { 63 | case .close: 64 | window.performClose(nil) 65 | case .minimize: 66 | window.performMiniaturize(nil) 67 | case .zoom: 68 | window.toggleFullScreen(nil) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ora/Modules/SplitView/SplitStyling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitStyling.swift 3 | // SplitView 4 | // 5 | // Created by Steven Harris on 3/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor 11 | public class SplitStyling: ObservableObject { 12 | /// Color of the visible part of the default Splitter. 13 | public var color: Color 14 | /// The inset for the visible part of the default Splitter from the ends it reaches to. 15 | public var inset: CGFloat 16 | /// The visible thickness of the default Splitter and the `spacing` between the `primary` and `secondary` views. 17 | public var visibleThickness: CGFloat 18 | /// The thickness across which the dragging will be detected. 19 | public var invisibleThickness: CGFloat 20 | /// Whether to hide the splitter along with the side when SplitSide is set. 21 | public var hideSplitter: Bool 22 | /// Whether we are previewing what hiding will look like. 23 | @Published public var previewHide: Bool 24 | 25 | public init( 26 | color: Color? = nil, 27 | inset: CGFloat? = nil, 28 | visibleThickness: CGFloat? = nil, 29 | invisibleThickness: CGFloat? = nil, 30 | hideSplitter: Bool = false 31 | ) { 32 | self.color = color ?? Splitter.defaultColor 33 | self.inset = inset ?? Splitter.defaultInset 34 | self.visibleThickness = visibleThickness ?? Splitter.defaultVisibleThickness 35 | self.invisibleThickness = invisibleThickness ?? Splitter.defaultInvisibleThickness 36 | self.hideSplitter = hideSplitter 37 | self.previewHide = false // We never start out previewing 38 | } 39 | 40 | /// As an ObservableObject, when we want to change to a different SplitStyling, we need to just modify the 41 | /// properties of this instance. 42 | public func reset(from styling: SplitStyling) { 43 | color = styling.color 44 | inset = styling.inset 45 | visibleThickness = styling.visibleThickness 46 | invisibleThickness = styling.invisibleThickness 47 | hideSplitter = styling.hideSplitter 48 | previewHide = styling.previewHide 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ora/UI/LinkPreview.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LinkPreview: View { 4 | let text: String 5 | @Environment(\.theme) private var theme 6 | 7 | private func getAppVersion() -> String { 8 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?" 9 | return "Ora \(version)" 10 | } 11 | 12 | var body: some View { 13 | VStack { 14 | Spacer() 15 | HStack { 16 | ZStack { 17 | Text(text) 18 | .font(.system(size: 12, weight: .regular)) 19 | .foregroundStyle(theme.foreground) 20 | .lineLimit(1) 21 | .truncationMode(.middle) 22 | .multilineTextAlignment(.leading) 23 | } 24 | .padding(.vertical, 6) 25 | .padding(.horizontal, 8) 26 | .background( 27 | RoundedRectangle(cornerRadius: 99, style: .continuous) 28 | .fill(Color(.windowBackgroundColor)) 29 | .overlay( 30 | RoundedRectangle(cornerRadius: 99, style: .continuous) 31 | .stroke(Color(.separatorColor), lineWidth: 1) 32 | ) 33 | ) 34 | 35 | Spacer() 36 | 37 | Text(getAppVersion()) 38 | .font(.system(size: 10, weight: .regular)) 39 | .foregroundStyle(Color.white.opacity(0.6)) 40 | .padding(.horizontal, 8) 41 | .padding(.vertical, 4) 42 | .background( 43 | RoundedRectangle(cornerRadius: 6, style: .continuous) 44 | .fill(Color.black.opacity(0.2)) 45 | ) 46 | .padding(.trailing, 12) 47 | } 48 | .padding(.bottom, 8) 49 | .padding(.leading, 8) 50 | } 51 | .transition(.opacity) 52 | .animation(.easeOut(duration: 0.1), value: text) 53 | .zIndex(900) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Suggest a new idea or improvement for Ora Browser 3 | labels: ["enhancement", "triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for contributing to Ora’s future! 9 | Please share as much detail as possible about your idea. 10 | 11 | - type: textarea 12 | id: feature 13 | attributes: 14 | label: Describe the Feature 15 | description: A clear description of what the feature is and what problem it solves. 16 | placeholder: Explain your feature idea in detail. 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: use-case 22 | attributes: 23 | label: Use Case 24 | description: Describe how users (including yourself) would use this feature. 25 | placeholder: I’d use this to quickly switch between Spaces without reopening tabs... 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: fit 31 | attributes: 32 | label: Fit with Ora’s Goals 33 | description: Explain how this aligns with Ora’s goals (fast, secure, beautiful). 34 | placeholder: This improves performance by... 35 | validations: 36 | required: false 37 | 38 | - type: textarea 39 | id: complexity 40 | attributes: 41 | label: Implementation Complexity & User Impact 42 | description: Your thoughts on how difficult this might be to implement and who benefits. 43 | placeholder: Seems easy to add since WebKit already supports X... 44 | validations: 45 | required: false 46 | 47 | - type: textarea 48 | id: alternatives 49 | attributes: 50 | label: Alternatives Considered 51 | description: If you’ve thought of other solutions or workarounds, list them here. 52 | placeholder: I also tried using Safari extensions, but... 53 | validations: 54 | required: false 55 | 56 | - type: textarea 57 | id: additional 58 | attributes: 59 | label: Additional Context 60 | description: Add any extra context, links, or mockups. 61 | validations: 62 | required: false -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Commitment 4 | 5 | We are committed to providing a welcoming and inclusive environment for all contributors to Ora, regardless of experience level, background, or identity. 6 | 7 | Ora is built by volunteers who contribute their time and expertise out of passion for creating great software. We appreciate everyone who takes time to contribute to making Ora better. 8 | 9 | ## Expected Behavior 10 | 11 | - Be respectful, welcoming, and considerate in all interactions 12 | - Discuss ideas, give/receive constructive criticism gracefully 13 | - Focus on what's best for the community and project 14 | - Show empathy toward other community members 15 | - Help create a positive environment for learning and collaboration 16 | 17 | ## Unacceptable Behavior 18 | 19 | - Harassment, discrimination, or intimidation of any kind 20 | - Offensive, derogatory, or discriminatory comments 21 | - Personal attacks or trolling 22 | - Publishing others' private information without permission 23 | - Spam or off-topic discussions 24 | - Any conduct that would be inappropriate in a professional setting 25 | 26 | ## Scope 27 | 28 | This Code of Conduct applies to all project spaces, including: 29 | - GitHub repository (issues, PRs, discussions) 30 | - Discord community 31 | - Any other official Ora communication channels 32 | 33 | ## Enforcement 34 | 35 | Project maintainers are responsible for clarifying standards and will take appropriate action in response to violations. This may include: 36 | - Warning the individual 37 | - Temporary or permanent ban from project spaces (GitHub, Discord, etc.) 38 | - Reporting to appropriate platforms (GitHub, Discord, etc.) 39 | 40 | ## Reporting 41 | 42 | If you experience or witness unacceptable behavior, please report it by: 43 | - Opening a GitHub issue (for public matters) 44 | - Contacting maintainers directly on Discord 45 | 46 | All reports will be handled confidentially and reviewed promptly. 47 | 48 | ## Attribution 49 | 50 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. 51 | 52 | --- 53 | 54 | By participating in the Ora community, you agree to abide by this Code of Conduct. 55 | -------------------------------------------------------------------------------- /ora/Modules/Settings/Sections/AppearanceSelector.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppearanceSelector: View { 4 | @Binding var selection: AppAppearance 5 | @Environment(\.theme) var theme 6 | 7 | private struct Option: Identifiable { 8 | let id = UUID() 9 | let appearance: AppAppearance 10 | let imageName: String 11 | let title: String 12 | } 13 | 14 | private var options: [Option] { 15 | [ 16 | .init(appearance: .light, imageName: "appearance-light", title: "Light"), 17 | .init(appearance: .dark, imageName: "appearance-dark", title: "Dark"), 18 | .init(appearance: .system, imageName: "appearance-system", title: "Auto") 19 | ] 20 | } 21 | 22 | var body: some View { 23 | VStack(alignment: .leading, spacing: 8) { 24 | Text("Appearance").foregroundStyle(.secondary) 25 | HStack(spacing: 16) { 26 | ForEach(options) { opt in 27 | let isSelected = selection == opt.appearance 28 | Button { 29 | selection = opt.appearance 30 | } label: { 31 | VStack(alignment: .leading, spacing: 8) { 32 | Image(opt.imageName) 33 | .resizable() 34 | .scaledToFit() 35 | .frame(width: 105, height: 68) 36 | .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) 37 | Text(opt.title) 38 | .fontWeight(isSelected ? .semibold : .regular) 39 | } 40 | .padding(6) 41 | .background( 42 | RoundedRectangle(cornerRadius: 10, style: .continuous) 43 | .fill(isSelected ? theme.foreground.opacity(0.12) : Color.clear) 44 | ) 45 | } 46 | .buttonStyle(.plain) 47 | } 48 | } 49 | .frame(maxWidth: .infinity, alignment: .leading) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ora/Modules/Settings/SettingsContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum SettingsTab: Hashable { 4 | case general, spaces, privacySecurity, shortcuts, searchEngines 5 | 6 | var title: String { 7 | switch self { 8 | case .general: return "General" 9 | case .spaces: return "Spaces" 10 | case .privacySecurity: return "Privacy" 11 | case .shortcuts: return "Shortcuts" 12 | case .searchEngines: return "Search" 13 | } 14 | } 15 | 16 | var symbol: String { 17 | switch self { 18 | case .general: return "gearshape" 19 | case .spaces: return "rectangle.3.group" 20 | case .privacySecurity: return "lock.shield" 21 | case .shortcuts: return "command" 22 | case .searchEngines: return "magnifyingglass" 23 | } 24 | } 25 | } 26 | 27 | struct SettingsContentView: View { 28 | @State private var selection: SettingsTab = .general 29 | 30 | var body: some View { 31 | TabView(selection: $selection) { 32 | GeneralSettingsView() 33 | .tabItem { Label(SettingsTab.general.title, systemImage: SettingsTab.general.symbol) } 34 | .tag(SettingsTab.general) 35 | 36 | SpacesSettingsView() 37 | .tabItem { Label(SettingsTab.spaces.title, systemImage: SettingsTab.spaces.symbol) } 38 | .tag(SettingsTab.spaces) 39 | 40 | PrivacySecuritySettingsView() 41 | .tabItem { 42 | Label(SettingsTab.privacySecurity.title, systemImage: SettingsTab.privacySecurity.symbol) 43 | } 44 | .tag(SettingsTab.privacySecurity) 45 | 46 | ShortcutsSettingsView() 47 | .tabItem { Label(SettingsTab.shortcuts.title, systemImage: SettingsTab.shortcuts.symbol) } 48 | .tag(SettingsTab.shortcuts) 49 | 50 | SearchEngineSettingsView() 51 | .tabItem { Label(SettingsTab.searchEngines.title, systemImage: SettingsTab.searchEngines.symbol) } 52 | .tag(SettingsTab.searchEngines) 53 | } 54 | .tabViewStyle(.automatic) 55 | .frame(width: 600, height: 350) 56 | .padding(0) 57 | .controlSize(.regular) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a reproducible issue or unexpected behavior in Ora Browser 3 | labels: ["bug", "triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to report a bug! 9 | Please fill out the details below as clearly as possible. 10 | 11 | - type: input 12 | id: os-version 13 | attributes: 14 | label: macOS Version 15 | placeholder: e.g., macOS Sequoia 15.0 16 | validations: 17 | required: true 18 | 19 | - type: input 20 | id: ora-version 21 | attributes: 22 | label: Ora Browser Version 23 | placeholder: e.g., 0.2.7 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Describe the Issue 31 | description: A clear and concise description of what the bug is. 32 | placeholder: Tell us what went wrong. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: steps 38 | attributes: 39 | label: Steps to Reproduce 40 | description: List the exact steps to reproduce the issue. 41 | placeholder: | 42 | 1. Open Ora 43 | 2. Go to example.com 44 | 3. Click on ... 45 | 4. See error 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: expected 51 | attributes: 52 | label: Expected Behavior 53 | placeholder: It should have opened the page instantly... 54 | validations: 55 | required: false 56 | 57 | - type: textarea 58 | id: actual 59 | attributes: 60 | label: Actual Behavior 61 | placeholder: The page froze and showed a blank view... 62 | validations: 63 | required: false 64 | 65 | - type: textarea 66 | id: console 67 | attributes: 68 | label: Console / Error Messages 69 | description: Paste any relevant logs or error outputs if available. 70 | render: shell 71 | validations: 72 | required: false 73 | 74 | - type: textarea 75 | id: screenshots 76 | attributes: 77 | label: Screenshots 78 | description: Add any relevant screenshots or recordings. 79 | validations: 80 | required: false 81 | -------------------------------------------------------------------------------- /ora/Modules/URLBar/FloatingURLBar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct FloatingURLBar: View { 4 | @Binding var showFloatingURLBar: Bool 5 | @Binding var isMouseOverURLBar: Bool 6 | 7 | private var triggerAreaPadding: CGFloat { 8 | showFloatingURLBar ? 50 : 16 9 | } 10 | 11 | var body: some View { 12 | ZStack(alignment: .top) { 13 | if showFloatingURLBar { 14 | URLBar( 15 | onSidebarToggle: { 16 | NotificationCenter.default.post( 17 | name: .toggleSidebar, object: nil 18 | ) 19 | } 20 | ) 21 | .shadow(color: Color.black.opacity(0.2), radius: 10, y: 4) 22 | .overlay( 23 | Rectangle() 24 | .frame(height: 0.5) 25 | .foregroundColor(Color(.separatorColor)), 26 | alignment: .bottom 27 | ) 28 | .transition(.move(edge: .top).combined(with: .opacity)) 29 | .zIndex(1) 30 | } 31 | 32 | VStack(alignment: .leading) { 33 | hoverStrip(width: .infinity) 34 | Spacer() 35 | } 36 | .frame(maxWidth: .infinity) 37 | .frame(height: triggerAreaPadding) 38 | } 39 | .animation(.easeInOut(duration: 0.1), value: showFloatingURLBar) 40 | } 41 | 42 | @ViewBuilder 43 | private func hoverStrip(width: CGFloat) -> some View { 44 | Color.clear 45 | .overlay( 46 | GlobalMouseTrackingArea( 47 | mouseEntered: Binding( 48 | get: { showFloatingURLBar }, 49 | set: { newValue in 50 | withAnimation(.easeInOut(duration: 0.25)) { 51 | isMouseOverURLBar = newValue 52 | showFloatingURLBar = newValue 53 | } 54 | } 55 | ), 56 | edge: .top, 57 | padding: triggerAreaPadding, 58 | slack: 8 59 | ) 60 | .id(triggerAreaPadding) 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Ora Browser Roadmap 2 | 3 | Ora Browser is currently in **early development**. 4 | This roadmap tracks progress toward the **Beta** milestone and beyond. 5 | 6 | > Goal: A stable, privacy-first macOS browser with a clean native interface. 7 | 8 | ## Beta Feature Targets 9 | 10 | Below is the list of core features planned or completed for the Beta release. 11 | 12 | ### Tabs & Navigation 13 | - [x] Spaces (containers) 14 | - [x] Pinning and reordering 15 | - [x] Floating tab switcher 16 | - [x] Auto-closing inactive tabs 17 | 18 | ### Interface & Layout 19 | - [x] Vertical tabs (sidebar) 20 | - [x] Picture in Picture 21 | - [ ] Split tabs (side-by-side view) 22 | - [ ] Peek webview 23 | - [ ] Settings 24 | - [ ] History - [#130](https://github.com/the-ora/browser/pull/130) 25 | 26 | ### Stability & Performance 27 | - [x] Session restore after app restart or crash 28 | - [ ] Download manager with pause/resume support 29 | 30 | ### Privacy & Security 31 | - [x] Private browsing mode 32 | - [ ] iCloud Keychain password autofill 33 | - [x] Passkeys 34 | - [ ] Permissions [#48](https://github.com/the-ora/browser/pull/49) 35 | - [ ] Ad Blocker 36 | - [ ] Fingerprint 37 | 38 | ### Personalization 39 | - [ ] Bookmark management with folders 40 | 41 | ### Developer Features 42 | - [x] Developer Tools 43 | - [ ] Extensions — Safari & Chrome extensions (beta) [#137](https://github.com/the-ora/browser/pull/137) 44 | 45 | ### System Integration 46 | - [ ] Web notifications 47 | 48 | ### Power User Features 49 | - [x] Keyboard shortcuts for navigation and tabs 50 | 51 | --- 52 | 53 | ## Milestones 54 | 55 | | Phase | Status | Focus | 56 | |--------|---------|--------| 57 | | **Alpha (Current)** | 🟢 Active | Core browsing, tabs, session management, and core UIs | 58 | | **Beta 1** | Soon | Autofill, bookmarks(folders), downloads, peek, split tab views and more | 59 | | **Stable 1.0** | Future | Full extensions support, performance, UI polish | 60 | 61 | --- 62 | 63 | ## Post‑Beta Plans 64 | - [ ] Full extensions ecosystem 65 | - [ ] Theming 66 | 67 | --- 68 | 69 | ## Contributing & Feedback 70 | - 💬 Join discussions on [Discord](https://discord.gg/9aZWH52Zjm) 71 | - 💡 Suggest features or discuss roadmap items in our discord 72 | - 📘 See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions 73 | 74 | --- 75 | 76 | _Last updated: October 2025_ 77 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/BottomOption/NewContainerButton.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct NewContainerButton: View { 5 | @State private var isHovering = false 6 | @State private var isPopoverOpen = false 7 | @State private var name = "" 8 | @State private var emoji = "" 9 | @State private var isEmojiPickerOpen = false 10 | @FocusState private var isTextFieldFocused: Bool 11 | 12 | @Environment(\.theme) private var theme 13 | @EnvironmentObject var tabManager: TabManager 14 | 15 | var body: some View { 16 | Button(action: { 17 | isPopoverOpen.toggle() 18 | }) { 19 | HStack { 20 | Image(systemName: "plus") 21 | .frame(width: 12, height: 12) 22 | .foregroundColor(.secondary) 23 | } 24 | .padding(8) 25 | .background(isHovering ? theme.invertedSolidWindowBackgroundColor.opacity(0.3) : .clear) 26 | .cornerRadius(8) 27 | } 28 | .buttonStyle(.plain) 29 | .onHover { isHovering = $0 } 30 | .popover(isPresented: $isPopoverOpen) { 31 | VStack(alignment: .leading, spacing: 10) { 32 | Text("New Container") 33 | .font(.headline) 34 | 35 | ContainerForm( 36 | name: $name, 37 | emoji: $emoji, 38 | isEmojiPickerOpen: $isEmojiPickerOpen, 39 | isTextFieldFocused: $isTextFieldFocused, 40 | onSubmit: createContainer, 41 | defaultEmoji: ContainerConstants.defaultEmoji 42 | ) 43 | 44 | Button("Create") { 45 | createContainer() 46 | } 47 | .disabled(name.isEmpty) 48 | } 49 | .frame(width: ContainerConstants.UI.popoverWidth) 50 | .padding() 51 | } 52 | } 53 | 54 | private func createContainer() { 55 | guard !name.isEmpty else { return } 56 | 57 | let finalEmoji = emoji.isEmpty ? ContainerConstants.defaultEmoji : emoji 58 | tabManager.createContainer(name: name, emoji: finalEmoji) 59 | isPopoverOpen = false 60 | resetForm() 61 | } 62 | 63 | private func resetForm() { 64 | name = "" 65 | emoji = "" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ora/Services/SectionDropDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct SectionDropDelegate: DropDelegate { 5 | let items: [Tab] 6 | @Binding var draggedItem: UUID? 7 | let targetSection: TabSection 8 | let tabManager: TabManager 9 | 10 | func dropEntered(info: DropInfo) { 11 | guard let provider = info.itemProviders(for: [.text]).first else { return } 12 | performHapticFeedback(pattern: .alignment) 13 | 14 | provider.loadObject(ofClass: NSString.self) { object, _ in 15 | guard 16 | let string = object as? String, 17 | let uuid = UUID(uuidString: string) 18 | else { return } 19 | 20 | DispatchQueue.main.async { 21 | guard let container = self.items.first?.container ?? self.tabManager.activeContainer, 22 | let from = container.tabs.first(where: { $0.id == uuid }) 23 | else { return } 24 | 25 | if self.items.isEmpty { 26 | // Section is empty, just change type and order 27 | let newType = tabType(for: self.targetSection) 28 | from.type = newType 29 | // Update savedURL when moving into pinned/fav; clear when moving to normal 30 | switch newType { 31 | case .pinned, .fav: 32 | from.savedURL = from.url 33 | case .normal: 34 | from.savedURL = nil 35 | } 36 | let maxOrder = container.tabs.max(by: { $0.order < $1.order })?.order ?? 0 37 | from.order = maxOrder + 1 38 | try? self.tabManager.modelContext.save() 39 | } 40 | // else if let to = self.items.last { 41 | // if isInSameSection(from: from, to: to) { 42 | // withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { 43 | // container.reorderTabs(from: from, to: to) 44 | // } 45 | // } else { 46 | // moveTabBetweenSections(from: from, to: to) 47 | // } 48 | // } 49 | } 50 | } 51 | } 52 | 53 | func dropUpdated(info: DropInfo) -> DropProposal? { 54 | DropProposal(operation: .move) 55 | } 56 | 57 | func performDrop(info: DropInfo) -> Bool { 58 | draggedItem = nil 59 | return true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo.imageset/opeai-black-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/TabList/PinnedTabsList.swift: -------------------------------------------------------------------------------- 1 | import SwiftData 2 | import SwiftUI 3 | 4 | struct PinnedTabsList: View { 5 | let tabs: [Tab] 6 | @Binding var draggedItem: UUID? 7 | let onDrag: (UUID) -> NSItemProvider 8 | let onSelect: (Tab) -> Void 9 | let onPinToggle: (Tab) -> Void 10 | let onFavoriteToggle: (Tab) -> Void 11 | let onClose: (Tab) -> Void 12 | let onDuplicate: (Tab) -> Void 13 | let onMoveToContainer: (Tab, TabContainer) -> Void 14 | let containers: [TabContainer] 15 | @EnvironmentObject var tabManager: TabManager 16 | @Environment(\.theme) var theme 17 | 18 | var body: some View { 19 | VStack(spacing: 8) { 20 | Text("Pinned") 21 | .font(.callout) 22 | .foregroundColor(theme.mutedForeground) 23 | .padding(.top, 8) 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | if tabs.isEmpty { 26 | EmptyPinnedTabs() 27 | } else { 28 | ForEach(tabs) { tab in 29 | TabItem( 30 | tab: tab, 31 | isSelected: tabManager.isActive(tab), 32 | isDragging: draggedItem == tab.id, 33 | onTap: { onSelect(tab) }, 34 | onPinToggle: { onPinToggle(tab) }, 35 | onFavoriteToggle: { onFavoriteToggle(tab) }, 36 | onClose: { onClose(tab) }, 37 | onDuplicate: { onDuplicate(tab) }, 38 | onMoveToContainer: { onMoveToContainer(tab, $0) }, 39 | availableContainers: containers 40 | ) 41 | .onDrag { onDrag(tab.id) } 42 | .onDrop( 43 | of: [.text], 44 | delegate: TabDropDelegate( 45 | item: tab, 46 | draggedItem: $draggedItem, 47 | targetSection: .pinned 48 | ) 49 | ) 50 | } 51 | } 52 | } 53 | .frame(maxWidth: .infinity, alignment: .leading) 54 | .onDrop( 55 | of: [.text], 56 | delegate: SectionDropDelegate( 57 | items: tabs, 58 | draggedItem: $draggedItem, 59 | targetSection: .pinned, 60 | tabManager: tabManager 61 | ) 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo-inverted.imageset/opeai-black-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo.imageset/openai-white-capsule-logo 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Capsule.xcassets/openai-capsule-logo-inverted.imageset/openai-white-capsule-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ora/Modules/Browser/BrowserWebContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BrowserWebContentView: View { 4 | @Environment(\.theme) var theme 5 | @EnvironmentObject var tabManager: TabManager 6 | @EnvironmentObject private var appState: AppState 7 | @EnvironmentObject private var toolbarManager: ToolbarManager 8 | let tab: Tab 9 | 10 | var body: some View { 11 | VStack(alignment: .leading, spacing: 0) { 12 | if !toolbarManager.isToolbarHidden { 13 | URLBar( 14 | onSidebarToggle: { 15 | NotificationCenter.default.post( 16 | name: .toggleSidebar, object: nil 17 | ) 18 | } 19 | ) 20 | .transition( 21 | .asymmetric( 22 | insertion: .push(from: .top), 23 | removal: .push(from: .bottom) 24 | ) 25 | ) 26 | } 27 | 28 | if tab.isWebViewReady { 29 | if tab.hasNavigationError, let error = tab.navigationError { 30 | StatusPageView( 31 | error: error, 32 | failedURL: tab.failedURL, 33 | onRetry: { tab.retryNavigation() }, 34 | onGoBack: tab.webView.canGoBack 35 | ? { 36 | tab.webView.goBack() 37 | tab.clearNavigationError() 38 | } : nil 39 | ) 40 | .id(tab.id) 41 | } else { 42 | ZStack(alignment: .topTrailing) { 43 | WebView(webView: tab.webView).id(tab.id) 44 | 45 | if appState.showFinderIn == tab.id { 46 | FindView(webView: tab.webView) 47 | .padding(.top, 16) 48 | .padding(.trailing, 16) 49 | .zIndex(1000) 50 | } 51 | 52 | if let hovered = tab.hoveredLinkURL, !hovered.isEmpty { 53 | LinkPreview(text: hovered) 54 | } 55 | } 56 | } 57 | } else { 58 | ZStack { 59 | Rectangle().fill(theme.background) 60 | ProgressView().frame(width: 32, height: 32) 61 | } 62 | .frame(maxWidth: .infinity, maxHeight: .infinity) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ora/Services/KeyModifierListener.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /** 4 | * KeyModifierListener is an ObservableObject that monitors keyboard modifier key changes and global key down events. 5 | * 6 | * It publishes the current modifier flags via the `modifierFlags` property, which updates whenever modifier keys are pressed or released. 7 | * 8 | * Additionally, it allows registering custom handlers for key down events using `registerKeyDownHandler`. 9 | * If any registered handler returns true, the event is consumed and not propagated further. 10 | * 11 | * 12 | * Why not use onKeyPressed? 13 | * 14 | * SwiftUI's .onKeyPress modifier is attached to specific views and only triggers when that view has keyboard focus. 15 | * In contrast, KeyModifierListener uses NSEvent monitors to capture modifier flag changes and key down events 16 | * globally across the entire application, 17 | * regardless of focus. This enables app-wide keyboard shortcuts and consistent modifier state tracking. 18 | * 19 | * Also, it's not possible to use onKeyPressed to detect modifier key changes like if modifier is released. 20 | * 21 | * Usage: 22 | * let listener = KeyModifierListener() 23 | * @StateObject var keyListener = listener // In a SwiftUI View 24 | * 25 | * listener.registerKeyDownHandler { event in 26 | * if event.modifierFlags.contains(.command) && event.keyCode == 12 { // Command + Q 27 | * print("Command + Q pressed") 28 | * return true // Consume the event 29 | * } 30 | * return false 31 | * } 32 | */ 33 | 34 | final class KeyModifierListener: ObservableObject { 35 | @Published var modifierFlags = NSEvent.ModifierFlags([]) 36 | 37 | init() { 38 | NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in 39 | self?.modifierFlags = event.modifierFlags 40 | return event 41 | } 42 | 43 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in 44 | guard let self else { return event } 45 | if self.handleGlobalKeyDown(event) { 46 | return nil 47 | } 48 | return event 49 | } 50 | } 51 | 52 | typealias KeyDownHandler = (NSEvent) -> Bool 53 | 54 | private var keyDownHandlers: [KeyDownHandler] = [] 55 | 56 | func registerKeyDownHandler(_ handler: @escaping KeyDownHandler) { 57 | keyDownHandlers.append(handler) 58 | } 59 | 60 | private func handleGlobalKeyDown(_ event: NSEvent) -> Bool { 61 | for handler in keyDownHandlers { 62 | if handler(event) { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ora/Services/PrivacyService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | 4 | enum CookiesPolicy: String, CaseIterable, Identifiable, Codable { 5 | case allowAll = "Allow all" 6 | case blockThirdParty = "Block third-party" 7 | case blockAll = "Block all" 8 | var id: String { rawValue } 9 | } 10 | 11 | class PrivacyService { 12 | private static func clearData(_ container: TabContainer, _ types: Set, _ completion: (() -> Void)?) { 13 | let dataStore = WKWebsiteDataStore(forIdentifier: container.id) 14 | dataStore 15 | .removeData( 16 | ofTypes: types, 17 | modifiedSince: .distantPast 18 | ) { 19 | completion?() 20 | } 21 | } 22 | 23 | static func clearCookies(_ container: TabContainer, completion: (() -> Void)? = nil) { 24 | let types: Set = [WKWebsiteDataTypeCookies] 25 | self.clearData( 26 | container, 27 | types, 28 | completion 29 | ) 30 | } 31 | 32 | static func clearCache(_ container: TabContainer, completion: (() -> Void)? = nil) { 33 | let types: Set = WKWebsiteDataStore.allWebsiteDataTypes() 34 | self.clearData( 35 | container, 36 | types, 37 | completion 38 | ) 39 | } 40 | 41 | static func clearCookiesForHost(for host: String, container: TabContainer, completion: (() -> Void)? = nil) { 42 | let dataStore = WKWebsiteDataStore(forIdentifier: container.id) 43 | let types: Set = [WKWebsiteDataTypeCookies] 44 | dataStore.fetchDataRecords(ofTypes: types) { records in 45 | let targetRecords = records.filter { $0.displayName.contains(host) } 46 | guard !targetRecords.isEmpty else { 47 | completion?() 48 | return 49 | } 50 | 51 | dataStore.removeData(ofTypes: types, for: targetRecords) { 52 | completion?() 53 | } 54 | } 55 | } 56 | 57 | static func clearCacheForHost(for host: String, container: TabContainer, completion: (() -> Void)? = nil) { 58 | let dataStore = WKWebsiteDataStore(forIdentifier: container.id) 59 | let types: Set = WKWebsiteDataStore.allWebsiteDataTypes() 60 | 61 | dataStore.fetchDataRecords(ofTypes: types) { records in 62 | let targetRecords = records.filter { $0.displayName.contains(host) } 63 | guard !targetRecords.isEmpty else { 64 | completion?() 65 | return 66 | } 67 | 68 | dataStore.removeData(ofTypes: types, for: targetRecords) { 69 | completion?() 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ora/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | Web URL 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Owner 16 | LSItemContentTypes 17 | 18 | public.url 19 | 20 | 21 | 22 | CFBundleTypeName 23 | HTML document 24 | CFBundleTypeRole 25 | Viewer 26 | LSHandlerRank 27 | Default 28 | LSItemContentTypes 29 | 30 | public.html 31 | 32 | 33 | 34 | CFBundleTypeName 35 | XHTML document 36 | CFBundleTypeRole 37 | Viewer 38 | LSHandlerRank 39 | Default 40 | LSItemContentTypes 41 | 42 | public.xhtml 43 | 44 | 45 | 46 | CFBundleExecutable 47 | $(EXECUTABLE_NAME) 48 | CFBundleIdentifier 49 | $(PRODUCT_BUNDLE_IDENTIFIER) 50 | CFBundleInfoDictionaryVersion 51 | 6.0 52 | CFBundleName 53 | $(PRODUCT_NAME) 54 | CFBundlePackageType 55 | APPL 56 | CFBundleShortVersionString 57 | 1.0 58 | CFBundleURLTypes 59 | 60 | 61 | CFBundleTypeRole 62 | Editor 63 | CFBundleURLName 64 | Ora Browser 65 | CFBundleURLSchemes 66 | 67 | http 68 | https 69 | 70 | LSHandlerRank 71 | Owner 72 | 73 | 74 | CFBundleVersion 75 | 1 76 | LSApplicationCategoryType 77 | public.app-category.web-browser 78 | LSMinimumSystemVersion 79 | 13.0 80 | NSUserActivityTypes 81 | 82 | NSUserActivityTypeBrowsingWeb 83 | 84 | SUEnableAutomaticChecks 85 | 86 | SUFeedURL 87 | https://the-ora.github.io/browser/appcast.xml 88 | SUPublicEDKey 89 | Ozj+rezzbJAD76RfajtfQ7rFojJbpFSCl/0DcFSBCTI= 90 | 91 | 92 | -------------------------------------------------------------------------------- /ora/Services/TabDropDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | extension Array where Element: Hashable { 5 | func unique() -> [Element] { 6 | var seen = Set() 7 | return filter { seen.insert($0).inserted } 8 | } 9 | } 10 | 11 | struct TabDropDelegate: DropDelegate { 12 | let item: Tab // to 13 | @Binding var draggedItem: UUID? 14 | 15 | let targetSection: TabSection 16 | 17 | func dropEntered(info: DropInfo) { 18 | guard let provider = info.itemProviders(for: [.text]).first else { return } 19 | performHapticFeedback(pattern: .alignment) 20 | provider.loadObject(ofClass: NSString.self) { object, _ in 21 | if let string = object as? String, 22 | let uuid = UUID(uuidString: string) 23 | { 24 | DispatchQueue.main.async { 25 | // First try to find the tab in the target container 26 | var from = self.item.container.tabs.first(where: { $0.id == uuid }) 27 | 28 | // If not found, try to find it in all containers of the same type 29 | if from == nil { 30 | // Look through all tabs in all containers to find the dragged tab 31 | for container in self.item.container.tabs.compactMap(\.container).unique() { 32 | if let foundTab = container.tabs.first(where: { $0.id == uuid }) { 33 | from = foundTab 34 | break 35 | } 36 | } 37 | } 38 | 39 | guard let from else { return } 40 | 41 | if isInSameSection( 42 | from: from, 43 | to: self.item 44 | ) { 45 | withAnimation( 46 | .spring( 47 | response: 0.3, 48 | dampingFraction: 0.8 49 | ) 50 | ) { 51 | self.item.container 52 | .reorderTabs( 53 | from: from, 54 | to: self.item 55 | ) 56 | } 57 | } else { 58 | moveTabBetweenSections(from: from, to: self.item) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | func dropUpdated(info: DropInfo) -> DropProposal? { 66 | .init(operation: .move) 67 | } 68 | 69 | func performDrop(info: DropInfo) -> Bool { 70 | draggedItem = nil 71 | return true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/TabList/FavTabsList.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct FavTabsGrid: View { 5 | @Environment(\.theme) var theme 6 | @EnvironmentObject var tabManager: TabManager 7 | let tabs: [Tab] 8 | @Binding var draggedItem: UUID? 9 | let onDrag: (UUID) -> NSItemProvider 10 | let selectedContainerId: String 11 | let onSelect: (Tab) -> Void 12 | let onFavoriteToggle: (Tab) -> Void 13 | let onClose: (Tab) -> Void 14 | let onDuplicate: (Tab) -> Void 15 | let onMoveToContainer: 16 | ( 17 | Tab, 18 | TabContainer 19 | ) -> Void 20 | 21 | private var adaptiveColumns: [GridItem] { 22 | let maxColumns = 3 23 | let columnCount = min(max(1, tabs.count), maxColumns) 24 | return Array(repeating: GridItem(spacing: 10), count: columnCount) 25 | } 26 | 27 | var body: some View { 28 | LazyVGrid(columns: adaptiveColumns, spacing: 10) { 29 | if tabs.isEmpty { 30 | EmptyFavTabItem() 31 | .onDrop( 32 | of: [.text], 33 | delegate: SectionDropDelegate( 34 | items: tabs, 35 | draggedItem: $draggedItem, 36 | targetSection: .fav, 37 | tabManager: tabManager 38 | ) 39 | ) 40 | } else { 41 | ForEach(tabs) { tab in 42 | FavTabItem( 43 | tab: tab, 44 | isSelected: tabManager.isActive(tab), 45 | isDragging: draggedItem == tab.id, 46 | onTap: { onSelect(tab) }, 47 | onFavoriteToggle: { onFavoriteToggle(tab) }, 48 | onClose: { onClose(tab) }, 49 | onDuplicate: { onDuplicate(tab) }, 50 | onMoveToContainer: { onMoveToContainer(tab, $0) } 51 | ) 52 | .onDrag { onDrag(tab.id) } 53 | .onDrop( 54 | of: [.text], 55 | delegate: TabDropDelegate( 56 | item: tab, 57 | draggedItem: $draggedItem, 58 | targetSection: .fav 59 | ) 60 | ) 61 | } 62 | } 63 | } 64 | .animation(.easeOut(duration: 0.1), value: adaptiveColumns.count) 65 | .onDrop( 66 | of: [.text], 67 | delegate: SectionDropDelegate( 68 | items: tabs, 69 | draggedItem: $draggedItem, 70 | targetSection: .fav, 71 | tabManager: tabManager 72 | ) 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/brew-release.yml: -------------------------------------------------------------------------------- 1 | name: Update Homebrew Cask 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version for testing (e.g., 0.2.5; omit "v" prefix)' 10 | required: false 11 | type: string 12 | default: '' 13 | dmg_url: 14 | description: 'Full DMG download URL for testing (e.g., https://github.com/the-ora/browser/releases/download/v0.2.5/Ora-Browser-0.2.5.dmg)' 15 | required: false 16 | type: string 17 | default: '' 18 | 19 | jobs: 20 | update-cask: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout homebrew-ora repo 24 | uses: actions/checkout@v4 25 | with: 26 | repository: the-ora/homebrew-ora 27 | token: ${{ secrets.HOMEBREW_SECRET }} 28 | ref: main 29 | 30 | - name: Download Ora.dmg and set version 31 | run: | 32 | EVENT_NAME="${{ github.event_name }}" 33 | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then 34 | if [ -n "${{ github.event.inputs.dmg_url }}" ]; then 35 | DOWNLOAD_URL="${{ github.event.inputs.dmg_url }}" 36 | else 37 | echo "DMG URL is required for manual dispatch" 38 | exit 1 39 | fi 40 | if [ -n "${{ github.event.inputs.version }}" ]; then 41 | STRIPPED_VERSION="${{ github.event.inputs.version }}" 42 | else 43 | echo "Version is required for manual dispatch" 44 | exit 1 45 | fi 46 | else 47 | VERSION="${{ github.event.release.tag_name }}" 48 | STRIPPED_VERSION=$(echo "$VERSION" | sed 's/^v//') 49 | DOWNLOAD_URL="https://github.com/the-ora/browser/releases/download/${VERSION}/Ora-Browser-${STRIPPED_VERSION}.dmg" 50 | fi 51 | echo "STRIPPED_VERSION=$STRIPPED_VERSION" >> $GITHUB_ENV 52 | curl -L --fail -o Ora.dmg "$DOWNLOAD_URL" 53 | 54 | - name: Compute SHA256 55 | run: | 56 | SHA256=$(shasum -a 256 Ora.dmg | awk '{print $1}') 57 | echo "SHA256=$SHA256" >> $GITHUB_ENV 58 | 59 | - name: Update ora.rb 60 | run: | 61 | sed -i 's/version ".*"/version "${{ env.STRIPPED_VERSION }}"/g' Casks/ora.rb 62 | sed -i 's/sha256 ".*"/sha256 "${{ env.SHA256 }}"/g' Casks/ora.rb 63 | 64 | - name: Commit and push changes 65 | run: | 66 | git config user.name "GitHub Actions" 67 | git config user.email "actions@github.com" 68 | git add Casks/ora.rb 69 | if git diff --staged --quiet; then 70 | echo "No changes to commit" 71 | else 72 | git commit -m "Update Ora cask to ${{ env.STRIPPED_VERSION }} (SHA256: ${{ env.SHA256 }})" 73 | git push 74 | fi -------------------------------------------------------------------------------- /ora/Common/Representables/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | struct WindowAccessor: NSViewRepresentable { 5 | @Binding var isFullscreen: Bool 6 | 7 | func makeCoordinator() -> Coordinator { 8 | Coordinator(self) 9 | } 10 | 11 | class Coordinator { 12 | var parent: WindowAccessor 13 | var observers: [Any] = [] 14 | 15 | init(_ parent: WindowAccessor) { 16 | self.parent = parent 17 | } 18 | 19 | @objc func willEnterFullScreenNotification(_ notification: Notification) { 20 | guard let window = notification.object as? NSWindow else { return } 21 | parent.isFullscreen = true 22 | parent.updateTrafficLights(for: window) 23 | } 24 | 25 | @objc func willExitFullScreenNotification(_ notification: Notification) { 26 | guard let window = notification.object as? NSWindow else { return } 27 | parent.isFullscreen = false 28 | parent.updateTrafficLights(for: window) 29 | } 30 | } 31 | 32 | func makeNSView(context: Context) -> NSView { 33 | let view = NSView() 34 | 35 | DispatchQueue.main.async { 36 | guard let window = view.window else { return } 37 | isFullscreen = window.styleMask.contains(.fullScreen) 38 | 39 | let coordinator = context.coordinator 40 | 41 | let enterObserver = NotificationCenter.default.addObserver( 42 | forName: NSWindow.willEnterFullScreenNotification, 43 | object: window, 44 | queue: nil, 45 | using: coordinator.willEnterFullScreenNotification 46 | ) 47 | 48 | let exitObserver = NotificationCenter.default.addObserver( 49 | forName: NSWindow.willExitFullScreenNotification, 50 | object: window, 51 | queue: nil, 52 | using: coordinator.willExitFullScreenNotification 53 | ) 54 | 55 | coordinator.observers = [enterObserver, exitObserver] 56 | updateTrafficLights(for: window) 57 | } 58 | 59 | return view 60 | } 61 | 62 | func updateNSView(_ nsView: NSView, context: Context) { 63 | guard let window = nsView.window else { return } 64 | updateTrafficLights(for: window) 65 | } 66 | 67 | func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { 68 | for observer in coordinator.observers { 69 | NotificationCenter.default.removeObserver(observer) 70 | } 71 | } 72 | 73 | private func updateTrafficLights(for window: NSWindow) { 74 | for type in [ 75 | NSWindow.ButtonType.closeButton, 76 | .miniaturizeButton, 77 | .zoomButton 78 | ] { 79 | guard let button = window.standardWindowButton(type) else { continue } 80 | button.animator().isHidden = !isFullscreen 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ora/Icons/OraIconDev.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "automatic-gradient" : "display-p3:0.20593,0.42358,0.93652,1.00000" 4 | }, 5 | "groups" : [ 6 | { 7 | "blur-material" : 0.5, 8 | "layers" : [ 9 | { 10 | "image-name" : "momento.svg", 11 | "name" : "momento", 12 | "opacity" : 0.5, 13 | "position" : { 14 | "scale" : 0.98, 15 | "translation-in-points" : [ 16 | 0, 17 | 0 18 | ] 19 | } 20 | }, 21 | { 22 | "image-name" : "OraLogo.svg", 23 | "name" : "OraLogo", 24 | "opacity" : 1, 25 | "position" : { 26 | "scale" : 0.88, 27 | "translation-in-points" : [ 28 | 0, 29 | 0 30 | ] 31 | } 32 | } 33 | ], 34 | "position" : { 35 | "scale" : 1, 36 | "translation-in-points" : [ 37 | 0, 38 | 0 39 | ] 40 | }, 41 | "shadow" : { 42 | "kind" : "neutral", 43 | "opacity" : 0.5 44 | }, 45 | "translucency" : { 46 | "enabled" : true, 47 | "value" : 0.5 48 | } 49 | }, 50 | { 51 | "blur-material-specializations" : [ 52 | { 53 | "appearance" : "tinted", 54 | "value" : 1 55 | } 56 | ], 57 | "layers" : [ 58 | { 59 | "fill" : { 60 | "solid" : "srgb:1.00000,1.00000,1.00000,1.00000" 61 | }, 62 | "image-name" : "Grid.png", 63 | "name" : "Grid", 64 | "opacity" : 0.6, 65 | "position" : { 66 | "scale" : 0.5, 67 | "translation-in-points" : [ 68 | 0, 69 | 0 70 | ] 71 | } 72 | } 73 | ], 74 | "opacity-specializations" : [ 75 | { 76 | "appearance" : "tinted", 77 | "value" : 0.08 78 | } 79 | ], 80 | "position" : { 81 | "scale" : 1, 82 | "translation-in-points" : [ 83 | 0, 84 | 0 85 | ] 86 | }, 87 | "shadow-specializations" : [ 88 | { 89 | "appearance" : "tinted", 90 | "value" : { 91 | "kind" : "none", 92 | "opacity" : 0.5 93 | } 94 | } 95 | ], 96 | "specular-specializations" : [ 97 | { 98 | "appearance" : "tinted", 99 | "value" : false 100 | } 101 | ], 102 | "translucency-specializations" : [ 103 | { 104 | "appearance" : "tinted", 105 | "value" : { 106 | "enabled" : true, 107 | "value" : 0.5 108 | } 109 | } 110 | ] 111 | } 112 | ], 113 | "supported-platforms" : { 114 | "circles" : [ 115 | "watchOS" 116 | ], 117 | "squares" : "shared" 118 | } 119 | } -------------------------------------------------------------------------------- /ora/Models/Download.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | enum DownloadStatus: String, Codable { 5 | case pending 6 | case downloading 7 | case completed 8 | case failed 9 | case cancelled 10 | } 11 | 12 | @Model 13 | class Download: ObservableObject, Identifiable { 14 | var id: UUID 15 | var originalURL: URL 16 | var originalURLString: String 17 | var fileName: String 18 | var fileSize: Int64 19 | var downloadedBytes: Int64 20 | var status: DownloadStatus 21 | var progress: Double 22 | var destinationURL: URL? 23 | var createdAt: Date 24 | var completedAt: Date? 25 | var error: String? 26 | 27 | @Transient @Published var isActive: Bool = false 28 | @Transient @Published var displayProgress: Double = 0.0 29 | @Transient @Published var displayDownloadedBytes: Int64 = 0 30 | @Transient @Published var displayFileSize: Int64 = 0 31 | 32 | init( 33 | id: UUID = UUID(), 34 | originalURL: URL, 35 | fileName: String, 36 | fileSize: Int64 = 0 37 | ) { 38 | self.id = id 39 | self.originalURL = originalURL 40 | self.originalURLString = originalURL.absoluteString 41 | self.fileName = fileName 42 | self.fileSize = fileSize 43 | self.downloadedBytes = 0 44 | self.status = .pending 45 | self.progress = 0.0 46 | self.createdAt = Date() 47 | 48 | // Initialize published properties 49 | self.displayFileSize = fileSize 50 | self.displayDownloadedBytes = 0 51 | self.displayProgress = 0.0 52 | } 53 | 54 | func updateProgress(downloadedBytes: Int64, totalBytes: Int64) { 55 | self.downloadedBytes = downloadedBytes 56 | self.fileSize = totalBytes 57 | self.progress = totalBytes > 0 ? Double(downloadedBytes) / Double(totalBytes) : 0.0 58 | 59 | // Update published properties for UI 60 | DispatchQueue.main.async { 61 | self.displayDownloadedBytes = downloadedBytes 62 | self.displayFileSize = totalBytes > 0 ? totalBytes : self.fileSize 63 | self.displayProgress = self.progress 64 | } 65 | } 66 | 67 | func markCompleted(destinationURL: URL) { 68 | self.status = .completed 69 | self.progress = 1.0 70 | self.destinationURL = destinationURL 71 | self.completedAt = Date() 72 | self.isActive = false 73 | } 74 | 75 | func markFailed(error: String) { 76 | self.status = .failed 77 | self.error = error 78 | self.isActive = false 79 | } 80 | 81 | func markCancelled() { 82 | self.status = .cancelled 83 | self.isActive = false 84 | } 85 | 86 | var formattedFileSize: String { 87 | return ByteCountFormatter.string( 88 | fromByteCount: displayFileSize > 0 ? displayFileSize : fileSize, 89 | countStyle: .file 90 | ) 91 | } 92 | 93 | var formattedDownloadedSize: String { 94 | return ByteCountFormatter.string(fromByteCount: displayDownloadedBytes, countStyle: .file) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ora/Icons/OraIcon.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : "system-light", 3 | "groups" : [ 4 | { 5 | "blur-material" : null, 6 | "layers" : [ 7 | { 8 | "fill-specializations" : [ 9 | { 10 | "value" : { 11 | "solid" : "display-p3:0.00000,0.00000,0.00000,1.00000" 12 | } 13 | }, 14 | { 15 | "appearance" : "dark", 16 | "value" : { 17 | "solid" : "extended-gray:1.00000,1.00000" 18 | } 19 | }, 20 | { 21 | "appearance" : "tinted", 22 | "value" : { 23 | "solid" : "extended-gray:1.00000,1.00000" 24 | } 25 | } 26 | ], 27 | "image-name" : "OraLogo.svg", 28 | "name" : "OraLogo", 29 | "opacity-specializations" : [ 30 | { 31 | "appearance" : "tinted", 32 | "value" : 1 33 | } 34 | ], 35 | "position" : { 36 | "scale" : 0.88, 37 | "translation-in-points" : [ 38 | 0, 39 | 0 40 | ] 41 | } 42 | } 43 | ], 44 | "shadow" : { 45 | "kind" : "neutral", 46 | "opacity" : 0.5 47 | }, 48 | "translucency" : { 49 | "enabled" : true, 50 | "value" : 0.5 51 | } 52 | }, 53 | { 54 | "blur-material" : null, 55 | "layers" : [ 56 | { 57 | "fill-specializations" : [ 58 | { 59 | "value" : { 60 | "solid" : "srgb:0.00000,0.00000,0.00000,1.00000" 61 | } 62 | }, 63 | { 64 | "appearance" : "dark", 65 | "value" : { 66 | "solid" : "srgb:1.00000,1.00000,1.00000,1.00000" 67 | } 68 | }, 69 | { 70 | "appearance" : "tinted", 71 | "value" : { 72 | "solid" : "srgb:1.00000,1.00000,1.00000,1.00000" 73 | } 74 | } 75 | ], 76 | "image-name" : "momento.svg", 77 | "name" : "momento", 78 | "opacity-specializations" : [ 79 | { 80 | "value" : 0.2 81 | }, 82 | { 83 | "appearance" : "dark", 84 | "value" : 0.05 85 | } 86 | ], 87 | "position" : { 88 | "scale" : 0.93, 89 | "translation-in-points" : [ 90 | 0, 91 | 0 92 | ] 93 | } 94 | } 95 | ], 96 | "shadow" : { 97 | "kind" : "neutral", 98 | "opacity" : 0.5 99 | }, 100 | "specular" : true, 101 | "translucency" : { 102 | "enabled" : true, 103 | "value" : 0.5 104 | } 105 | } 106 | ], 107 | "supported-platforms" : { 108 | "circles" : [ 109 | "watchOS" 110 | ], 111 | "squares" : "shared" 112 | } 113 | } -------------------------------------------------------------------------------- /ora/Modules/Sidebar/TabList/NormalTabsList.swift: -------------------------------------------------------------------------------- 1 | import SwiftData 2 | import SwiftUI 3 | 4 | struct NormalTabsList: View { 5 | let tabs: [Tab] 6 | @Binding var draggedItem: UUID? 7 | let onDrag: (UUID) -> NSItemProvider 8 | let onSelect: (Tab) -> Void 9 | let onPinToggle: (Tab) -> Void 10 | let onFavoriteToggle: (Tab) -> Void 11 | let onClose: (Tab) -> Void 12 | let onDuplicate: (Tab) -> Void 13 | let onMoveToContainer: 14 | ( 15 | Tab, 16 | TabContainer 17 | ) -> Void 18 | let onAddNewTab: () -> Void 19 | @Query var containers: [TabContainer] 20 | @EnvironmentObject var tabManager: TabManager 21 | @State private var previousTabIds: [UUID] = [] 22 | 23 | var body: some View { 24 | VStack(spacing: 8) { 25 | NewTabButton(addNewTab: onAddNewTab) 26 | ForEach(tabs) { tab in 27 | TabItem( 28 | tab: tab, 29 | isSelected: tabManager.isActive(tab), 30 | isDragging: draggedItem == tab.id, 31 | onTap: { onSelect(tab) }, 32 | onPinToggle: { onPinToggle(tab) }, 33 | onFavoriteToggle: { onFavoriteToggle(tab) }, 34 | onClose: { onClose(tab) }, 35 | onDuplicate: { onDuplicate(tab) }, 36 | onMoveToContainer: { onMoveToContainer(tab, $0) }, 37 | availableContainers: containers 38 | ) 39 | .onDrag { onDrag(tab.id) } 40 | .onDrop( 41 | of: [.text], 42 | delegate: TabDropDelegate( 43 | item: tab, 44 | draggedItem: $draggedItem, 45 | targetSection: .normal 46 | ) 47 | ) 48 | .transition(.asymmetric( 49 | insertion: .opacity.combined(with: .move(edge: .bottom)), 50 | removal: .opacity.combined(with: .move(edge: .top)) 51 | )) 52 | .animation(.spring(response: 0.3, dampingFraction: 0.8), value: shouldAnimate(tab)) 53 | } 54 | } 55 | .onDrop( 56 | of: [.text], 57 | delegate: SectionDropDelegate( 58 | items: tabs, 59 | draggedItem: $draggedItem, 60 | targetSection: .normal, 61 | tabManager: tabManager 62 | ) 63 | ) 64 | .onAppear { 65 | previousTabIds = tabs.map(\.id) 66 | } 67 | .onChange(of: tabs.map(\.id)) { newTabIds in 68 | previousTabIds = newTabIds 69 | } 70 | } 71 | 72 | private func shouldAnimate(_ tab: Tab) -> Bool { 73 | // Only animate if the tab's position has actually changed 74 | guard let currentIndex = tabs.firstIndex(where: { $0.id == tab.id }), 75 | let previousIndex = previousTabIds.firstIndex(where: { $0 == tab.id }) 76 | else { 77 | return true // Animate new tabs or tabs that were just created 78 | } 79 | return currentIndex != previousIndex 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/DownloadsWidget.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DownloadsWidget: View { 4 | @EnvironmentObject var downloadManager: DownloadManager 5 | @Environment(\.theme) private var theme 6 | @State private var isHovered = false 7 | 8 | var body: some View { 9 | VStack(spacing: 0) { 10 | // Active downloads 11 | if !downloadManager.activeDownloads.isEmpty { 12 | VStack(spacing: 6) { 13 | ForEach(downloadManager.activeDownloads) { download in 14 | DownloadProgressView(download: download) { 15 | downloadManager.cancelDownload(download) 16 | } 17 | } 18 | } 19 | .padding(.bottom, 8) 20 | } 21 | 22 | // Downloads button 23 | Button(action: { 24 | downloadManager.isDownloadsPopoverOpen.toggle() 25 | }) { 26 | HStack(spacing: 8) { 27 | Image(systemName: "arrow.down") 28 | .foregroundColor(downloadButtonColor) 29 | .frame(width: 12, height: 12) 30 | 31 | // Text("Downloads") 32 | // .font(.system(size: 13, weight: .medium)) 33 | // .foregroundColor(theme.foreground) 34 | 35 | // Spacer() 36 | 37 | // if !downloadManager.recentDownloads.isEmpty { 38 | // Text("\(downloadManager.recentDownloads.count)") 39 | // .font(.system(size: 11, weight: .medium)) 40 | // .foregroundColor(.secondary) 41 | // .padding(.horizontal, 6) 42 | // .padding(.vertical, 2) 43 | // .background(theme.background.opacity(0.6)) 44 | // .cornerRadius(8) 45 | // } 46 | 47 | // Image(systemName: downloadManager.isDownloadsPopoverOpen ? "chevron.up" : "chevron.down") 48 | // .foregroundColor(.secondary) 49 | // .frame(width: 12, height: 12) 50 | } 51 | .padding(8) 52 | .background(isHovered ? theme.invertedSolidWindowBackgroundColor.opacity(0.3) : .clear) 53 | .cornerRadius(8) 54 | } 55 | .buttonStyle(.plain) 56 | .onHover { hovering in 57 | withAnimation(.easeOut(duration: 0.15)) { 58 | isHovered = hovering 59 | } 60 | } 61 | .popover(isPresented: $downloadManager.isDownloadsPopoverOpen, arrowEdge: .bottom) { 62 | DownloadsListView() 63 | .environmentObject(downloadManager) 64 | } 65 | } 66 | } 67 | 68 | private var downloadButtonColor: Color { 69 | if !downloadManager.activeDownloads.isEmpty { 70 | return .blue 71 | } else if downloadManager.recentDownloads.contains(where: { $0.status == .completed }) { 72 | return .green 73 | } else { 74 | return .secondary 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ora/Modules/Sidebar/BottomOption/ContainerForm.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContainerForm: View { 4 | @Binding var name: String 5 | @Binding var emoji: String 6 | @Binding var isEmojiPickerOpen: Bool 7 | @FocusState.Binding var isTextFieldFocused: Bool 8 | 9 | let onSubmit: () -> Void 10 | let defaultEmoji: String 11 | 12 | @Environment(\.theme) private var theme 13 | @State private var isEmojiPickerHovering = false 14 | 15 | var body: some View { 16 | HStack(spacing: 8) { 17 | emojiPickerButton 18 | nameTextField 19 | } 20 | } 21 | 22 | private var emojiPickerButton: some View { 23 | Button(action: { 24 | isEmojiPickerOpen.toggle() 25 | }) { 26 | ZStack { 27 | RoundedRectangle(cornerRadius: ContainerConstants.UI.cornerRadius, style: .continuous) 28 | .stroke( 29 | emoji.isEmpty ? theme.border : theme.border, 30 | style: emoji.isEmpty 31 | ? StrokeStyle(lineWidth: 1, dash: [5]) 32 | : StrokeStyle(lineWidth: 1) 33 | ) 34 | .animation( 35 | .easeOut(duration: ContainerConstants.Animation.emojiPickerDuration), 36 | value: emoji.isEmpty 37 | ) 38 | .background(isEmojiPickerHovering ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2)) 39 | .cornerRadius(ContainerConstants.UI.cornerRadius) 40 | 41 | if emoji.isEmpty { 42 | Image(systemName: "plus") 43 | .font(.system(size: 12)) 44 | } else { 45 | Text(emoji) 46 | .font(.system(size: 12)) 47 | } 48 | } 49 | } 50 | .popover(isPresented: $isEmojiPickerOpen, arrowEdge: .bottom) { 51 | EmojiPickerView(onSelect: { selectedEmoji in 52 | emoji = selectedEmoji 53 | isEmojiPickerOpen = false 54 | }) 55 | } 56 | .frame(width: ContainerConstants.UI.emojiButtonSize, height: ContainerConstants.UI.emojiButtonSize) 57 | .background(isEmojiPickerHovering ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2)) 58 | .cornerRadius(ContainerConstants.UI.cornerRadius) 59 | .buttonStyle(.plain) 60 | .onHover { isEmojiPickerHovering = $0 } 61 | } 62 | 63 | private var nameTextField: some View { 64 | TextField("Name", text: $name) 65 | .textFieldStyle(.plain) 66 | .frame(maxWidth: .infinity) 67 | .padding(8) 68 | .background(Color.gray.opacity(0.1)) 69 | .cornerRadius(ContainerConstants.UI.cornerRadius) 70 | .focused($isTextFieldFocused) 71 | .onSubmit(onSubmit) 72 | .overlay( 73 | RoundedRectangle(cornerRadius: ContainerConstants.UI.cornerRadius, style: .continuous) 74 | .stroke( 75 | isTextFieldFocused ? theme.foreground.opacity(0.5) : theme.border, 76 | lineWidth: isTextFieldFocused ? 2 : 1 77 | ) 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ora/Modules/Launcher/Main/LauncherTextField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LauncherTextField: NSViewRepresentable { 4 | @Binding var text: String 5 | var font: NSFont 6 | let onTab: () -> Void 7 | let onSubmit: () -> Void 8 | let onDelete: () -> Bool 9 | let onMoveUp: () -> Void 10 | let onMoveDown: () -> Void 11 | var cursorColor: Color 12 | var placeholder: String 13 | 14 | class CustomTextField: NSTextField { 15 | var cursorColor: NSColor? 16 | 17 | override func becomeFirstResponder() -> Bool { 18 | let didBecome = super.becomeFirstResponder() 19 | if didBecome, let textView = currentEditor() as? NSTextView, let color = cursorColor { 20 | textView.insertionPointColor = color 21 | } 22 | return didBecome 23 | } 24 | } 25 | 26 | func makeCoordinator() -> Coordinator { 27 | Coordinator(self) 28 | } 29 | 30 | func makeNSView(context: Context) -> CustomTextField { 31 | let textField = CustomTextField() 32 | textField.delegate = context.coordinator 33 | textField.font = font 34 | textField.bezelStyle = .roundedBezel 35 | textField.isBordered = false 36 | textField.focusRingType = .none 37 | textField.drawsBackground = false 38 | textField.placeholderString = placeholder 39 | return textField 40 | } 41 | 42 | func updateNSView(_ nsView: CustomTextField, context: Context) { 43 | nsView.stringValue = text 44 | nsView.cursorColor = NSColor(cursorColor) 45 | nsView.placeholderString = placeholder 46 | if let textView = nsView.currentEditor() as? NSTextView { 47 | textView.insertionPointColor = nsView.cursorColor 48 | } 49 | } 50 | 51 | class Coordinator: NSObject, NSTextFieldDelegate { 52 | var parent: LauncherTextField 53 | 54 | init(_ parent: LauncherTextField) { 55 | self.parent = parent 56 | } 57 | 58 | func controlTextDidChange(_ obj: Notification) { 59 | if let textField = obj.object as? NSTextField { 60 | parent.text = textField.stringValue 61 | } 62 | } 63 | 64 | func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool { 65 | if selector == #selector(NSResponder.insertTab(_:)) { 66 | parent.onTab() 67 | return true 68 | } else if selector == #selector(NSResponder.insertNewline(_:)) { 69 | parent.onSubmit() 70 | return true 71 | } else if selector == #selector(NSResponder.deleteBackward(_:)) { 72 | return parent.onDelete() 73 | } else if selector == #selector(NSResponder.moveUp(_:)) || selector == 74 | #selector(NSResponder.moveToBeginningOfParagraph(_:)) 75 | { 76 | parent.onMoveUp() 77 | return true 78 | } else if selector == #selector(NSResponder.moveDown(_:)) || selector == 79 | #selector(NSResponder.moveToEndOfParagraph(_:)) 80 | { 81 | parent.onMoveDown() 82 | return true 83 | } 84 | 85 | return false 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ora/UI/Toast/ToastView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct Toast: Identifiable { 4 | private(set) var id: String = UUID().uuidString 5 | var content: AnyView 6 | var offsetX: CGFloat = 0 7 | 8 | init(@ViewBuilder content: @escaping (String) -> some View) { 9 | self.content = .init(content(id)) 10 | } 11 | } 12 | 13 | extension View { 14 | @ViewBuilder 15 | func toast(toasts: Binding<[Toast]>) -> some View { 16 | self.frame(maxWidth: .infinity, maxHeight: .infinity) 17 | .overlay(alignment: .bottom) { 18 | ToastsView(toasts: toasts) 19 | } 20 | } 21 | } 22 | 23 | private struct ToastsView: View { 24 | @Binding var toasts: [Toast] 25 | @State private var isExpanded: Bool = false 26 | @Environment(\.theme) private var theme 27 | 28 | var body: some View { 29 | ZStack(alignment: .bottom) { 30 | // Toast stack 31 | VStack(spacing: isExpanded ? 10 : 0) { 32 | ForEach(Array(toasts.enumerated()), id: \.element.id) { index, toast in 33 | toast.content 34 | .fixedSize() 35 | .layoutPriority(1) 36 | .visualEffect { content, _ in 37 | content 38 | .scaleEffect(isExpanded ? 1 : scale(index: index), anchor: .bottom) 39 | .offset(y: isExpanded ? 0 : offsetY(index: index)) 40 | } 41 | .transition( 42 | .asymmetric( 43 | insertion: .offset(y: 100), 44 | removal: .move(edge: .bottom) 45 | ) 46 | ) 47 | } 48 | } 49 | .padding(.bottom, 30) 50 | .onHover { isHovering in 51 | withAnimation(.bouncy) { 52 | isExpanded = isHovering 53 | } 54 | } 55 | } 56 | // Animate when toasts array changes 57 | // .animation(.bouncy, value: $toasts) 58 | } 59 | 60 | private func offsetY(index: Int) -> CGFloat { 61 | let offset = min(CGFloat(index) * 15, 30) 62 | return -offset 63 | } 64 | 65 | private func scale(index: Int) -> CGFloat { 66 | let scale = min(CGFloat(index) * 0.1, 1) 67 | return 1 - scale 68 | } 69 | } 70 | 71 | struct ToastView: View { 72 | var id: String 73 | var message: String 74 | var systemImage: String? = "checkmark.circle.fill" 75 | let action: () -> Void 76 | @Environment(\.theme) private var theme 77 | 78 | var body: some View { 79 | HStack(spacing: 8) { 80 | if let systemImage { 81 | Image(systemName: systemImage).foregroundColor(theme.background) 82 | } 83 | Text(message).foregroundColor(theme.background) 84 | Spacer(minLength: 10) 85 | Button(action: { 86 | // withAnimation(.bouncy) { 87 | action() 88 | // } 89 | }) { 90 | Image(systemName: "xmark").foregroundColor(theme.background) 91 | } 92 | .buttonStyle(.plain) 93 | } 94 | .padding(8) 95 | .background(theme.foreground) 96 | .cornerRadius(10) 97 | .shadow(radius: 10) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ora/Modules/Browser/BrowserSplitView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BrowserSplitView: View { 4 | @EnvironmentObject var tabManager: TabManager 5 | @EnvironmentObject var appState: AppState 6 | @EnvironmentObject var toolbarManager: ToolbarManager 7 | @EnvironmentObject var sidebarManager: SidebarManager 8 | 9 | private var targetSide: SplitSide { 10 | sidebarManager.sidebarPosition == .primary ? .primary : .secondary 11 | } 12 | 13 | private var splitFraction: FractionHolder { 14 | sidebarManager.sidebarPosition == .primary 15 | ? sidebarManager.currentFraction 16 | : sidebarManager.currentFraction.inverted() 17 | } 18 | 19 | private var minPF: CGFloat { 20 | sidebarManager.sidebarPosition == .primary ? 0.16 : 0.7 21 | } 22 | 23 | private var minSF: CGFloat { 24 | sidebarManager.sidebarPosition == .primary ? 0.7 : 0.16 25 | } 26 | 27 | private var prioritySide: SplitSide { 28 | sidebarManager.sidebarPosition == .primary ? .primary : .secondary 29 | } 30 | 31 | private var dragToHidePFlag: Bool { 32 | sidebarManager.sidebarPosition == .primary 33 | } 34 | 35 | private var dragToHideSFlag: Bool { 36 | sidebarManager.sidebarPosition == .secondary 37 | } 38 | 39 | var body: some View { 40 | HSplit(left: { primaryPane() }, right: { secondaryPane() }) 41 | .hide(sidebarManager.hiddenSidebar) 42 | .splitter { Splitter.invisible() } 43 | .fraction(splitFraction) 44 | .constraints( 45 | minPFraction: minPF, 46 | minSFraction: minSF, 47 | priority: prioritySide, 48 | dragToHideP: dragToHidePFlag, 49 | dragToHideS: dragToHideSFlag 50 | ) 51 | .styling(hideSplitter: true) 52 | } 53 | 54 | @ViewBuilder 55 | private func primaryPane() -> some View { 56 | paneContent( 57 | isSidebarPane: sidebarManager.sidebarPosition == .primary, 58 | isOtherPaneHidden: sidebarManager.hiddenSidebar.side == .secondary 59 | ) 60 | } 61 | 62 | @ViewBuilder 63 | private func secondaryPane() -> some View { 64 | paneContent( 65 | isSidebarPane: sidebarManager.sidebarPosition == .secondary, 66 | isOtherPaneHidden: sidebarManager.hiddenSidebar.side == .primary 67 | ) 68 | } 69 | 70 | @ViewBuilder 71 | private func paneContent(isSidebarPane: Bool, isOtherPaneHidden: Bool) -> some View { 72 | if isSidebarPane, !isOtherPaneHidden { 73 | SidebarView() 74 | } else { 75 | contentView() 76 | } 77 | } 78 | 79 | @ViewBuilder 80 | private func contentView() -> some View { 81 | if tabManager.activeTab == nil { 82 | BrowserContentContainer { 83 | HomeView() 84 | } 85 | } 86 | ZStack { 87 | let activeId = tabManager.activeTab?.id 88 | ForEach(tabManager.tabsToRender) { tab in 89 | if tab.isWebViewReady { 90 | BrowserContentContainer { 91 | BrowserWebContentView(tab: tab) 92 | } 93 | .opacity(tab.id == activeId ? 1 : 0) 94 | .allowsHitTesting(tab.id == activeId) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /scripts/setup-sparkle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Setup Sparkle for Ora Browser 5 | # This script generates DSA keys and creates the initial appcast.xml 6 | 7 | echo "🔐 Setting up Sparkle for Ora Browser..." 8 | 9 | # Check if generate_keys is available 10 | if ! command -v generate_keys &> /dev/null; then 11 | echo "📦 Installing Sparkle tools..." 12 | 13 | # Try Homebrew first 14 | if command -v brew &> /dev/null; then 15 | echo "🍺 Installing via Homebrew..." 16 | brew install sparkle 17 | else 18 | echo "❌ Homebrew not found. Please install Homebrew first:" 19 | echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" 20 | exit 1 21 | fi 22 | 23 | # Check again after installation 24 | if ! command -v generate_keys &> /dev/null; then 25 | echo "❌ generate_keys still not found after installation." 26 | echo "🔧 Trying alternative installation method..." 27 | 28 | # Try downloading Sparkle tools directly 29 | SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/2.7.1/Sparkle-2.7.1.tar.xz" 30 | SPARKLE_DIR="$HOME/.sparkle-tools" 31 | 32 | echo "⬇️ Downloading Sparkle tools..." 33 | mkdir -p "$SPARKLE_DIR" 34 | cd "$SPARKLE_DIR" 35 | 36 | if command -v curl &> /dev/null; then 37 | curl -L "$SPARKLE_URL" -o sparkle.tar.xz 38 | elif command -v wget &> /dev/null; then 39 | wget "$SPARKLE_URL" -O sparkle.tar.xz 40 | else 41 | echo "❌ Neither curl nor wget found. Please install one of them." 42 | exit 1 43 | fi 44 | 45 | echo "📦 Extracting Sparkle tools..." 46 | tar -xf sparkle.tar.xz 47 | 48 | # Find the generate_keys binary 49 | GENERATE_KEYS_PATH=$(find . -name "generate_keys" -type f 2>/dev/null | head -1) 50 | 51 | if [ -z "$GENERATE_KEYS_PATH" ]; then 52 | echo "❌ generate_keys binary not found in downloaded Sparkle tools." 53 | echo "🔍 Contents of Sparkle directory:" 54 | find . -type f -name "*" | head -10 55 | exit 1 56 | fi 57 | 58 | echo "✅ Found generate_keys at: $GENERATE_KEYS_PATH" 59 | 60 | # Add to PATH for this session 61 | export PATH="$SPARKLE_DIR/bin:$PATH" 62 | 63 | # Create symlink for future use 64 | mkdir -p "$HOME/bin" 65 | ln -sf "$GENERATE_KEYS_PATH" "$HOME/bin/generate_keys" 66 | export PATH="$HOME/bin:$PATH" 67 | fi 68 | fi 69 | 70 | # Verify generate_keys is now available 71 | if ! command -v generate_keys &> /dev/null; then 72 | echo "❌ generate_keys command still not available." 73 | echo "🔧 Please check your Sparkle installation or PATH." 74 | exit 1 75 | fi 76 | 77 | # Generate DSA keys 78 | echo "🔑 Generating DSA keys..." 79 | generate_keys 80 | 81 | # Move keys to build directory 82 | if [ -f "dsa_priv.pem" ]; then 83 | mv dsa_priv.pem build/ 84 | fi 85 | if [ -f "dsa_pub.pem" ]; then 86 | mv dsa_pub.pem build/ 87 | fi 88 | 89 | # Copy appcast template to build directory 90 | if [ -f "appcast.xml" ]; then 91 | cp appcast.xml build/ 92 | fi 93 | 94 | echo "✅ DSA keys generated!" 95 | echo "" 96 | echo "📋 Next steps:" 97 | echo "1. Copy the public key from build/dsa_pub.pem" 98 | echo "2. Add it to your Info.plist as SUPublicEDKey" 99 | echo "3. Keep build/dsa_priv.pem secure for signing releases" 100 | echo "4. Update build/appcast.xml template with your GitHub repo URL" 101 | echo "" 102 | echo "🔒 IMPORTANT: Keep build/dsa_priv.pem secure and never commit it to version control!" 103 | echo "📁 All build files are now organized in the build/ directory" --------------------------------------------------------------------------------
Changes since last release: