213 lines
6.5 KiB
Swift
213 lines
6.5 KiB
Swift
//
|
|
// 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()
|
|
}
|