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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Nativ.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@
D2D5168B1A4243EB55EB1D68 /* RealtimeAudioMeterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01D6D1C8C321F16B5FD31F8 /* RealtimeAudioMeterTests.swift */; };
D47008C83EF6C9B110342EA2 /* ChatStreamEventRelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C037FCFD18E57EA3829DEEAB /* ChatStreamEventRelayTests.swift */; };
D4AA61C3BEF5538E3DA9A54F /* OfficeArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE82B4EBA796206F12005498 /* OfficeArchive.swift */; };
D4BF8F0FF34A3553A68B3FF1 /* NativExtensionCatalog.json in Resources */ = {isa = PBXBuildFile; fileRef = CBE0E0311AD122F4DC12D86A /* NativExtensionCatalog.json */; };
D4F073244DE11FD98AD708E2 /* Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D808CCA7AE53C7245C3D7 /* Formatting.swift */; };
D51D08482F5C0E76ACFEFDB1 /* ChatImageTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EF3E0B4A9EEC293EFD5F8A /* ChatImageTool.swift */; };
D635FD9C6938DC35CC56CAD7 /* NativApplicationCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1450610DDB81712B1F7F02B6 /* NativApplicationCommands.swift */; };
Expand Down Expand Up @@ -453,6 +454,7 @@
F39E0A3FF5B7D6375EBB71BC /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50DB6DEB5C434C27FE9AF66 /* MarkdownText.swift */; };
F3D21045349377FF044ABDC7 /* MarkdownSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE109F2077912969C25E134 /* MarkdownSelection.swift */; };
F419F46D380D8BB63BEE29EC /* SwaTexRender in Frameworks */ = {isa = PBXBuildFile; productRef = CB3602F4C9B417450A9FAA0B /* SwaTexRender */; };
F4F5A265970521BCC2273D23 /* NativExtensionMarketplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC0B3C7BCF8243FF1A3CA56 /* NativExtensionMarketplace.swift */; };
F55977B2BF1C55FDBC7AD537 /* HuggingFaceDownloadPreflight.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4D6131B6961BE835918A998 /* HuggingFaceDownloadPreflight.swift */; };
F58ED8B576ABBE02882D43D1 /* HubModelSizeResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B412A48D040D2DE044FF083 /* HubModelSizeResolver.swift */; };
F5EB384AAC13043938A0A7EB /* IntegrationTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8DD9E277F67490B9744732 /* IntegrationTypes.swift */; };
Expand Down Expand Up @@ -580,6 +582,7 @@
1C695753548A6C1F2F37C602 /* WebReadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebReadService.swift; sourceTree = "<group>"; };
1D3C334D11AD00D0290AA42D /* SoftwareUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdaterTests.swift; sourceTree = "<group>"; };
1D4B44C59153CB242FE92D3A /* ImageGenerationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerationView.swift; sourceTree = "<group>"; };
1DC0B3C7BCF8243FF1A3CA56 /* NativExtensionMarketplace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativExtensionMarketplace.swift; sourceTree = "<group>"; };
1F39043C3E9B07A5EC3B7688 /* FormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingTests.swift; sourceTree = "<group>"; };
1F7D407CD0B6A6F5440AE923 /* NativWindowRoot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativWindowRoot.swift; sourceTree = "<group>"; };
20EE1CE9AAE5A3B9A2D4669C /* NativComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativComponents.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -801,6 +804,7 @@
C870DD5A3B825D12BC04EB9D /* PersistedDataChangeHubTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedDataChangeHubTests.swift; sourceTree = "<group>"; };
C8B94192425D46A59F516118 /* PersistedDataChangeHub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedDataChangeHub.swift; sourceTree = "<group>"; };
C9A57792805B0438492CB6AB /* ChatToolRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatToolRegistryTests.swift; sourceTree = "<group>"; };
CBE0E0311AD122F4DC12D86A /* NativExtensionCatalog.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = NativExtensionCatalog.json; sourceTree = "<group>"; };
CC755271A2C8F057EB1D5995 /* ChatAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnnotation.swift; sourceTree = "<group>"; };
CCB2C50E190D5DBA2E237FB2 /* PDFDocumentTextExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFDocumentTextExtractor.swift; sourceTree = "<group>"; };
CD8DD9E277F67490B9744732 /* IntegrationTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTypes.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1393,6 +1397,7 @@
3B1B970F41F1B5612B791ABE /* KitsSectionView.swift */,
86A814B8C04BCBF1C1F869A8 /* MCPSectionView.swift */,
586D3A7976823159DE5C00F9 /* NativExtensionHostBroker.swift */,
1DC0B3C7BCF8243FF1A3CA56 /* NativExtensionMarketplace.swift */,
7BD28C5E1607C8E6025B7240 /* NativExtensionPlatform.swift */,
27F42CEC1B39477AC13352E3 /* NativExtensionStateStore.swift */,
AA7F610FC8739BA80227DB26 /* NativKit.swift */,
Expand Down Expand Up @@ -1426,6 +1431,7 @@
isa = PBXGroup;
children = (
9C57E985705899CDD8DB5996 /* MCPCatalog.json */,
CBE0E0311AD122F4DC12D86A /* NativExtensionCatalog.json */,
77A07388D57B3CE845A0773B /* AudioCues */,
4E16AB8790E5F75C078C45F8 /* Browsing */,
);
Expand Down Expand Up @@ -1756,11 +1762,11 @@
projectDirPath = "";
projectRoot = "";
targets = (
465C23BB3ADA415A5004AAD9 /* Nativ */,
ED4CB78AB93E015D0BE21F7B /* NativExtensionSDK */,
1622B274F4DF5A85C2FDB081 /* NativServerKit */,
51AD42FA29E71F2450386688 /* NativTests */,
465C23BB3ADA415A5004AAD9 /* Nativ */,
7082E254E8272A5E8D13E6F0 /* VoiceDictationExtensionRuntime */,
51AD42FA29E71F2450386688 /* NativTests */,
);
};
/* End PBXProject section */
Expand Down Expand Up @@ -1796,6 +1802,7 @@
402FF67FA99A26739AA4A653 /* MCPCatalog.json in Resources */,
0DF842E2BE07D345918F3D54 /* Manifest.json in Resources */,
137BF3A5342405863C49CC10 /* ModelProviderIcons in Resources */,
D4BF8F0FF34A3553A68B3FF1 /* NativExtensionCatalog.json in Resources */,
11BDCB609CF070A52C8394DB /* Nimble.png in Resources */,
67560D2323090F583CCC39C6 /* NotificationAppIcon.icns in Resources */,
94504A0DB46777F5DA8CDBD6 /* Parallel.png in Resources */,
Expand Down Expand Up @@ -2003,6 +2010,7 @@
18BA08A2DD4C4CAD2DC6B0E3 /* NativBulkSelection.swift in Sources */,
9954F8086FE0A7A9B3378EC9 /* NativComponents.swift in Sources */,
39ADE4524CF22ED36D505546 /* NativExtensionHostBroker.swift in Sources */,
F4F5A265970521BCC2273D23 /* NativExtensionMarketplace.swift in Sources */,
F6BFEEE99336BB34E3194214 /* NativExtensionPlatform.swift in Sources */,
2B988CEEF0FD7FCCD3862AF6 /* NativExtensionStateStore.swift in Sources */,
C03E948D1C7E27B27838C4C6 /* NativKit.swift in Sources */,
Expand Down
44 changes: 32 additions & 12 deletions Sources/Nativ/Features/Extensions/ExtensionsHubView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,23 +170,43 @@ struct HubEmptyHint: View {

private struct ExtensionsSectionView: View {
@ObservedObject var manager: NativExtensionManager
@State private var selection: Selection = .discover

private enum Selection: String, CaseIterable, Identifiable {
case discover = "Discover"
case installed = "Installed"

var id: String { rawValue }
}

var body: some View {
HubSectionScaffold(
title: "Extensions",
subtitle: "Packages that add features to Nativ."
title: "Nativ MarketPlace",
subtitle: "Discover and manage extensions for Nativ."
) {
EmptyView()
Picker("Extension view", selection: $selection) {
ForEach(Selection.allCases) { selection in
Text(selection.rawValue).tag(selection)
}
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 190)
} content: {
if manager.records.isEmpty {
HubEmptyHint(
icon: "square.stack.3d.up.slash",
text: "No extensions installed."
)
} else {
VStack(spacing: 12) {
ForEach(manager.records) { record in
ExtensionRow(record: record, manager: manager)
switch selection {
case .discover:
NativExtensionMarketplaceView()
case .installed:
if manager.records.isEmpty {
HubEmptyHint(
icon: "square.stack.3d.up.slash",
text: "No extensions installed."
)
} else {
VStack(spacing: 12) {
ForEach(manager.records) { record in
ExtensionRow(record: record, manager: manager)
}
}
}
}
Expand Down
235 changes: 235 additions & 0 deletions Sources/Nativ/Features/Extensions/NativExtensionMarketplace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import Foundation
import Observation
import SwiftUI

struct NativExtensionCatalogDocument: Decodable, Sendable {
let schemaVersion: Int
let extensions: [NativExtensionCatalogEntry]
}

struct NativExtensionCatalogEntry: Decodable, Identifiable, Sendable {
let id: String
let displayName: String
let summary: String
let developer: String
let version: String
let category: String
let systemImage: String
let status: Status

enum Status: String, Decodable, Sendable {
case preview
case available

var title: String {
switch self {
case .preview: "Preview"
case .available: "Available"
}
}
}
}

@MainActor
@Observable
final class NativExtensionCatalogStore {
enum Phase: Equatable {
case idle
case loading
case loaded
case failed
}

private(set) var entries: [NativExtensionCatalogEntry] = []
private(set) var phase: Phase = .idle
var query = ""

var filteredEntries: [NativExtensionCatalogEntry] {
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedQuery.isEmpty else { return entries }
return entries.filter {
$0.displayName.localizedStandardContains(trimmedQuery)
|| $0.summary.localizedStandardContains(trimmedQuery)
|| $0.category.localizedStandardContains(trimmedQuery)
}
}

func load() async {
guard phase == .idle else { return }
await refresh()
}

func refresh() async {
phase = .loading
do {
entries = try await fetchRemoteCatalog()
phase = .loaded
} catch {
guard let bundledEntries = loadBundledCatalog() else {
phase = .failed
return
}
entries = bundledEntries
phase = .loaded
}
}

private func fetchRemoteCatalog() async throws -> [NativExtensionCatalogEntry] {
let url = URL(
string: "https://blaizzy.github.io/nativ/extensions/catalog.json"
)!
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse,
(200..<300).contains(response.statusCode) else {
throw URLError(.badServerResponse)
}
return try decode(data)
}

private func loadBundledCatalog() -> [NativExtensionCatalogEntry]? {
guard let url = Bundle.main.url(
forResource: "NativExtensionCatalog",
withExtension: "json"
), let data = try? Data(contentsOf: url) else {
return nil
}
return try? decode(data)
}

private func decode(_ data: Data) throws -> [NativExtensionCatalogEntry] {
let document = try JSONDecoder().decode(NativExtensionCatalogDocument.self, from: data)
guard document.schemaVersion == 1 else {
throw DecodingError.dataCorrupted(
.init(codingPath: [], debugDescription: "Unsupported extension catalog schema")
)
}
return document.extensions
}
}

struct NativExtensionMarketplaceView: View {
@State private var store = NativExtensionCatalogStore()

var body: some View {
VStack(alignment: .leading, spacing: 14) {
searchField
content
}
.task {
await store.load()
}
}

private var searchField: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Search Nativ MarketPlace", text: $store.query)
.textFieldStyle(.plain)
}
.padding(.horizontal, 11)
.frame(height: 34)
.background(
Color(nsColor: .controlBackgroundColor),
in: RoundedRectangle(cornerRadius: 8, style: .continuous)
)
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5)
}
}

@ViewBuilder
private var content: some View {
switch store.phase {
case .idle, .loading:
ProgressView("Loading extensions…")
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
case .failed:
VStack(spacing: 10) {
Text("The extension marketplace could not be loaded.")
.legacyTextStyle(.supporting)
.foregroundStyle(.secondary)
Button("Try Again") {
Task { await store.refresh() }
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
case .loaded:
if store.filteredEntries.isEmpty {
VStack(spacing: 14) {
HubEmptyHint(
icon: store.entries.isEmpty ? "square.grid.2x2" : "magnifyingglass",
text: store.entries.isEmpty
? "The Nativ MarketPlace is opening soon. Reviewed extensions will appear here."
: "No extensions match your search."
)
Link(
"Open Nativ MarketPlace",
destination: URL(string: "https://blaizzy.github.io/nativ/extensions/")!
)
.controlSize(.small)
}
} else {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 280), spacing: 12)],
spacing: 12
) {
ForEach(store.filteredEntries) { entry in
NativExtensionMarketplaceCard(entry: entry)
}
}
}
}
}
}

private struct NativExtensionMarketplaceCard: View {
let entry: NativExtensionCatalogEntry

var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .top, spacing: 12) {
NativTintedIconTile(symbol: entry.systemImage, size: 44)
VStack(alignment: .leading, spacing: 3) {
Text(entry.displayName)
.legacyTextStyle(.compactCardTitle)
Text(entry.category)
.legacyTextStyle(.metadata)
.foregroundStyle(.secondary)
}
Spacer(minLength: 8)
NativStatusBadge(text: entry.status.title, tone: .active)
}

Text(entry.summary)
.legacyTextStyle(.supporting)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

HStack {
Text("By \(entry.developer) · Version \(entry.version)")
.legacyTextStyle(.metadata)
.foregroundStyle(.tertiary)
Spacer(minLength: 8)
Link(
"View on Web",
destination: URL(string: "https://blaizzy.github.io/nativ/extensions/")!
)
.controlSize(.small)
}
}
.padding(16)
.background(
Color.primary.opacity(0.03),
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
)
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5)
}
.accessibilityElement(children: .contain)
}
}
4 changes: 4 additions & 0 deletions Sources/Nativ/Resources/NativExtensionCatalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extensions": [],
"schemaVersion": 1
}
2 changes: 2 additions & 0 deletions website/downloads.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<nav class="desktop-nav" aria-label="Primary navigation">
<a href="./#models">Models</a>
<a href="./#integrations">Integrations</a>
<a href="extensions/">MarketPlace</a>
<a href="https://github.com/Blaizzy/nativ">Docs</a>
<a href="./#manifesto">Manifesto</a>
<a href="downloads.html" aria-current="page">Downloads</a>
Expand Down Expand Up @@ -57,6 +58,7 @@
<div class="mobile-menu" data-mobile-menu>
<a href="./#models">Models</a>
<a href="./#integrations">Integrations</a>
<a href="extensions/">MarketPlace</a>
<a href="https://github.com/Blaizzy/nativ">Docs</a>
<a href="./#manifesto">Manifesto</a>
<a href="downloads.html" aria-current="page">Downloads</a>
Expand Down
Binary file added website/extensions/assets/app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions website/extensions/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extensions": [],
"schemaVersion": 1
}
Loading
Loading