refactor: Update job status icons and UI elements in Swift client

- Replaced job status icons with SF Symbols for improved visual consistency.
- Adjusted spacing and font sizes in JobMonitorView and JobRowView for better layout and readability.
- Simplified progress percentage and duration formatting to display whole numbers.
- Enhanced the overall appearance of job rows with consistent padding and background styling.
This commit is contained in:
Jamie Pine
2025-09-22 15:27:43 -07:00
parent 9793466153
commit e53aee331a
3 changed files with 84 additions and 116 deletions

View File

@@ -48,15 +48,15 @@ enum JobStatus: String, Codable, CaseIterable {
var icon: String {
switch self {
case .running:
return "⚡️"
return "circle.fill"
case .completed:
return ""
return "checkmark.circle.fill"
case .failed:
return ""
return "xmark.circle.fill"
case .paused:
return "⏸️"
return "pause.circle.fill"
case .queued:
return ""
return "clock.fill"
}
}
}

View File

@@ -23,18 +23,18 @@ struct JobMonitorView: View {
private var headerView: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: 3) {
Text("Spacedrive Jobs")
.font(.title2)
.fontWeight(.semibold)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.primary)
HStack(spacing: 6) {
Circle()
.fill(connectionStatusColor)
.frame(width: 8, height: 8)
.frame(width: 6, height: 6)
Text(viewModel.connectionStatus.displayName)
.font(.caption)
.font(.system(size: 11))
.foregroundColor(.secondary)
}
}
@@ -46,14 +46,14 @@ struct JobMonitorView: View {
viewModel.reconnect()
}) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14, weight: .medium))
.font(.system(size: 12, weight: .medium))
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
.help("Reconnect to daemon")
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.padding(.horizontal, 12)
.padding(.vertical, 10)
}
private var connectionStatusColor: Color {
@@ -90,7 +90,7 @@ struct JobMonitorView: View {
private var jobListView: some View {
ScrollView {
LazyVStack(spacing: 12) {
LazyVStack(spacing: 4) {
ForEach(viewModel.jobs) { job in
JobRowView(job: job)
.transition(.asymmetric(
@@ -99,10 +99,10 @@ struct JobMonitorView: View {
))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
.animation(.easeInOut(duration: 0.3), value: viewModel.jobs.count)
.animation(.easeInOut(duration: 0.2), value: viewModel.jobs.count)
}
}

View File

@@ -4,7 +4,7 @@ struct JobRowView: View {
let job: JobInfo
private var progressPercentage: String {
return String(format: "%.1f%%", job.progress * 100)
return String(format: "%.0f%%", job.progress * 100)
}
private var timeAgo: String {
@@ -18,95 +18,93 @@ struct JobRowView: View {
let duration = completedAt.timeIntervalSince(job.startedAt)
if duration < 60 {
return String(format: "%.1fs", duration)
return String(format: "%.0fs", duration)
} else if duration < 3600 {
return String(format: "%.1fm", duration / 60)
return String(format: "%.0fm", duration / 60)
} else {
return String(format: "%.1fh", duration / 3600)
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header with job name and status
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 12) {
// Status indicator - simple colored circle
Circle()
.fill(statusColor)
.frame(width: 8, height: 8)
.opacity(job.status == .completed ? 1.0 : 0.8)
// Main content area
VStack(alignment: .leading, spacing: 3) {
// Top row: Job name and status/progress
HStack {
Text(job.name)
.font(.headline)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
Text("ID: \(String(job.id.prefix(8)))...")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Spacer()
// Status and progress info
HStack(spacing: 6) {
if job.status == .running {
Text(progressPercentage)
.font(.system(size: 11, weight: .medium))
.foregroundColor(.secondary)
} else {
Text(job.status.displayName)
.font(.system(size: 11, weight: .medium))
.foregroundColor(job.status == .failed ? .red : .secondary)
}
HStack(spacing: 4) {
Text(job.status.icon)
.font(.title2)
Text(job.status.displayName)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(statusColor)
}
}
// Progress bar (only show for running jobs)
if job.status == .running {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Progress")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(progressPercentage)
.font(.caption)
.fontWeight(.medium)
if let duration = duration {
Text("\(duration)")
.font(.system(size: 11))
.foregroundColor(.secondary)
.opacity(0.7)
} else if job.status == .running {
Text("\(timeAgo)")
.font(.system(size: 11))
.foregroundColor(.secondary)
.opacity(0.7)
}
}
ProgressView(value: job.progress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
.scaleEffect(x: 1, y: 0.8, anchor: .center)
}
}
// Error message (if any)
if let errorMessage = job.errorMessage, !errorMessage.isEmpty {
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.red.opacity(0.1))
.cornerRadius(4)
}
// Progress bar for active jobs
if job.status == .running || job.status == .paused {
ProgressView(value: job.progress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: statusColor))
.scaleEffect(x: 1, y: 0.6, anchor: .center)
} else if job.status == .completed {
// Completed indicator line
Rectangle()
.fill(Color.green.opacity(0.3))
.frame(height: 2)
.cornerRadius(1)
}
// Timestamps
HStack {
Text("Started \(timeAgo)")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if let duration = duration {
Text("Duration: \(duration)")
.font(.caption)
.foregroundColor(.secondary)
// Error message (compact display)
if let errorMessage = job.errorMessage, !errorMessage.isEmpty {
Text(errorMessage)
.font(.system(size: 11))
.foregroundColor(.red)
.lineLimit(1)
.truncationMode(.tail)
}
}
}
.frame(height: 44) // Fixed height for uniform appearance
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(backgroundColorForStatus)
.cornerRadius(8)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(Color.primary.opacity(0.03))
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColorForStatus, lineWidth: 1)
RoundedRectangle(cornerRadius: 6)
.stroke(Color.primary.opacity(0.08), lineWidth: 0.5)
)
}
@@ -124,36 +122,6 @@ struct JobRowView: View {
return .gray
}
}
private var backgroundColorForStatus: Color {
switch job.status {
case .running:
return Color.blue.opacity(0.05)
case .completed:
return Color.green.opacity(0.05)
case .failed:
return Color.red.opacity(0.05)
case .paused:
return Color.orange.opacity(0.05)
case .queued:
return Color.gray.opacity(0.05)
}
}
private var borderColorForStatus: Color {
switch job.status {
case .running:
return Color.blue.opacity(0.2)
case .completed:
return Color.green.opacity(0.2)
case .failed:
return Color.red.opacity(0.2)
case .paused:
return Color.orange.opacity(0.2)
case .queued:
return Color.gray.opacity(0.2)
}
}
}
#Preview {