Compare commits
5 Commits
101475facc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 190cfc4071 | |||
| 93cf6f8483 | |||
| a607e33ce9 | |||
| 750a096999 | |||
| d40c73399e |
@@ -62,6 +62,7 @@ struct ContentView: View {
|
|||||||
@State private var isShowingSettings = false
|
@State private var isShowingSettings = false
|
||||||
@State private var selectedStopwatch: Stopwatch?
|
@State private var selectedStopwatch: Stopwatch?
|
||||||
@State private var draggingStopwatch: Stopwatch?
|
@State private var draggingStopwatch: Stopwatch?
|
||||||
|
@State private var stopwatchToDelete: Stopwatch?
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -73,7 +74,8 @@ struct ContentView: View {
|
|||||||
stopwatch: stopwatch,
|
stopwatch: stopwatch,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
selectedStopwatch: $selectedStopwatch,
|
selectedStopwatch: $selectedStopwatch,
|
||||||
draggingStopwatch: $draggingStopwatch
|
draggingStopwatch: $draggingStopwatch,
|
||||||
|
stopwatchToDelete: $stopwatchToDelete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,9 +116,31 @@ struct ContentView: View {
|
|||||||
},
|
},
|
||||||
onCancel: {
|
onCancel: {
|
||||||
selectedStopwatch = nil
|
selectedStopwatch = nil
|
||||||
|
},
|
||||||
|
onReset: {
|
||||||
|
stopwatch.reset()
|
||||||
|
viewModel.save()
|
||||||
|
selectedStopwatch = nil
|
||||||
|
},
|
||||||
|
onDelete: {
|
||||||
|
viewModel.deleteStopwatch(id: stopwatch.id)
|
||||||
|
selectedStopwatch = nil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.alert("Are you sure you want to delete the Stopwatch \(stopwatchToDelete?.name ?? "")?", isPresented: Binding(
|
||||||
|
get: { stopwatchToDelete != nil },
|
||||||
|
set: { if !$0 { stopwatchToDelete = nil } }
|
||||||
|
)) {
|
||||||
|
if let stopwatch = stopwatchToDelete {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
withAnimation {
|
||||||
|
viewModel.deleteStopwatch(id: stopwatch.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
if viewModel.stopwatches.isEmpty {
|
if viewModel.stopwatches.isEmpty {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
@@ -140,15 +164,14 @@ struct StopwatchListItem: View {
|
|||||||
@ObservedObject var viewModel: ContentViewModel
|
@ObservedObject var viewModel: ContentViewModel
|
||||||
@Binding var selectedStopwatch: Stopwatch?
|
@Binding var selectedStopwatch: Stopwatch?
|
||||||
@Binding var draggingStopwatch: Stopwatch?
|
@Binding var draggingStopwatch: Stopwatch?
|
||||||
|
@Binding var stopwatchToDelete: Stopwatch?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
selectedStopwatch = stopwatch
|
selectedStopwatch = stopwatch
|
||||||
} label: {
|
} label: {
|
||||||
StopwatchRow(stopwatch: stopwatch, onDelete: {
|
StopwatchRow(stopwatch: stopwatch, onDelete: {
|
||||||
withAnimation {
|
stopwatchToDelete = stopwatch
|
||||||
viewModel.deleteStopwatch(id: stopwatch.id)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -156,6 +179,13 @@ struct StopwatchListItem: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(Color(UIColor.systemBackground))
|
.background(Color(UIColor.systemBackground))
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
.contextMenu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
stopwatchToDelete = stopwatch
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
.onDrag {
|
.onDrag {
|
||||||
self.draggingStopwatch = stopwatch
|
self.draggingStopwatch = stopwatch
|
||||||
return NSItemProvider(object: stopwatch.id.uuidString as NSString)
|
return NSItemProvider(object: stopwatch.id.uuidString as NSString)
|
||||||
|
|||||||
165
MultiChrono/EditLapView.swift
Normal file
165
MultiChrono/EditLapView.swift
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//
|
||||||
|
// EditLapView.swift
|
||||||
|
// MultiChrono
|
||||||
|
//
|
||||||
|
// Created by Beatrice Dellacà on 27/01/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditLapView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var lap: Stopwatch.Lap
|
||||||
|
var onSave: (Stopwatch.Lap) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("Start Time")) {
|
||||||
|
PreciseTimePicker(date: $lap.startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("End Time")) {
|
||||||
|
PreciseTimePicker(date: $lap.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Duration")) {
|
||||||
|
HStack {
|
||||||
|
Text("Duration")
|
||||||
|
Spacer()
|
||||||
|
Text(Stopwatch.format(interval: lap.endTime.timeIntervalSince(lap.startTime), format: AppSettings.shared.timeFormat))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Edit Lap \(lap.number)")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Save") {
|
||||||
|
onSave(lap)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreciseTimePicker: View {
|
||||||
|
@Binding var date: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
DatePicker("Date & Time", selection: $date, displayedComponents: [.hourAndMinute, .date])
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("Sec")
|
||||||
|
.font(.caption)
|
||||||
|
.frame(width: 40)
|
||||||
|
|
||||||
|
Picker("Seconds", selection: Binding(
|
||||||
|
get: { Calendar.current.component(.second, from: date) },
|
||||||
|
set: { newSecond in
|
||||||
|
if let newDate = Calendar.current.date(bySetting: .second, value: newSecond, of: date) {
|
||||||
|
date = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
ForEach(0..<60) {
|
||||||
|
Text(String(format: "%02d", $0)).tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 100)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
.frame(width: 20)
|
||||||
|
|
||||||
|
Text("ms")
|
||||||
|
.font(.caption)
|
||||||
|
.frame(width: 30)
|
||||||
|
|
||||||
|
// Milliseconds - Hundreds
|
||||||
|
Picker("Hundreds", selection: Binding(
|
||||||
|
get: { (Calendar.current.component(.nanosecond, from: date) / 1_000_000) / 100 },
|
||||||
|
set: { newHundreds in
|
||||||
|
let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000
|
||||||
|
let tensAndUnits = currentMillis % 100
|
||||||
|
let totalMillis = (newHundreds * 100) + tensAndUnits
|
||||||
|
if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) {
|
||||||
|
date = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
ForEach(0..<10) {
|
||||||
|
Text("\($0)").tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(width: 40, height: 100)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
// Milliseconds - Tens
|
||||||
|
Picker("Tens", selection: Binding(
|
||||||
|
get: { ((Calendar.current.component(.nanosecond, from: date) / 1_000_000) % 100) / 10 },
|
||||||
|
set: { newTens in
|
||||||
|
let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000
|
||||||
|
let hundreds = currentMillis / 100
|
||||||
|
let units = currentMillis % 10
|
||||||
|
let totalMillis = (hundreds * 100) + (newTens * 10) + units
|
||||||
|
if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) {
|
||||||
|
date = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
ForEach(0..<10) {
|
||||||
|
Text("\($0)").tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(width: 40, height: 100)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
// Milliseconds - Units
|
||||||
|
Picker("Units", selection: Binding(
|
||||||
|
get: { (Calendar.current.component(.nanosecond, from: date) / 1_000_000) % 10 },
|
||||||
|
set: { newUnits in
|
||||||
|
let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000
|
||||||
|
let hundredsAndTens = (currentMillis / 10) * 10
|
||||||
|
let totalMillis = hundredsAndTens + newUnits
|
||||||
|
if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) {
|
||||||
|
date = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
ForEach(0..<10) {
|
||||||
|
Text("\($0)").tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(width: 40, height: 100)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
EditLapView(
|
||||||
|
lap: Stopwatch.Lap(
|
||||||
|
id: UUID(),
|
||||||
|
number: 1,
|
||||||
|
startTime: Date(),
|
||||||
|
endTime: Date().addingTimeInterval(65),
|
||||||
|
duration: 65
|
||||||
|
),
|
||||||
|
onSave: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,17 +9,26 @@ import Foundation
|
|||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class Stopwatch: ObservableObject, Identifiable, Codable {
|
class Stopwatch: ObservableObject, Identifiable, Codable {
|
||||||
|
struct Lap: Identifiable, Codable {
|
||||||
|
let id: UUID
|
||||||
|
let number: Int
|
||||||
|
var startTime: Date
|
||||||
|
var endTime: Date
|
||||||
|
var duration: TimeInterval
|
||||||
|
}
|
||||||
|
|
||||||
let id: UUID
|
let id: UUID
|
||||||
@Published var name: String
|
@Published var name: String
|
||||||
@Published var elapsedTime: TimeInterval = 0
|
@Published var elapsedTime: TimeInterval = 0
|
||||||
@Published var isRunning: Bool = false
|
@Published var isRunning: Bool = false
|
||||||
|
@Published var laps: [Lap] = []
|
||||||
|
|
||||||
private var timer: AnyCancellable?
|
private var timer: AnyCancellable?
|
||||||
private var startTime: Date?
|
private var startTime: Date?
|
||||||
private var accumulatedTime: TimeInterval = 0
|
private var accumulatedTime: TimeInterval = 0
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, name, elapsedTime, isRunning, startTime, accumulatedTime
|
case id, name, elapsedTime, isRunning, startTime, accumulatedTime, laps
|
||||||
}
|
}
|
||||||
|
|
||||||
init(name: String) {
|
init(name: String) {
|
||||||
@@ -35,6 +44,7 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
isRunning = try container.decode(Bool.self, forKey: .isRunning)
|
isRunning = try container.decode(Bool.self, forKey: .isRunning)
|
||||||
startTime = try container.decodeIfPresent(Date.self, forKey: .startTime)
|
startTime = try container.decodeIfPresent(Date.self, forKey: .startTime)
|
||||||
accumulatedTime = try container.decode(TimeInterval.self, forKey: .accumulatedTime)
|
accumulatedTime = try container.decode(TimeInterval.self, forKey: .accumulatedTime)
|
||||||
|
laps = try container.decodeIfPresent([Lap].self, forKey: .laps) ?? []
|
||||||
|
|
||||||
if isRunning {
|
if isRunning {
|
||||||
// Restart the timer if it was running.
|
// Restart the timer if it was running.
|
||||||
@@ -54,6 +64,7 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
try container.encode(isRunning, forKey: .isRunning)
|
try container.encode(isRunning, forKey: .isRunning)
|
||||||
try container.encode(startTime, forKey: .startTime)
|
try container.encode(startTime, forKey: .startTime)
|
||||||
try container.encode(accumulatedTime, forKey: .accumulatedTime)
|
try container.encode(accumulatedTime, forKey: .accumulatedTime)
|
||||||
|
try container.encode(laps, forKey: .laps)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -82,8 +93,20 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
timer = nil
|
timer = nil
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
if let startTime = startTime {
|
if let startTime = startTime {
|
||||||
accumulatedTime += Date().timeIntervalSince(startTime)
|
let sessionDuration = now.timeIntervalSince(startTime)
|
||||||
|
accumulatedTime += sessionDuration
|
||||||
|
|
||||||
|
// Record Lap
|
||||||
|
let newLap = Lap(
|
||||||
|
id: UUID(),
|
||||||
|
number: laps.count + 1,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: now,
|
||||||
|
duration: sessionDuration
|
||||||
|
)
|
||||||
|
laps.append(newLap)
|
||||||
}
|
}
|
||||||
|
|
||||||
isRunning = false
|
isRunning = false
|
||||||
@@ -94,9 +117,17 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
pause()
|
// Stop timer without recording a lap? Or should reset clear laps?
|
||||||
|
// Usually reset clears everything.
|
||||||
|
if isRunning {
|
||||||
|
timer?.cancel()
|
||||||
|
timer = nil
|
||||||
|
isRunning = false
|
||||||
|
startTime = nil
|
||||||
|
}
|
||||||
accumulatedTime = 0
|
accumulatedTime = 0
|
||||||
elapsedTime = 0
|
elapsedTime = 0
|
||||||
|
laps.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tick() {
|
private func tick() {
|
||||||
@@ -104,28 +135,28 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
elapsedTime = accumulatedTime + Date().timeIntervalSince(startTime)
|
elapsedTime = accumulatedTime + Date().timeIntervalSince(startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formattedTime(format: TimeFormat = .cents) -> String {
|
static func format(interval: TimeInterval, format: TimeFormat) -> String {
|
||||||
let hours = Int(elapsedTime) / 3600
|
let hours = Int(interval) / 3600
|
||||||
let minutes = Int(elapsedTime) / 60 % 60
|
let minutes = Int(interval) / 60 % 60
|
||||||
let seconds = Int(elapsedTime) % 60
|
let seconds = Int(interval) % 60
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case .millis:
|
case .millis:
|
||||||
let millis = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 1000)
|
let millis = Int((interval.truncatingRemainder(dividingBy: 1)) * 1000)
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
return String(format: "%02i:%02i:%02i.%03i", hours, minutes, seconds, millis)
|
return String(format: "%02i:%02i:%02i.%03i", hours, minutes, seconds, millis)
|
||||||
} else {
|
} else {
|
||||||
return String(format: "%02i:%02i.%03i", minutes, seconds, millis)
|
return String(format: "%02i:%02i.%03i", minutes, seconds, millis)
|
||||||
}
|
}
|
||||||
case .cents:
|
case .cents:
|
||||||
let cents = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 100)
|
let cents = Int((interval.truncatingRemainder(dividingBy: 1)) * 100)
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
return String(format: "%02i:%02i:%02i.%02i", hours, minutes, seconds, cents)
|
return String(format: "%02i:%02i:%02i.%02i", hours, minutes, seconds, cents)
|
||||||
} else {
|
} else {
|
||||||
return String(format: "%02i:%02i.%02i", minutes, seconds, cents)
|
return String(format: "%02i:%02i.%02i", minutes, seconds, cents)
|
||||||
}
|
}
|
||||||
case .tenths:
|
case .tenths:
|
||||||
let tenths = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 10)
|
let tenths = Int((interval.truncatingRemainder(dividingBy: 1)) * 10)
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths)
|
return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths)
|
||||||
} else {
|
} else {
|
||||||
@@ -140,8 +171,51 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formattedTime(format: TimeFormat = .cents) -> String {
|
||||||
|
return Stopwatch.format(interval: elapsedTime, format: format)
|
||||||
|
}
|
||||||
|
|
||||||
// Legacy support to avoid breaking existing code immediately
|
// Legacy support to avoid breaking existing code immediately
|
||||||
var formattedTime: String {
|
var formattedTime: String {
|
||||||
formattedTime(format: AppSettings.shared.timeFormat)
|
formattedTime(format: AppSettings.shared.timeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentRunDuration: TimeInterval? {
|
||||||
|
guard let startTime = startTime else { return nil }
|
||||||
|
return Date().timeIntervalSince(startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentRunStartTime: Date? {
|
||||||
|
return startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(lap: Lap) {
|
||||||
|
guard let index = laps.firstIndex(where: { $0.id == lap.id }) else { return }
|
||||||
|
|
||||||
|
var updatedLap = lap
|
||||||
|
// Recalculate duration in case start/end times were changed
|
||||||
|
updatedLap.duration = updatedLap.endTime.timeIntervalSince(updatedLap.startTime)
|
||||||
|
|
||||||
|
laps[index] = updatedLap
|
||||||
|
|
||||||
|
// Recalculate accumulatedTime from all laps
|
||||||
|
// Note: This assumes accumulatedTime is strictly the sum of all closed laps.
|
||||||
|
// If the stopwatch is running, the current "open" lap is not in `laps` yet (it's implicit).
|
||||||
|
// However, `accumulatedTime` tracks the time *before* the current run started.
|
||||||
|
// If we edited a *past* lap, we should update accumulatedTime to be the sum of all past laps.
|
||||||
|
|
||||||
|
// Let's assume laps are only recorded when paused or when split.
|
||||||
|
// If we claim `accumulatedTime` is the sum of all stored laps:
|
||||||
|
let totalLapDuration = laps.reduce(0) { $0 + $1.duration }
|
||||||
|
accumulatedTime = totalLapDuration
|
||||||
|
|
||||||
|
if isRunning {
|
||||||
|
// function tick() will update elapsedTime naturally, but to be immediate:
|
||||||
|
if let start = startTime {
|
||||||
|
elapsedTime = accumulatedTime + Date().timeIntervalSince(start)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elapsedTime = accumulatedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,29 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct StopwatchDetailView: View {
|
struct StopwatchDetailView: View {
|
||||||
let stopwatch: Stopwatch
|
@ObservedObject var stopwatch: Stopwatch
|
||||||
let onSave: (String) -> Void
|
let onSave: (String) -> Void
|
||||||
let onCancel: () -> Void
|
let onCancel: () -> Void
|
||||||
|
let onReset: () -> Void
|
||||||
|
let onDelete: () -> Void
|
||||||
|
|
||||||
@State private var draftName: String
|
@State private var draftName: String
|
||||||
|
@State private var isShowingResetAlert = false
|
||||||
|
@State private var isShowingDeleteAlert = false
|
||||||
|
@State private var editingLap: Stopwatch.Lap?
|
||||||
|
|
||||||
init(stopwatch: Stopwatch, onSave: @escaping (String) -> Void, onCancel: @escaping () -> Void) {
|
private let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(stopwatch: Stopwatch, onSave: @escaping (String) -> Void, onCancel: @escaping () -> Void, onReset: @escaping () -> Void, onDelete: @escaping () -> Void) {
|
||||||
self.stopwatch = stopwatch
|
self.stopwatch = stopwatch
|
||||||
self.onSave = onSave
|
self.onSave = onSave
|
||||||
self.onCancel = onCancel
|
self.onCancel = onCancel
|
||||||
|
self.onReset = onReset
|
||||||
|
self.onDelete = onDelete
|
||||||
_draftName = State(initialValue: stopwatch.name)
|
_draftName = State(initialValue: stopwatch.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,9 +40,109 @@ struct StopwatchDetailView: View {
|
|||||||
Section(header: Text("Name")) {
|
Section(header: Text("Name")) {
|
||||||
TextField("Enter name...", text: $draftName)
|
TextField("Enter name...", text: $draftName)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Section(header: Text("Laps")) {
|
||||||
|
if stopwatch.isRunning,
|
||||||
|
let currentStart = stopwatch.currentRunStartTime,
|
||||||
|
let currentDuration = stopwatch.currentRunDuration {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text("Lap \(stopwatch.laps.count + 1)")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Spacer()
|
||||||
|
Text(Stopwatch.format(interval: currentDuration, format: AppSettings.shared.timeFormat))
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.bold()
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Current")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Text(dateFormatter.string(from: currentStart))
|
||||||
|
Text("Running...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopwatch.laps.isEmpty && !stopwatch.isRunning {
|
||||||
|
Text("No laps recorded yet.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
ForEach(stopwatch.laps) { lap in
|
||||||
|
Button {
|
||||||
|
editingLap = lap
|
||||||
|
} label: {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text("Lap \(lap.number)")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Text(Stopwatch.format(interval: lap.duration, format: AppSettings.shared.timeFormat))
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Text(dateFormatter.string(from: lap.startTime))
|
||||||
|
Text(dateFormatter.string(from: lap.endTime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
isShowingResetAlert = true
|
||||||
|
} label: {
|
||||||
|
Text("Reset Stopwatch")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
isShowingDeleteAlert = true
|
||||||
|
} label: {
|
||||||
|
Text("Delete Stopwatch")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navigationTitle("Edit Stopwatch")
|
.navigationTitle("Edit Stopwatch")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.alert("Are you sure you want to reset the Stopwatch \(stopwatch.name)?", isPresented: $isShowingResetAlert) {
|
||||||
|
Button("Reset", role: .destructive) {
|
||||||
|
onReset()
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
|
.alert("Are you sure you want to delete the Stopwatch \(stopwatch.name)?", isPresented: $isShowingDeleteAlert) {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
@@ -43,6 +156,11 @@ struct StopwatchDetailView: View {
|
|||||||
.disabled(draftName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
.disabled(draftName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(item: $editingLap) { lap in
|
||||||
|
EditLapView(lap: lap) { updatedLap in
|
||||||
|
stopwatch.update(lap: updatedLap)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +169,8 @@ struct StopwatchDetailView: View {
|
|||||||
StopwatchDetailView(
|
StopwatchDetailView(
|
||||||
stopwatch: Stopwatch(name: "Test Timer"),
|
stopwatch: Stopwatch(name: "Test Timer"),
|
||||||
onSave: { _ in },
|
onSave: { _ in },
|
||||||
onCancel: {}
|
onCancel: {},
|
||||||
|
onReset: {},
|
||||||
|
onDelete: {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ struct StopwatchRow: View {
|
|||||||
Text("\(stopwatch.formattedTime(format: settings.timeFormat))")
|
Text("\(stopwatch.formattedTime(format: settings.timeFormat))")
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
|
Text("Laps: \(stopwatch.laps.count + (stopwatch.isRunning ? 1 : 0))")
|
||||||
|
.font(.caption)
|
||||||
|
.monospaced()
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user