Initial Commit
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject private var automation: BackgroundSyncManager
|
||||
@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
|
||||
|
||||
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() }
|
||||
.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 Manager")
|
||||
.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())
|
||||
}
|
||||
|
||||
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 Manager 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user