chore: adiciona .gitignore e commit.command

This commit is contained in:
João Henrique
2026-08-18 08:25:29 -04:00
parent 68958fde00
commit 8fca456ceb
215 changed files with 65752 additions and 0 deletions
@@ -0,0 +1,606 @@
import ComposableArchitecture
import HexCore
import Inject
import SwiftUI
private func modelNamesMatch(_ lhs: String, _ rhs: String) -> Bool {
ModelPatternMatcher.namesMatch(lhs, rhs)
}
public struct ModelDownloadView: View {
@ObserveInjection var inject
@Bindable var store: StoreOf<ModelDownloadFeature>
@State private var isModelLibraryPresented = false
var shouldFlash: Bool = false
public init(store: StoreOf<ModelDownloadFeature>, shouldFlash: Bool = false) {
self.store = store
self.shouldFlash = shouldFlash
}
public var body: some View {
VStack(alignment: .leading, spacing: 12) {
if !store.modelBootstrapState.isModelReady,
let message = store.modelBootstrapState.lastError,
!message.isEmpty
{
AutoDownloadBannerView(
title: "Download failed",
subtitle: message,
progress: nil,
style: .error
)
}
if selectedModelName == nil, downloadingModel == nil {
NoModelChooser(
models: parakeetModels,
suggestedModel: suggestedParakeetIdentifier,
isLoading: store.isLoadingModels,
isFlashing: shouldFlash,
onDownload: { store.send(.downloadModel($0.internalName)) },
onBrowse: { isModelLibraryPresented = true },
onRetry: { store.send(.fetchModels) }
)
} else {
CurrentModelSummary(
model: selectedModel,
selectedModelName: selectedModelName,
isInstalled: store.selectedModelIsDownloaded,
isDownloadingAnything: store.isDownloading,
downloadingModel: downloadingModel,
downloadProgress: store.downloadProgress,
onBrowse: { isModelLibraryPresented = true },
onDownload: {
if let name = selectedModelName {
store.send(.downloadModel(name))
}
},
onCancelDownload: { store.send(.cancelDownload) }
)
}
if let err = store.downloadError,
err != store.modelBootstrapState.lastError
{
Text("Model Error: \(err)")
.foregroundColor(.red)
.font(.caption)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.sheet(isPresented: $isModelLibraryPresented) {
ModelLibrarySheet(store: store)
}
.task {
if store.availableModels.isEmpty {
store.send(.fetchModels)
}
}
.enableInjection()
}
private var selectedModel: CuratedModelInfo? {
guard let selectedModelName else { return nil }
return store.curatedModels.first { model in
modelNamesMatch(model.internalName, selectedModelName)
}
}
private var selectedModelName: String? {
store.selectedModelNameForDisplay
}
private var downloadingModel: CuratedModelInfo? {
guard let downloadingModelName = store.downloadingModelName else { return nil }
return store.curatedModels.first { model in
modelNamesMatch(model.internalName, downloadingModelName)
}
}
private var parakeetModels: [CuratedModelInfo] {
let models = store.curatedModels.filter(\.isParakeet)
return models.filter { modelNamesMatch($0.internalName, suggestedParakeetIdentifier) } +
models.filter { !modelNamesMatch($0.internalName, suggestedParakeetIdentifier) }
}
private var suggestedParakeetIdentifier: String {
store.preferredParakeetIdentifier
}
}
private struct NoModelChooser: View {
let models: [CuratedModelInfo]
let suggestedModel: String
let isLoading: Bool
let isFlashing: Bool
let onDownload: (CuratedModelInfo) -> Void
let onBrowse: () -> Void
let onRetry: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Image(systemName: "arrow.down.circle.fill")
.font(.title3)
.foregroundStyle(Color.accentColor)
.frame(width: 28)
VStack(alignment: .leading, spacing: 3) {
Text("Choose a transcription model")
.font(.body.weight(.medium))
Text("Download a local model to start transcribing.")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button("View All Models…", action: onBrowse)
.controlSize(.small)
}
if models.isEmpty {
if isLoading {
Text("Loading model choices…")
.font(.caption)
.foregroundStyle(.secondary)
} else {
HStack {
Text("Model choices couldn’t be loaded.")
.font(.caption)
.foregroundStyle(.secondary)
Button("Retry", action: onRetry)
.controlSize(.small)
}
}
} else {
HStack(spacing: 10) {
ForEach(models) { model in
NoModelCard(
model: model,
isRecommended: matches(model),
isFlashing: isFlashing && matches(model),
onDownload: { onDownload(model) }
)
}
}
}
}
.padding(10)
.background(Color(NSColor.controlBackgroundColor), in: RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColor, lineWidth: isFlashing ? 3 : 1)
.animation(.easeInOut(duration: 0.5).repeatCount(3, autoreverses: true), value: isFlashing)
)
}
private var borderColor: Color {
if isFlashing { return Color.accentColor }
return Color.primary.opacity(0.08)
}
private func matches(_ model: CuratedModelInfo) -> Bool {
modelNamesMatch(model.internalName, suggestedModel)
}
}
private struct NoModelCard: View {
let model: CuratedModelInfo
let isRecommended: Bool
let isFlashing: Bool
let onDownload: () -> Void
var body: some View {
Button(action: onDownload) {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top, spacing: 8) {
Text(model.displayName)
.font(.body.weight(.medium))
.foregroundStyle(.primary)
Spacer(minLength: 8)
if isRecommended {
Text("Suggested")
.font(.caption2.weight(.medium))
.foregroundStyle(Color.accentColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.accentColor.opacity(0.12), in: Capsule())
}
}
HStack(spacing: 6) {
Text(model.size)
Text("·")
Text(model.storageSize)
Spacer()
Image(systemName: "arrow.down.circle")
}
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(NSColor.windowBackgroundColor).opacity(0.45), in: RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColor, lineWidth: isFlashing ? 3 : 1)
.animation(.easeInOut(duration: 0.5).repeatCount(3, autoreverses: true), value: isFlashing)
)
.contentShape(.rect)
}
.buttonStyle(.plain)
}
private var borderColor: Color {
if isFlashing { return Color.accentColor }
if isRecommended { return Color.accentColor.opacity(0.45) }
return Color.primary.opacity(0.08)
}
}
private struct CurrentModelSummary: View {
let model: CuratedModelInfo?
let selectedModelName: String?
let isInstalled: Bool
let isDownloadingAnything: Bool
let downloadingModel: CuratedModelInfo?
let downloadProgress: Double
let onBrowse: () -> Void
let onDownload: () -> Void
let onCancelDownload: () -> Void
var body: some View {
HStack(spacing: 12) {
Image(systemName: iconName)
.font(.title3)
.foregroundStyle(needsDownload ? Color.orange : Color.accentColor)
.frame(width: 28)
VStack(alignment: .leading, spacing: 5) {
Text(title)
.font(.body.weight(.medium))
Text(subtitle)
.font(.caption)
.foregroundStyle(needsDownload ? Color.orange : Color.secondary)
if let activeDownloadStatus {
Text(activeDownloadStatus)
.font(.caption)
.foregroundStyle(.secondary)
}
if isDownloadingAnything {
ProgressView(value: downloadProgress)
.progressViewStyle(.linear)
}
}
Spacer()
if isDownloadingAnything {
VStack(alignment: .trailing, spacing: 4) {
Text("\(Int(downloadProgress * 100))%")
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
Button("Cancel", role: .destructive, action: onCancelDownload)
.controlSize(.small)
}
} else {
HStack(spacing: 8) {
if needsDownload {
Button("Download", action: onDownload)
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
Button("Browse Models…", action: onBrowse)
.controlSize(.small)
}
}
}
.padding(10)
.background(Color(NSColor.controlBackgroundColor), in: RoundedRectangle(cornerRadius: 8))
}
/// The selection references a model that isn't on disk (e.g. the scan came
/// back empty after an update). Offer a direct download instead of
/// pretending it's installed.
private var needsDownload: Bool {
selectedModelName != nil && !isInstalled && !isDownloadingAnything
}
private var title: String {
if let model { return model.displayName }
if let selectedModelName {
return selectedModelName
.replacingOccurrences(of: "-", with: " ")
.replacingOccurrences(of: "_", with: " ")
.capitalized
}
if let downloadingModel { return "Downloading \(downloadingModel.displayName)…" }
return "Choose a transcription model"
}
private var subtitle: String {
if needsDownload { return "Not downloaded — transcription won't work until you download it." }
if let model { return "\(model.size) · \(model.storageSize)" }
if selectedModelName != nil { return "Installed local model" }
if let downloadingModel { return "\(downloadingModel.storageSize) will be stored locally on this Mac." }
return "Download a local model to start transcribing."
}
private var activeDownloadStatus: String? {
guard selectedModelName != nil, let downloadingModel else { return nil }
return "Downloading \(downloadingModel.displayName)…"
}
private var iconName: String {
if needsDownload { return "exclamationmark.triangle.fill" }
if selectedModelName == nil { return "arrow.down.circle.fill" }
return "waveform"
}
}
private struct ModelLibrarySheet: View {
@Bindable var store: StoreOf<ModelDownloadFeature>
@Environment(\.dismiss) private var dismiss
@State private var pendingDelete: CuratedModelInfo?
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text("Model Library")
.font(.title2.weight(.semibold))
Text("Select an installed model to use it, or download another model for local transcription.")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button("Done") { dismiss() }
.keyboardShortcut(.defaultAction)
}
ScrollView {
VStack(alignment: .leading, spacing: 14) {
if let recommendedLibraryModel {
modelSection(title: "Recommended", models: [recommendedLibraryModel], showsBadges: false)
}
if !otherLibraryModels.isEmpty {
modelSection(title: "Other Models", models: otherLibraryModels, showsBadges: true)
}
}
}
Label("Models run locally on your Mac. Downloads are stored on this device.", systemImage: "lock.shield")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(18)
.frame(minWidth: 680, minHeight: 420)
.confirmationDialog(
"Remove \(pendingDelete?.displayName ?? "model")?",
isPresented: Binding(
get: { pendingDelete != nil },
set: { if !$0 { pendingDelete = nil } }
),
titleVisibility: .visible
) {
if let pendingDelete {
Button("Remove Download", role: .destructive) {
store.send(.deleteModel(pendingDelete.internalName))
self.pendingDelete = nil
}
}
Button("Cancel", role: .cancel) { pendingDelete = nil }
} message: {
if let pendingDelete {
Text("This frees \(pendingDelete.storageSize) on this Mac. You can download it again anytime.")
}
}
}
private func select(_ model: CuratedModelInfo) {
guard model.isDownloaded, !store.isDownloading else { return }
store.send(.selectModel(model.internalName))
}
@ViewBuilder
private func modelSection(title: String, models: [CuratedModelInfo], showsBadges: Bool) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.padding(.horizontal, 2)
VStack(spacing: 0) {
ForEach(models) { model in
ModelLibraryRow(
model: model,
isSelected: model.isDownloaded && isSelected(model),
isDownloading: isDownloading(model),
// Only the active row receives live progress so the other
// rows don't re-render on every tick.
downloadProgress: isDownloading(model) ? store.downloadProgress : 0,
isDisabled: store.isDownloading && !isDownloading(model),
showsBadge: showsBadges && !model.isDownloaded,
onSelect: { select(model) },
onDownload: { store.send(.downloadModel(model.internalName)) },
onCancelDownload: { store.send(.cancelDownload) },
onShowInFinder: { store.send(.openModelLocation(model.internalName)) },
onDelete: { pendingDelete = model }
)
if model.id != models.last?.id {
Divider().padding(.leading, 54)
}
}
}
.background(Color(NSColor.controlBackgroundColor), in: RoundedRectangle(cornerRadius: 10))
}
}
private var recommendedLibraryModel: CuratedModelInfo? {
store.curatedModels.first { modelNamesMatch($0.internalName, suggestedParakeetIdentifier) } ?? store.curatedModels.first(where: \.isParakeet)
}
private var otherLibraryModels: [CuratedModelInfo] {
guard let recommendedLibraryModel else { return Array(store.curatedModels) }
return store.curatedModels.filter { $0.id != recommendedLibraryModel.id }
}
private func isSelected(_ model: CuratedModelInfo) -> Bool {
let selected = store.hexSettings.selectedModel
return modelNamesMatch(model.internalName, selected)
}
private func isDownloading(_ model: CuratedModelInfo) -> Bool {
store.isDownloading && store.downloadingModelName == model.internalName
}
private var suggestedParakeetIdentifier: String {
store.preferredParakeetIdentifier
}
}
private struct ModelLibraryRow: View {
let model: CuratedModelInfo
let isSelected: Bool
let isDownloading: Bool
let downloadProgress: Double
let isDisabled: Bool
let showsBadge: Bool
let onSelect: () -> Void
let onDownload: () -> Void
let onCancelDownload: () -> Void
let onShowInFinder: () -> Void
let onDelete: () -> Void
var body: some View {
HStack(spacing: 0) {
Button(action: onSelect) {
HStack(spacing: 12) {
Image(systemName: leadingIcon)
.foregroundStyle(leadingColor)
.font(.body)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 7) {
Text(model.displayName)
.font(.body.weight(.medium))
if showsBadge, let badge = model.badge {
Text(badge)
.font(.caption2.weight(.medium))
.foregroundStyle(Color.accentColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.accentColor.opacity(0.12), in: Capsule())
}
}
HStack(spacing: 12) {
Text(model.size)
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 5) {
Text("Accuracy").font(.caption2).foregroundStyle(.secondary)
StarRatingView(model.accuracyStars)
}
HStack(spacing: 5) {
Text("Speed").font(.caption2).foregroundStyle(.secondary)
StarRatingView(model.speedStars)
}
}
}
Spacer()
VStack(alignment: .trailing, spacing: 3) {
Text(model.storageSize)
.font(.caption)
.foregroundStyle(.secondary)
if let statusText {
Text(statusText)
.font(.caption2)
.foregroundStyle(isSelected ? Color.accentColor : Color.secondary)
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.contentShape(.rect)
}
.buttonStyle(.plain)
trailingControls
.padding(.trailing, 12)
}
.disabled(isDisabled)
.opacity(isDisabled ? 0.55 : 1)
.background(isSelected ? Color.accentColor.opacity(0.06) : Color.clear)
.contextMenu {
if isDownloading {
Button("Cancel Download", role: .destructive, action: onCancelDownload)
Button("Show in Finder", action: onShowInFinder)
}
if model.isDownloaded {
managementMenuItems
}
}
}
/// Shared by the trailing ⋯ menu and the right-click context menu.
@ViewBuilder
private var managementMenuItems: some View {
Button("Show in Finder", action: onShowInFinder)
Divider()
Button("Remove Download…", role: .destructive, action: onDelete)
}
@ViewBuilder
private var trailingControls: some View {
if isDownloading {
HStack(spacing: 8) {
ProgressView(value: downloadProgress)
.progressViewStyle(.circular)
.controlSize(.small)
Text("\(Int(downloadProgress * 100))%")
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
Button("Cancel", role: .destructive, action: onCancelDownload)
.controlSize(.small)
}
} else if model.isDownloaded {
Menu {
managementMenuItems
} label: {
Image(systemName: "ellipsis.circle")
.font(.body)
.foregroundStyle(.secondary)
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()
.help("Show in Finder or remove this download")
} else {
Button("Download", action: onDownload)
.controlSize(.small)
.help("Download \(model.storageSize) and switch to this model")
}
}
private var statusText: String? {
if isDownloading { return "Downloading…" }
if isSelected { return "In Use" }
if model.isDownloaded { return "Installed" }
return nil
}
private var leadingIcon: String {
if isDownloading { return "arrow.down.circle.fill" }
if isSelected { return "checkmark.circle.fill" }
if model.isDownloaded { return "circle" }
return "arrow.down.circle"
}
private var leadingColor: Color {
if isDownloading || isSelected { return .accentColor }
return .secondary
}
}