import SwiftUI import SwiftData struct ContentView: View { let startupStorageError: String? @EnvironmentObject private var automation: BackgroundSyncCoordinator @Environment(\.openWindow) private var openWindow @State private var selectedJob: SyncJob? @State private var section: AppSection = .jobs @State private var showingNewJob = false @State private var showingSplash = true @State private var showingStorageAlert = false init(startupStorageError: String? = nil) { self.startupStorageError = startupStorageError } var body: some View { ZStack { mainContent .opacity(showingSplash ? 0 : 1) if showingSplash { SplashView() .transition(.opacity.combined(with: .scale(scale: 1.01))) .zIndex(10) } } .sheet(isPresented: $showingNewJob) { JobEditorView() } .alert("Storage Recovery Mode", isPresented: $showingStorageAlert) { Button("OK", role: .cancel) {} } message: { Text(startupStorageError ?? "Rsync Studio is using temporary storage.") } .onAppear { showingStorageAlert = startupStorageError != nil } .task { guard showingSplash else { return } try? await Task.sleep(for: .seconds(1.0)) withAnimation(.easeOut(duration: 0.28)) { showingSplash = false } } } private var mainContent: some View { VStack(spacing: 0) { sectionHeader Divider() switch section { case .jobs: HSplitView { JobListView(selection: $selectedJob, showingNewJob: $showingNewJob) .frame(minWidth: 320, idealWidth: 360, maxWidth: 390) Group { if let selectedJob { JobDetailView(job: selectedJob) } else { emptyJobsView } } .frame(minWidth: 620, maxWidth: .infinity, maxHeight: .infinity) } .frame(maxWidth: .infinity, maxHeight: .infinity) case .calendar: ScheduleCalendarView() case .history: RunHistoryView(onBack: { section = .jobs }) } } .background(Color(nsColor: .windowBackgroundColor)) } private var emptyJobsView: some View { VStack(spacing: 20) { ZStack { RoundedRectangle(cornerRadius: 24, style: .continuous) .fill(Color.accentColor.opacity(0.10)) .frame(width: 92, height: 92) Image(systemName: "arrow.triangle.2.circlepath") .font(.system(size: 42, weight: .medium)) .foregroundStyle(Color.accentColor) } VStack(spacing: 7) { Text("Select a Sync Job") .font(.title2.bold()) Text("Choose a job from the list to review its paths, automation and recent status.") .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 430) } Button { showingNewJob = true } label: { Label("New Sync Job", systemImage: "plus") .frame(minWidth: 120) } .buttonStyle(.borderedProminent) .controlSize(.large) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(40) } private var sectionHeader: some View { HStack(spacing: 18) { HStack(spacing: 11) { Image(systemName: "arrow.triangle.2.circlepath.circle.fill") .font(.system(size: 27, weight: .medium)) .symbolRenderingMode(.hierarchical) .foregroundStyle(Color.accentColor) VStack(alignment: .leading, spacing: 1) { Text("Rsync Studio") .font(.headline) Text(section.subtitle) .font(.caption) .foregroundStyle(.secondary) } } Spacer() if automation.activity != .idle { HStack(spacing: 7) { AnimatedSyncIndicator(size: 18, lineWidth: 2.6) Text(automation.activityDisplayTitle) .font(.caption.weight(.medium)) .lineLimit(1) } .padding(.horizontal, 10) .padding(.vertical, 6) .background(.thinMaterial, in: Capsule()) } if let serviceError = automation.lastError { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(Color.orange) .help(serviceError) .accessibilityLabel("Background service error") } Picker("Section", selection: $section) { Label("Jobs", systemImage: "rectangle.stack").tag(AppSection.jobs) Label("Calendar", systemImage: "calendar").tag(AppSection.calendar) Label("History", systemImage: "clock.arrow.circlepath").tag(AppSection.history) } .pickerStyle(.segmented) .labelsHidden() .frame(width: 390) Button { openWindow(id: "help") } label: { Label("Help", systemImage: "questionmark.circle") } .buttonStyle(.bordered) .help("Open Rsync Studio Help") } .padding(.horizontal, 18) .frame(height: 70) .background(.bar) } } enum AppSection: Hashable { case jobs, calendar, history var subtitle: String { switch self { case .jobs: return "Jobs & Automation" case .calendar: return "Schedule Calendar" case .history: return "Run History" } } } struct AnimatedSyncIndicator: View { let size: CGFloat var lineWidth: CGFloat = 7 @State private var rotation = 0.0 @State private var pulse = false var body: some View { ZStack { Circle() .stroke(Color.accentColor.opacity(0.10), lineWidth: lineWidth) Circle() .trim(from: 0.05, to: 0.76) .stroke( AngularGradient( colors: [ Color.accentColor.opacity(0.18), Color.accentColor, Color.accentColor.opacity(0.40) ], center: .center ), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) ) .rotationEffect(.degrees(rotation)) Circle() .fill(Color.accentColor) .frame(width: max(4, lineWidth * 1.05), height: max(4, lineWidth * 1.05)) .offset(y: -(size / 2)) .rotationEffect(.degrees(rotation + 18)) Image(systemName: "arrow.triangle.2.circlepath") .font(.system(size: size * 0.31, weight: .semibold)) .foregroundStyle(Color.accentColor) .scaleEffect(pulse ? 1.04 : 0.94) .opacity(pulse ? 1 : 0.72) } .frame(width: size, height: size) .onAppear { rotation = 0 pulse = false withAnimation(.linear(duration: 1.15).repeatForever(autoreverses: false)) { rotation = 360 } withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) { pulse = true } } } }