// // ContentView.swift // MultiChrono // // Created by Beatrice DellacĂ  on 26/01/26. // import SwiftUI import Combine import UniformTypeIdentifiers class ContentViewModel: ObservableObject { @Published var stopwatches: [Stopwatch] = [] private let saveKey = "SavedStopwatches" init() { load() } func addStopwatch(name: String) { let newStopwatch = Stopwatch(name: name) stopwatches.append(newStopwatch) save() } func deleteStopwatch(at index: Int) { stopwatches[index].pause() // Ensure timer is stopped stopwatches.remove(at: index) save() } func deleteStopwatch(id: UUID) { if let index = stopwatches.firstIndex(where: { $0.id == id }) { deleteStopwatch(at: index) } } func moveStopwatch(fromOffsets source: IndexSet, toOffset destination: Int) { stopwatches.move(fromOffsets: source, toOffset: destination) save() } func save() { if let encoded = try? JSONEncoder().encode(stopwatches) { UserDefaults.standard.set(encoded, forKey: saveKey) } } func load() { if let data = UserDefaults.standard.data(forKey: saveKey) { if let decoded = try? JSONDecoder().decode([Stopwatch].self, from: data) { stopwatches = decoded } } } } struct ContentView: View { @StateObject private var viewModel = ContentViewModel() @State private var isShowingAddSheet = false @State private var isShowingSettings = false @State private var selectedStopwatch: Stopwatch? @State private var draggingStopwatch: Stopwatch? @Environment(\.scenePhase) private var scenePhase var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 0) { ForEach(viewModel.stopwatches) { stopwatch in StopwatchListItem( stopwatch: stopwatch, viewModel: viewModel, selectedStopwatch: $selectedStopwatch, draggingStopwatch: $draggingStopwatch ) } } } .navigationTitle("MultiChrono") .toolbar { ToolbarItem(placement: .topBarLeading) { Button { isShowingSettings = true } label: { Image(systemName: "gearshape") } } ToolbarItem(placement: .primaryAction) { Button(action: { isShowingAddSheet = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $isShowingAddSheet) { AddStopwatchView { name in viewModel.addStopwatch(name: name) isShowingAddSheet = false } } .sheet(isPresented: $isShowingSettings) { SettingsView() } .sheet(item: $selectedStopwatch) { stopwatch in StopwatchDetailView( stopwatch: stopwatch, onSave: { newName in stopwatch.name = newName viewModel.save() selectedStopwatch = nil }, onCancel: { selectedStopwatch = nil } ) } .overlay { if viewModel.stopwatches.isEmpty { ContentUnavailableView( "No Stopwatches", systemImage: "timer", description: Text("Tap the + button to create a stopwatch.") ) } } } .onChange(of: scenePhase) { newPhase in if newPhase == .background || newPhase == .inactive { viewModel.save() } } } } struct StopwatchListItem: View { @ObservedObject var stopwatch: Stopwatch @ObservedObject var viewModel: ContentViewModel @Binding var selectedStopwatch: Stopwatch? @Binding var draggingStopwatch: Stopwatch? var body: some View { Button { selectedStopwatch = stopwatch } label: { StopwatchRow(stopwatch: stopwatch, onDelete: { withAnimation { viewModel.deleteStopwatch(id: stopwatch.id) } }) } .buttonStyle(.plain) .padding(.horizontal) .padding(.vertical, 8) .background(Color(UIColor.systemBackground)) .contentShape(Rectangle()) .contextMenu { Button(role: .destructive) { withAnimation { viewModel.deleteStopwatch(id: stopwatch.id) } } label: { Label("Delete", systemImage: "trash") } } .onDrag { self.draggingStopwatch = stopwatch return NSItemProvider(object: stopwatch.id.uuidString as NSString) } .onDrop(of: [UTType.text], delegate: StopwatchDropDelegate(item: stopwatch, viewModel: viewModel, draggingItem: $draggingStopwatch)) Divider() .padding(.leading) } } struct StopwatchDropDelegate: DropDelegate { let item: Stopwatch let viewModel: ContentViewModel @Binding var draggingItem: Stopwatch? func dropEntered(info: DropInfo) { guard let draggingItem = draggingItem else { return } if draggingItem.id != item.id { if let from = viewModel.stopwatches.firstIndex(where: { $0.id == draggingItem.id }), let to = viewModel.stopwatches.firstIndex(where: { $0.id == item.id }) { withAnimation { viewModel.stopwatches.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to) } } } } func performDrop(info: DropInfo) -> Bool { viewModel.save() draggingItem = nil return true } func dropUpdated(info: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } } #Preview { ContentView() }