diff --git a/NearFuture.xcodeproj/project.pbxproj b/NearFuture.xcodeproj/project.pbxproj index 0960932..0a3c59a 100644 --- a/NearFuture.xcodeproj/project.pbxproj +++ b/NearFuture.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CD2DE7308E0008D61C /* ArchiveView.swift */; }; A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CF2DE731BD0008D61C /* HomeView.swift */; }; A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20D32DE7339E0008D61C /* AboutView.swift */; }; + A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; }; + A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; }; A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8402DCAABE00064DCA0 /* EventListView.swift */; }; A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; }; /* End PBXBuildFile section */ @@ -157,6 +159,7 @@ A98C20CD2DE7308E0008D61C /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = ""; }; A98C20CF2DE731BD0008D61C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; A98C20D32DE7339E0008D61C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteEventButton.swift; sourceTree = ""; }; A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -227,6 +230,7 @@ A90D49512DDE2D0000781124 /* Extensions.swift */, A90D49202DDE0A3B00781124 /* Model */, A91EF80A2DFC910000B8463D /* ViewModifiers.swift */, + A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */, ); path = Shared; sourceTree = ""; @@ -529,6 +533,7 @@ A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */, A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */, A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */, + A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */, A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */, A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */, A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */, @@ -575,6 +580,7 @@ A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */, A90D49532DDE2D0000781124 /* Extensions.swift in Sources */, A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */, + A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */, A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */, A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */, A91EF81E2DFD796600B8463D /* SymbolsPicker.swift in Sources */, diff --git a/NearFuture/Views/Home/EventListView.swift b/NearFuture/Views/Home/EventListView.swift index 2077ea9..29b6c98 100644 --- a/NearFuture/Views/Home/EventListView.swift +++ b/NearFuture/Views/Home/EventListView.swift @@ -12,43 +12,11 @@ struct EventListView: View { @ObservedObject var viewModel: EventViewModel @State var event: Event - @State var completeInProgress: Bool = false - @State var completeStartTime: Date = .now - @State var progress: Double = 0 - @State var timer: Timer? - private let completeDuration: TimeInterval = 3.0 @Environment(\.openWindow) var openWindow - @State var largeTick: Bool = false @State var hovering: Bool = false - func startCompleting() { -#if canImport(UIKit) - UIImpactFeedbackGenerator(style: .heavy).impactOccurred() -#endif - completeInProgress = true - progress = 0 - completeStartTime = .now - - timer = Timer(timeInterval: 0.01, repeats: true) { timer in - guard timer.isValid else { return } - guard completeInProgress else { return } - let elapsed = Date().timeIntervalSince(completeStartTime) - progress = min(elapsed, 1.0) - - if progress >= 1.0 { - timer.invalidate() - viewModel.completeEvent(&event) -#if canImport(UIKit) - UINotificationFeedbackGenerator().notificationOccurred(.success) -#endif - completeInProgress = false - } - } - RunLoop.main.add(timer!, forMode: .common) - } - - #if canImport(AppKit) +#if canImport(AppKit) var body: some View { ZStack { Color.black.opacity(hovering ? 0.5 : 0.0) @@ -107,42 +75,9 @@ struct EventListView: View { .multilineTextAlignment(.trailing) .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one) } - - Group { - if completeInProgress { - ZStack { - ProgressView(value: progress) - .progressViewStyle(.circular) - Image(systemName: "xmark") - .bold() - } - .onTapGesture { - timer?.invalidate() - completeInProgress = false - progress = 0 - } - } else { - Image(systemName: event.complete ? "checkmark.circle.fill" : "circle") - .resizable().scaledToFit() - .foregroundStyle(event.complete ? .green : event.color.color) - .bold() - .onTapGesture { - startCompleting() - } - .onHover() { hovering in - withAnimation { - largeTick.toggle() - } - } - .scaleEffect(largeTick ? 1.5 : 1) - } - } - .frame(maxWidth: 20) - .shadow(color: .one.opacity(0.2), radius: 2.5) - .padding(.trailing, 15) - .animation( - .spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2), - value: largeTick + CompleteEventButton( + viewModel: viewModel, + event: $event ) } .transition(.opacity) @@ -170,131 +105,94 @@ struct EventListView: View { } } } - #else +#else var body: some View { - NavigationLink() { - EditEventView( - viewModel: viewModel, - event: $event - ) - } label: { - ZStack { + HStack { + RoundedRectangle(cornerRadius: 5) + .frame(width: 7) + .foregroundStyle( + event.color.color.opacity( + event.complete ? 0.5 : 1 + ) + ) + VStack(alignment: .leading) { HStack { - RoundedRectangle(cornerRadius: 5) - .frame(width: 7) - .foregroundStyle( - event.color.color.opacity( - event.complete ? 0.5 : 1 - ) - ) - VStack(alignment: .leading) { - HStack { - Image(systemName: event.symbol) - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .shadow(radius: 5) - .foregroundStyle( - .one.opacity( - event.complete ? 0.5 : 1 - ) - ) - Text("\(event.name)") - .font(.headline) - .foregroundStyle(.one) - .strikethrough(event.complete) - .multilineTextAlignment(.leading) - } - if !event.notes.isEmpty { - Text(event.notes) - .font(.subheadline) - .foregroundStyle(.one.opacity(0.8)) - .multilineTextAlignment(.leading) - } - Text( - event.date.formatted( - date: .long, - time: .shortened - ) - ) - .font(.subheadline) + Image(systemName: event.symbol) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .shadow(radius: 5) .foregroundStyle( .one.opacity( event.complete ? 0.5 : 1 ) ) - if event.recurrence != .none { - Text("Occurs \(event.recurrence.rawValue)") - .font(.subheadline) - .foregroundStyle( - .one.opacity(event.complete ? 0.5 : 1)) - } - } - Spacer() - VStack { - Text("\(daysUntilEvent(event.date).long)") - .font(.subheadline) - .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one) - .multilineTextAlignment(.trailing) - } - Button() { - startCompleting() - } label: { - if completeInProgress { - ZStack { - ProgressView(value: progress) - .progressViewStyle(.circular) - Image(systemName: "xmark") - .bold() - } - .onTapGesture { - timer?.invalidate() - completeInProgress = false - progress = 0 - } - } else { - Image(systemName: event.complete ? "checkmark.circle.fill" : "circle") - .resizable().scaledToFit() - .foregroundStyle(event.complete ? .green : event.color.color) - .bold() - } - } - .buttonStyle(.borderless) - .frame(maxWidth: 25, maxHeight: 25) - .shadow(radius: 5) - .padding(.trailing, 5) - .modifier(hapticSuccess(trigger: event.complete)) + Text("\(event.name)") + .font(.headline) + .foregroundStyle(.one) + .strikethrough(event.complete) + .multilineTextAlignment(.leading) } - .transition(.opacity) - .padding(.vertical, 5) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke( - .one.opacity(0.5), - lineWidth: 1 - ) + if !event.notes.isEmpty { + Text(event.notes) + .font(.subheadline) + .foregroundStyle(.one.opacity(0.8)) + .multilineTextAlignment(.leading) + } + Text( + event.date.formatted( + date: .long, + time: .shortened + ) ) - .clipShape( - RoundedRectangle(cornerRadius: 10) + .font(.subheadline) + .foregroundStyle( + .one.opacity( + event.complete ? 0.5 : 1 + ) ) - .fixedSize(horizontal: false, vertical: true) + if event.recurrence != .none { + Text("Occurs \(event.recurrence.rawValue)") + .font(.subheadline) + .foregroundStyle( + .one.opacity(event.complete ? 0.5 : 1)) + } } - .contextMenu() { - Button(role: .destructive) { - let eventToModify = viewModel.events.firstIndex() { currEvent in - currEvent.id == event.id - } - if let eventToModify = eventToModify { - viewModel.events.remove(at: eventToModify) - viewModel.saveEvents() - } - } label: { - Label("Delete", systemImage: "trash") + Spacer() + VStack { + Text("\(daysUntilEvent(event.date).long)") + .font(.subheadline) + .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one) + .multilineTextAlignment(.trailing) + } + CompleteEventButton( + viewModel: viewModel, + event: $event + ) + } + .transition(.opacity) + .padding(.vertical, 5) + .overlay( + RoundedRectangle(cornerRadius: 15) + .stroke(.one.opacity(0.5), lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 15)) + .fixedSize(horizontal: false, vertical: true) + .contextMenu() { + Button(role: .destructive) { + let eventToModify = viewModel.events.firstIndex() { currEvent in + currEvent.id == event.id } + if let eventToModify = eventToModify { + viewModel.events.remove(at: eventToModify) + viewModel.saveEvents() + } + } label: { + Label("Delete", systemImage: "trash") } } } - #endif +#endif } #Preview("EventListView") { diff --git a/NearFuture/Views/Home/HomeView.swift b/NearFuture/Views/Home/HomeView.swift index 8649647..0290efc 100644 --- a/NearFuture/Views/Home/HomeView.swift +++ b/NearFuture/Views/Home/HomeView.swift @@ -47,9 +47,13 @@ struct HomeView: View { ScrollView { // LazyVStack { ForEach(filteredEvents) { event in - EventListView(viewModel: viewModel, event: event) - .transition(.moveAndFade) - .id(event.complete) + NavigationLink() { + + } label: { + EventListView(viewModel: viewModel, event: event) + .transition(.moveAndFade) + .id(event.complete) + } } .padding(.horizontal) // } diff --git a/Shared/CompleteEventButton.swift b/Shared/CompleteEventButton.swift new file mode 100644 index 0000000..63cdd85 --- /dev/null +++ b/Shared/CompleteEventButton.swift @@ -0,0 +1,126 @@ +// +// CompleteEventButton.swift +// NearFuture +// +// Created by neon443 on 15/06/2025. +// + +import SwiftUI + +struct CompleteEventButton: View { + @ObservedObject var viewModel: EventViewModel + @Binding var event: Event + + @State var timer: Timer? + @State var largeTick: Bool = false + @State var completeInProgress: Bool = false + @State var completeStartTime: Date = .now + @State var progress: Double = 0 + private let completeDuration: TimeInterval = 3.0 + + var isMac: Bool { +#if canImport(AppKit) + return true +#else + return false +#endif + } + + func startCompleting() { +#if canImport(UIKit) + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() +#endif + withAnimation { completeInProgress = true } + completeStartTime = .now + progress = 0 + + timer = Timer(timeInterval: 0.01, repeats: true) { timer in + guard completeInProgress else { return } + guard timer.isValid else { return } + let elapsed = Date().timeIntervalSince(completeStartTime) + progress = min(1, elapsed) + + if progress > 1 { + timer.invalidate() + progress = 0 + withAnimation { completeInProgress = false } + viewModel.completeEvent(&event) + } + } + RunLoop.main.add(timer!, forMode: .common) + } + + var body: some View { + Group { + if completeInProgress { + ZStack { + CircularProgressView(progress: $progress) + Image(systemName: "xmark") + .bold() + } + .onTapGesture { + withAnimation { completeInProgress = false } + } + } else { + Image(systemName: event.complete ? "checkmark.circle.fill" : "circle") + .resizable().scaledToFit() + .foregroundStyle(event.complete ? .green : event.color.color) + .bold() + .onTapGesture { + startCompleting() + } + .onHover() { hovering in + withAnimation { + largeTick.toggle() + } + } + .scaleEffect(largeTick ? 1.5 : 1) + } + } + .frame(maxWidth: isMac ? 20 : 30) + .shadow(color: .one.opacity(0.2), radius: 2.5) + .padding(.trailing, isMac ? 15 : 5) + .transition(.scale) + .animation(.spring, value: completeInProgress) + .animation( + .spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2), + value: largeTick + ) + } +} + +struct CircularProgressView: View { + @Binding var progress: Double + var body: some View { + ZStack { + Circle() + .stroke( + .two, + lineWidth: 5 + ) + Circle() + .trim(from: 0, to: progress) + .stroke( + .one, + lineWidth: 5 +// style: StrokeStyle( +// lineWidth: 5, +// lineCap: .round +// ) + ) + .rotationEffect(.degrees(-90)) + } + } +} + +#Preview { + CompleteEventButton( + viewModel: dummyEventViewModel(), + event: .constant(dummyEventViewModel().example) + ) + .scaleEffect(5) +} + +#Preview { + CircularProgressView(progress: .constant(0.5)) +}