vOOice/VoiceInk/Views/ModelCardRowView.swift

222 lines
7.5 KiB
Swift

import SwiftUI
import AppKit
struct ModelCardRowView: View {
let model: PredefinedModel
let isDownloaded: Bool
let isCurrent: Bool
let downloadProgress: [String: Double]
let modelURL: URL?
// Actions
var deleteAction: () -> Void
var setDefaultAction: () -> Void
var downloadAction: () -> Void
private var isDownloading: Bool {
downloadProgress.keys.contains(model.name + "_main") ||
downloadProgress.keys.contains(model.name + "_coreml")
}
var body: some View {
HStack(alignment: .top, spacing: 16) {
// Main Content
VStack(alignment: .leading, spacing: 6) {
headerSection
metadataSection
descriptionSection
progressSection
}
.frame(maxWidth: .infinity, alignment: .leading)
// Action Controls
actionSection
}
.padding(16)
.background(CardBackground(isSelected: isCurrent, useAccentGradientWhenSelected: isCurrent))
}
// MARK: - Components
private var headerSection: some View {
HStack(alignment: .firstTextBaseline) {
Text(model.displayName)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(Color(.labelColor))
statusBadge
Spacer()
}
}
private var statusBadge: some View {
Group {
if isCurrent {
Text("Default")
.font(.system(size: 11, weight: .medium))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Capsule().fill(Color.accentColor))
.foregroundColor(.white)
} else if isDownloaded {
Text("Downloaded")
.font(.system(size: 11, weight: .medium))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Capsule().fill(Color(.quaternaryLabelColor)))
.foregroundColor(Color(.labelColor))
}
}
}
private var metadataSection: some View {
HStack(spacing: 16) {
// Language
Label(model.language, systemImage: "globe")
.font(.system(size: 11))
.foregroundColor(Color(.secondaryLabelColor))
// Size
Label(model.size, systemImage: "internaldrive")
.font(.system(size: 11))
.foregroundColor(Color(.secondaryLabelColor))
// Speed
HStack(spacing: 4) {
Text("Speed")
.font(.system(size: 11, weight: .medium))
.foregroundColor(Color(.secondaryLabelColor))
progressDotsWithNumber(value: model.speed * 10)
}
// Accuracy
HStack(spacing: 4) {
Text("Accuracy")
.font(.system(size: 11, weight: .medium))
.foregroundColor(Color(.secondaryLabelColor))
progressDotsWithNumber(value: model.accuracy * 10)
}
}
}
private var descriptionSection: some View {
Text(model.description)
.font(.system(size: 11))
.foregroundColor(Color(.secondaryLabelColor))
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 4)
}
private var progressSection: some View {
Group {
if isDownloading {
DownloadProgressView(
modelName: model.name,
downloadProgress: downloadProgress
)
.padding(.top, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
private var actionSection: some View {
HStack(spacing: 8) {
if isCurrent {
Text("Default Model")
.font(.system(size: 12))
.foregroundColor(Color(.secondaryLabelColor))
} else if isDownloaded {
Button(action: setDefaultAction) {
Text("Set as Default")
.font(.system(size: 12))
}
.buttonStyle(.bordered)
.controlSize(.small)
} else {
Button(action: downloadAction) {
HStack(spacing: 4) {
Text(isDownloading ? "Downloading..." : "Download")
.font(.system(size: 12, weight: .medium))
Image(systemName: "arrow.down.circle")
.font(.system(size: 12, weight: .medium))
}
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
Capsule()
.fill(Color(.controlAccentColor))
.shadow(color: Color(.controlAccentColor).opacity(0.2), radius: 2, x: 0, y: 1)
)
}
.buttonStyle(.plain)
.disabled(isDownloading)
}
if isDownloaded {
Menu {
Button(action: deleteAction) {
Label("Delete Model", systemImage: "trash")
}
if isDownloaded {
Button {
if let modelURL = modelURL {
NSWorkspace.shared.selectFile(modelURL.path, inFileViewerRootedAtPath: "")
}
} label: {
Label("Show in Finder", systemImage: "folder")
}
}
} label: {
Image(systemName: "ellipsis.circle")
.font(.system(size: 14))
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.frame(width: 20, height: 20)
}
}
}
// MARK: - Helpers
private var downloadComponents: [(String, Double)] {
[
("Model", downloadProgress[model.name + "_main"] ?? 0),
("CoreML", downloadProgress[model.name + "_coreml"] ?? 0)
].filter { $0.1 > 0 }
}
private func progressDotsWithNumber(value: Double) -> some View {
HStack(spacing: 4) {
progressDots(value: value)
Text(String(format: "%.1f", value))
.font(.system(size: 10, weight: .medium, design: .monospaced))
.foregroundColor(Color(.secondaryLabelColor))
}
}
private func progressDots(value: Double) -> some View {
HStack(spacing: 2) {
ForEach(0..<5) { index in
Circle()
.fill(index < Int(value / 2) ? performanceColor(value: value / 10) : Color(.quaternaryLabelColor))
.frame(width: 6, height: 6)
}
}
}
private func performanceColor(value: Double) -> Color {
switch value {
case 0.8...1.0: return Color(.systemGreen)
case 0.6..<0.8: return Color(.systemYellow)
case 0.4..<0.6: return Color(.systemOrange)
default: return Color(.systemRed)
}
}
}