8451db1142
- Replace root VStack with NavigationSplitView (2-column, collapsible sidebar) - Add SidebarView: new chat button, conversation search, list with swipe actions - Slim HeaderView to text-only (provider + model + star); remove all icon rows - Move status pills (Online, MCP, Synced) to footer right side - Remove version number and shortcut hints from footer - Add resizable InputBar with drag handle (persisted height) and globe/network.slash online toggle - Fix Norwegian menu appearing on English systems (CFBundleLocalizations in Info.plist) - Add View menu (Model Info, History, Stats, Credits, Online Mode toggle ⌘⇧O) - Add ⌘L as alias for Search Conversations (muscle memory for /load users) - Add Check for Updates to Help menu with download URL from Gitea API - Add one-time Intel/Rosetta deprecation warning on first launch - Swift 6: fix self.Self.isoString() call sites in DatabaseService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
7.1 KiB
Swift
172 lines
7.1 KiB
Swift
//
|
|
// oAIApp.swift
|
|
// oAI
|
|
//
|
|
// Main app entry point
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (C) 2026 Rune Olsen
|
|
//
|
|
// This file is part of oAI.
|
|
//
|
|
// oAI is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// oAI is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
|
|
// Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public
|
|
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
import SwiftUI
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
|
|
@main
|
|
struct oAIApp: App {
|
|
@State private var chatViewModel = ChatViewModel()
|
|
@State private var showAbout = false
|
|
|
|
init() {
|
|
// Start email handler on app launch
|
|
EmailHandlerService.shared.start()
|
|
|
|
// Sync Git changes on app launch (pull + import)
|
|
Task {
|
|
await GitSyncService.shared.syncOnStartup()
|
|
}
|
|
|
|
// Check for updates in the background
|
|
UpdateCheckService.shared.checkForUpdates()
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.environment(chatViewModel)
|
|
.preferredColorScheme(.dark)
|
|
.sheet(isPresented: $showAbout) {
|
|
AboutView()
|
|
}
|
|
#if os(macOS)
|
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in
|
|
// Trigger auto-save on app quit
|
|
Task {
|
|
await chatViewModel.onAppWillTerminate()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#if os(macOS)
|
|
.windowStyle(.hiddenTitleBar)
|
|
.windowToolbarStyle(.unified)
|
|
.defaultSize(width: 1024, height: 800)
|
|
.windowResizability(.contentMinSize)
|
|
.commands {
|
|
// ── Apple menu ────────────────────────────────────────────────
|
|
CommandGroup(replacing: .appInfo) {
|
|
Button("About oAI") { showAbout = true }
|
|
}
|
|
|
|
CommandGroup(replacing: .appSettings) {
|
|
Button("Settings…") { chatViewModel.showSettings = true }
|
|
.keyboardShortcut(",", modifiers: .command)
|
|
}
|
|
|
|
// ── File menu ─────────────────────────────────────────────────
|
|
// Replacing .newItem removes the auto-added "New Window" entry
|
|
CommandGroup(replacing: .newItem) {
|
|
Button("New Chat") { chatViewModel.newConversation() }
|
|
.keyboardShortcut("n", modifiers: .command)
|
|
Button("Clear Chat") { chatViewModel.clearChat() }
|
|
.keyboardShortcut("k", modifiers: .command)
|
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
|
}
|
|
|
|
CommandGroup(after: .newItem) {
|
|
Button("Open Chat…") { chatViewModel.showConversations = true }
|
|
.keyboardShortcut("o", modifiers: .command)
|
|
Button("Search Conversations") { chatViewModel.showConversations = true }
|
|
.keyboardShortcut("l", modifiers: .command)
|
|
}
|
|
|
|
CommandGroup(replacing: .saveItem) {
|
|
Button("Save Chat") { chatViewModel.saveFromMenu() }
|
|
.keyboardShortcut("s", modifiers: .command)
|
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
|
Button("Save Chat As…") { chatViewModel.saveAsFromMenu() }
|
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
|
Button("Stats") { chatViewModel.showStats = true }
|
|
.keyboardShortcut("s", modifiers: [.command, .shift])
|
|
}
|
|
|
|
CommandGroup(after: .importExport) {
|
|
Button("Export as Markdown…") {
|
|
let name = chatViewModel.currentConversationName ?? "conversation"
|
|
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
|
chatViewModel.exportConversation(format: "md", filename: "\(safe).md")
|
|
}
|
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
|
}
|
|
|
|
// ── View menu ─────────────────────────────────────────────────
|
|
CommandMenu("View") {
|
|
Button("Select Model") { chatViewModel.showModelSelector = true }
|
|
.keyboardShortcut("m", modifiers: .command)
|
|
|
|
Button("Model Info") {
|
|
chatViewModel.modelInfoTarget = chatViewModel.selectedModel
|
|
}
|
|
.keyboardShortcut("i", modifiers: .command)
|
|
.disabled(chatViewModel.selectedModel == nil)
|
|
|
|
Divider()
|
|
|
|
Button("Command History") { chatViewModel.showHistory = true }
|
|
.keyboardShortcut("h", modifiers: .command)
|
|
|
|
Button("In-App Help") { chatViewModel.showHelp = true }
|
|
.keyboardShortcut("/", modifiers: .command)
|
|
|
|
Button("Credits") { chatViewModel.showCredits = true }
|
|
|
|
Divider()
|
|
|
|
Button(chatViewModel.onlineMode ? "Online Mode: On" : "Online Mode: Off") {
|
|
chatViewModel.onlineMode.toggle()
|
|
}
|
|
.keyboardShortcut("o", modifiers: [.command, .shift])
|
|
}
|
|
|
|
// ── Help menu ─────────────────────────────────────────────────
|
|
CommandGroup(replacing: .help) {
|
|
Button("oAI Help") { openHelp() }
|
|
.keyboardShortcut("?", modifiers: .command)
|
|
Divider()
|
|
Button(UpdateCheckService.shared.isCheckingManually ? "Checking…" : "Check for Updates…") {
|
|
UpdateCheckService.shared.checkForUpdatesManually()
|
|
}
|
|
.disabled(UpdateCheckService.shared.isCheckingManually)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if os(macOS)
|
|
private func openHelp() {
|
|
if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) {
|
|
NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html"))
|
|
} else {
|
|
// Fallback to Apple Help if help book not found
|
|
NSHelpManager.shared.openHelpAnchor("", inBook: Bundle.main.object(forInfoDictionaryKey: "CFBundleHelpBookName") as? String)
|
|
}
|
|
}
|
|
#endif
|
|
}
|