Image Playground integration: reader toolbar button ("Create Image
with Image Playground") opens the system generator, seeded with the
current text selection when there is one. Result uploads through the
same attachments.create/upload flow as everything else and inserts
Outline's own stable  reference
at the caret, via a new generic TextInsertionRequest/
pendingTextInsertion mechanism on NativeTextViewWrapper (nothing
previously let an embedder insert text into the editor from outside
at all).
Along the way, found and fixed a real pre-existing gap: standard
 Markdown images never rendered anywhere in the app -
services.images was never wired to anything but the no-op default, so
every such image silently fell back to dimmed raw source. Added
OutlineAPIClient.fetchAuthenticatedFile (Bearer-authed GET, for
attachments.redirect and similar) and OutlineImageProvider, a real
EmbeddedImageProvider backed by it, wired into every render surface
(reader, split-view preview, present sheet, collection overview).
Also fixes two Split View bugs surfaced while building this: the
raw-source pane was a plain SwiftUI TextEditor with no selection or
insertion hook, so the Image Playground button couldn't see a
highlighted selection there and generated images had nowhere to land;
replaced it with NativeTextViewWrapper in rawSourceMode (same engine,
no styling overhead, but now selection/insertion work like every
other pane). Also moved onSelectedTextChange's firing point earlier
in the delegate, since it was previously placed after the
rawSourceMode early-return and so could never fire for a raw-mode
editor at all. And images sized off a possibly-not-yet-settled text
container width during Split View's frequent per-keystroke rebuilds,
sticking at the wrong size until the document was reopened - now
re-measured once more a tick after layout settles.
Also reorganizes Settings: Command Palette and its full-workspace-
search toggle move out of Editor into a new Navigation section, since
they're about finding things, not about how documents are edited.
144 lines
5.6 KiB
Swift
144 lines
5.6 KiB
Swift
import Observation
|
|
|
|
/// Top-level groupings shown as section headers in `SettingsSidebarList`.
|
|
/// `general` holds everything that's ours, not Outline's own settings
|
|
/// categories — labeled "Outpost" so it reads as clearly distinct from the
|
|
/// Outline-sourced groups below it.
|
|
enum SettingsCategory: String, CaseIterable, Identifiable {
|
|
case general
|
|
case account
|
|
case workspace
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String? {
|
|
switch self {
|
|
case .general: return "Outpost"
|
|
case .account: return "Account"
|
|
case .workspace: return "Workspace"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One entry in the Settings sidebar. Mirrors Outline's own settings
|
|
/// categories (Account/Workspace) so this app's settings read as a native
|
|
/// counterpart to the web app's, plus a `general` group for things that are
|
|
/// ours and don't map onto Outline's structure (offline/sync, advanced,
|
|
/// about, appearance). Outline's own version info moved to the sidebar
|
|
/// footer (`SettingsSidebarList`) instead of a standalone
|
|
/// Integrations & Installation section.
|
|
///
|
|
/// Most of the Account/Workspace cases are navigation-only for now —
|
|
/// `SettingsView` renders a "Coming Soon" placeholder for anything not
|
|
/// explicitly built yet. Content lands section by section.
|
|
enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|
// General (ours)
|
|
case appearance, editor, navigation, offlineSync, advanced, about
|
|
|
|
// Account
|
|
case profile, preferences, notifications, passkeys, apiAccess
|
|
|
|
// Workspace
|
|
case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData
|
|
|
|
var id: String { rawValue }
|
|
|
|
var category: SettingsCategory {
|
|
switch self {
|
|
case .appearance, .editor, .navigation, .offlineSync, .advanced, .about:
|
|
return .general
|
|
case .profile, .preferences, .notifications, .passkeys, .apiAccess:
|
|
return .account
|
|
case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData:
|
|
return .workspace
|
|
}
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .appearance: return "Appearance"
|
|
case .editor: return "Editor"
|
|
case .navigation: return "Navigation"
|
|
case .offlineSync: return "Offline & Sync"
|
|
case .advanced: return "Advanced"
|
|
case .about: return "About"
|
|
case .profile: return "Profile"
|
|
case .preferences: return "Preferences"
|
|
case .notifications: return "Notifications"
|
|
case .passkeys: return "Passkeys"
|
|
case .apiAccess: return "API & Access"
|
|
case .details: return "Details"
|
|
case .authentication: return "Authentication"
|
|
case .security: return "Security"
|
|
case .ai: return "AI"
|
|
case .members: return "Members"
|
|
case .groups: return "Groups"
|
|
case .templates: return "Templates"
|
|
case .emojis: return "Emojis"
|
|
case .applications: return "Applications"
|
|
case .shared: return "Shared"
|
|
case .links: return "Links"
|
|
case .webhooks: return "Webhooks"
|
|
case .importData: return "Import"
|
|
case .exportData: return "Export"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .appearance: return "paintbrush"
|
|
case .editor: return "square.split.2x1"
|
|
case .navigation: return "command"
|
|
case .offlineSync: return "arrow.triangle.2.circlepath"
|
|
case .advanced: return "wrench.and.screwdriver"
|
|
case .about: return "info.circle"
|
|
case .profile: return "person.crop.circle"
|
|
case .preferences: return "gearshape"
|
|
case .notifications: return "bell"
|
|
case .passkeys: return "key"
|
|
case .apiAccess: return "chevron.left.forwardslash.chevron.right"
|
|
case .details: return "building.2"
|
|
case .authentication: return "lock"
|
|
case .security: return "shield"
|
|
case .ai: return "sparkles"
|
|
case .members: return "person.2"
|
|
case .groups: return "person.3"
|
|
case .templates: return "doc.on.doc"
|
|
case .emojis: return "face.smiling"
|
|
case .applications: return "app.badge"
|
|
case .shared: return "square.and.arrow.up.on.square"
|
|
case .links: return "link"
|
|
case .webhooks: return "bolt.horizontal"
|
|
case .importData: return "square.and.arrow.down"
|
|
case .exportData: return "square.and.arrow.up"
|
|
}
|
|
}
|
|
|
|
/// Everything actually built so far — everything else in Account/
|
|
/// Workspace renders a "Coming Soon" placeholder until its content is
|
|
/// specified and built.
|
|
var isImplemented: Bool {
|
|
switch self {
|
|
case .appearance, .editor, .navigation, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cross-cutting UI state that doesn't belong to any one screen. Lives at
|
|
/// `OutpostApp` (so both `RootView`'s content and its `⌘,` command can reach
|
|
/// it) and is read wherever something needs to open Settings (the profile
|
|
/// menu) or render it — as a swap of the *existing* sidebar/detail panes in
|
|
/// `ContentView_macOS`, not a separate popup window or an overlay that hides
|
|
/// the sidebar.
|
|
@Observable
|
|
@MainActor
|
|
final class AppNavigation {
|
|
var isShowingSettings = false
|
|
var selectedSettingsSection: SettingsSection? = .appearance
|
|
/// ⌘K, see `OutpostApp`'s `CommandGroup` and `CommandPaletteView`.
|
|
var isShowingCommandPalette = false
|
|
}
|