254 lines
8.7 KiB
Swift
254 lines
8.7 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(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(NSColor.controlBackgroundColor).opacity(1.0),
|
|
Color(NSColor.controlBackgroundColor).opacity(0.6)
|
|
]),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.stroke(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color.white.opacity(0.3),
|
|
Color.white.opacity(0.05)
|
|
]),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
.shadow(
|
|
color: Color.primary.opacity(0.08),
|
|
radius: 8,
|
|
x: 0,
|
|
y: 4
|
|
)
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|