From 5dd25f1ede0018bc9046ba49e18321acdaf1a19b Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:32:11 +0100 Subject: [PATCH] locked the freak in for this one - can cancel events being completed!!!!! added a cancel to symbolpicker and made it dip after choosing extracted completevent into viewmodel eventlistview is pretty full rn -- need to cleana added color picker ot addeventview wtf why wasnt it there --- MacNearFuture/MacNearFutureApp.swift | 1 + MacNearFuture/Views/EventListViewMac.swift | 59 ++++-- NearFuture.xcodeproj/project.pbxproj | 4 +- NearFuture/Views/Events/AddEventView.swift | 14 +- NearFuture/Views/Home/EventListView.swift | 196 ++++++++++++++++-- Shared/Model/Events.swift | 11 + .../Model/SymbolsPicker/SymbolsPicker.swift | 13 +- 7 files changed, 249 insertions(+), 49 deletions(-) diff --git a/MacNearFuture/MacNearFutureApp.swift b/MacNearFuture/MacNearFutureApp.swift index 618ffee..0db9918 100644 --- a/MacNearFuture/MacNearFutureApp.swift +++ b/MacNearFuture/MacNearFutureApp.swift @@ -49,6 +49,7 @@ struct NearFutureApp: App { ) ) } + .windowIdealSize(.fitToContent) .restorationBehavior(.disabled) Window("About Near Future", id: "about") { diff --git a/MacNearFuture/Views/EventListViewMac.swift b/MacNearFuture/Views/EventListViewMac.swift index 7d86eb0..ab12c44 100644 --- a/MacNearFuture/Views/EventListViewMac.swift +++ b/MacNearFuture/Views/EventListViewMac.swift @@ -14,8 +14,31 @@ struct EventListView: View { @State var largeTick: Bool = false @State var hovering: Bool = false + @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 + func startCompleting() { + NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .now) + completeInProgress = true + progress = 0 + completeStartTime = .now + + timer = Timer(timeInterval: 0.05, repeats: true) { timer in + let elapsed = Date().timeIntervalSince(completeStartTime) + progress = min(elapsed, 1.0) + + if progress >= 1.0 { + timer.invalidate() + viewModel.completeEvent(&event) + completeInProgress = false + } + } + RunLoop.main.add(timer!, forMode: .common) + } var body: some View { ZStack { Color.black.opacity(hovering ? 0.5 : 0.0) @@ -75,33 +98,20 @@ struct EventListView: View { .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one) } Button() { - withAnimation { - event.complete.toggle() - } - let eventToModify = viewModel.events.firstIndex() { currEvent in - currEvent.id == event.id - } - if let eventToModify = eventToModify { - viewModel.events[eventToModify] = event - viewModel.saveEvents() - } + startCompleting() } label: { - if event.complete { + if completeInProgress { ZStack { - Circle() - .foregroundStyle(.green) - Image(systemName: "checkmark") - .resizable() - .foregroundStyle(.white) - .scaledToFit() + ProgressView(value: progress) + .progressViewStyle(.circular) + Image(systemName: "xmark") .bold() - .frame(width: 15) } } else { - Image(systemName: "circle") - .resizable() - .scaledToFit() - .foregroundStyle(event.color.color) + Image(systemName: event.complete ? "checkmark.circle.fill" : "circle") + .resizable().scaledToFit() + .foregroundStyle(event.complete ? .green : event.color.color) + .bold() } } .onHover() { hovering in @@ -110,7 +120,10 @@ struct EventListView: View { } } .buttonStyle(.borderless) - .scaleEffect(largeTick ? 1.5 : 1) + .scaleEffect( + completeInProgress ? 1 : + largeTick ? 1.5 : 1 + ) .frame(maxWidth: 20) .shadow(radius: 5) .padding(.trailing, 15) diff --git a/NearFuture.xcodeproj/project.pbxproj b/NearFuture.xcodeproj/project.pbxproj index aca3f6b..b34967d 100644 --- a/NearFuture.xcodeproj/project.pbxproj +++ b/NearFuture.xcodeproj/project.pbxproj @@ -70,10 +70,10 @@ A979F6102D270AF90094C0B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A979F60F2D270AF80094C0B3 /* Assets.xcassets */; }; A979F6142D270AF90094C0B3 /* NearFutureWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A979F6022D270AF00094C0B3 /* NearFutureWidgetsExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A979F6182D2714310094C0B3 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Events.swift */; }; - A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CA2DE730740008D61C /* EventListViewMac.swift */; }; 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 */; }; + 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 */ @@ -531,7 +531,6 @@ A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */, A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */, A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */, - A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */, A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */, A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */, A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */, @@ -539,6 +538,7 @@ A95E9EE52DFC77E200ED655F /* ExportView.swift in Sources */, A91EF8132DFCC87D00B8463D /* EditEventView.swift in Sources */, A91EF8142DFCC87D00B8463D /* AddEventView.swift in Sources */, + A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */, A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */, A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */, A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */, diff --git a/NearFuture/Views/Events/AddEventView.swift b/NearFuture/Views/Events/AddEventView.swift index d8c172e..a17fa0f 100644 --- a/NearFuture/Views/Events/AddEventView.swift +++ b/NearFuture/Views/Events/AddEventView.swift @@ -25,6 +25,16 @@ struct AddEventView: View { @Environment(\.dismiss) var dismiss + var isMac: Bool { + if #available(iOS 1, *) { + return false + } else if #available(macOS 10, *) { + return true + } else { + return false + } + } + var body: some View { ZStack { if !adding { @@ -75,6 +85,7 @@ struct AddEventView: View { } } + ColorPicker("Event Color", selection: $event.color.colorBind) // date picker HStack { @@ -165,8 +176,9 @@ struct AddEventView: View { } } } + .navigationTitle("Editing \(event.name) - Ne") } - .scrollContentBackground(.hidden) + .scrollContentBackground(isMac ? .automatic : .hidden) .presentationDragIndicator(.visible) } } diff --git a/NearFuture/Views/Home/EventListView.swift b/NearFuture/Views/Home/EventListView.swift index e7003e8..3b1397a 100644 --- a/NearFuture/Views/Home/EventListView.swift +++ b/NearFuture/Views/Home/EventListView.swift @@ -12,6 +12,165 @@ 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) + var body: some View { + ZStack { + Color.black.opacity(hovering ? 0.5 : 0.0) + 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)") + .bold() + .foregroundStyle(.one) + .strikethrough(event.complete) + .multilineTextAlignment(.leading) + } + if !event.notes.isEmpty { + Text(event.notes) + .foregroundStyle(.one.opacity(0.8)) + .multilineTextAlignment(.leading) + } + Text( + event.date.formatted( + date: .long, + time: .shortened + ) + ) + .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)") + .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(radius: 5) + .padding(.trailing, 15) + .animation( + .spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2), + value: largeTick + ) + } + .transition(.opacity) + .fixedSize(horizontal: false, vertical: true) + } + .onHover { isHovering in + withAnimation { + hovering.toggle() + } + } + .onTapGesture { + openWindow(value: event.id) + } + .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") + } + } + } + #else var body: some View { NavigationLink() { EditEventView( @@ -79,33 +238,25 @@ struct EventListView: View { .multilineTextAlignment(.trailing) } Button() { - withAnimation { - event.complete.toggle() - } - let eventToModify = viewModel.events.firstIndex() { currEvent in - currEvent.id == event.id - } - if let eventToModify = eventToModify { - viewModel.events[eventToModify] = event - viewModel.saveEvents() - } + startCompleting() } label: { - if event.complete { + if completeInProgress { ZStack { - Circle() - .foregroundStyle(.green) - Image(systemName: "checkmark") - .resizable() - .foregroundStyle(.white) - .scaledToFit() + ProgressView(value: progress) + .progressViewStyle(.circular) + Image(systemName: "xmark") .bold() - .frame(width: 15) + } + .onTapGesture { + timer?.invalidate() + completeInProgress = false + progress = 0 } } else { - Image(systemName: "circle") - .resizable() - .scaledToFit() - .foregroundStyle(event.color.color) + Image(systemName: event.complete ? "checkmark.circle.fill" : "circle") + .resizable().scaledToFit() + .foregroundStyle(event.complete ? .green : event.color.color) + .bold() } } .buttonStyle(.borderless) @@ -143,6 +294,7 @@ struct EventListView: View { } } } + #endif } #Preview("EventListView") { diff --git a/Shared/Model/Events.swift b/Shared/Model/Events.swift index 78b9289..39a63f1 100644 --- a/Shared/Model/Events.swift +++ b/Shared/Model/Events.swift @@ -222,6 +222,17 @@ class EventViewModel: ObservableObject, @unchecked Sendable { saveEvents() //sync with icloud } + func completeEvent(_ event: inout Event) { + withAnimation { event.complete.toggle() } + let eventToModify = self.events.firstIndex() { currEvent in + currEvent.id == event.id + } + if let eventToModify = eventToModify { + self.events[eventToModify] = event + self.saveEvents() + } + } + func removeEvent(at index: IndexSet) { events.remove(atOffsets: index) saveEvents() //sync local and icl diff --git a/Shared/Model/SymbolsPicker/SymbolsPicker.swift b/Shared/Model/SymbolsPicker/SymbolsPicker.swift index f2ab309..011de62 100644 --- a/Shared/Model/SymbolsPicker/SymbolsPicker.swift +++ b/Shared/Model/SymbolsPicker/SymbolsPicker.swift @@ -14,6 +14,7 @@ struct SymbolsPicker: View { @FocusState var searchfocuesd: Bool @State var searchInput: String = "" + @Environment(\.dismiss) var dismiss var symbols: [String] { return symbolsLoader.getSymbols(searchInput) @@ -45,6 +46,8 @@ struct SymbolsPicker: View { ForEach(symbols, id: \.self) { symbol in Button() { selection = symbol + searchInput = "" + dismiss() } label: { VStack { Image(systemName: symbol) @@ -62,7 +65,15 @@ struct SymbolsPicker: View { } } } - .searchable(text: $searchInput) + } + .searchable(text: $searchInput) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + searchInput = "" + dismiss() + } + } } } }