7 Commits

Author SHA1 Message Date
neon443
e4107a2faa add success haptic and indentation 2025-06-15 21:24:15 +01:00
neon443
5ec16dd67a fix editing events on mac and ios
toolbar label()s instead of buton("label")s
fix event add/delete/tick animation
fix event complete progress on ios and mac and that it just spazzed out
browsing true for symbols browsing (not picking)
2025-06-15 21:18:58 +01:00
neon443
0cffe243eb completeevent button has a custo progress bar to work on ios aswell
extracted the com[lete button
2025-06-15 18:55:38 +01:00
neon443
6533fb85ed fix editing events 2025-06-15 15:41:37 +01:00
neon443
5c667679d5 fix "new edit event window" on mac not working
- it would save events with the same id and modify the previous one if u opened another one
add symbols to ios
date picker looks better mac
2025-06-15 15:34:08 +01:00
neon443
5dd25f1ede 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
2025-06-14 21:32:11 +01:00
neon443
d80011ea27 remove my shitty uikit 2025-06-14 18:55:09 +01:00
15 changed files with 432 additions and 551 deletions

View File

@@ -32,23 +32,26 @@ struct NearFutureApp: App {
}
WindowGroup("Edit Event", for: Event.ID.self) { $eventID in
EditEventView(
viewModel: viewModel,
event: Binding(
get: {
viewModel.events.first(where: {$0.id == eventID}) ?? viewModel.template
},
set: { newValue in
if let eventIndex = viewModel.events.firstIndex(where: {
$0.id == eventID
}) {
viewModel.events[eventIndex] = newValue
}
viewModel.saveEvents()
}
if viewModel.events.first(where: {$0.id == eventID}) == nil {
AddEventView(
viewModel: viewModel
)
)
} else {
EditEventView(
viewModel: viewModel,
event: Binding(
get: {
viewModel.events.first(where: {$0.id == eventID}) ?? viewModel.template
},
set: { newValue in
viewModel.editEvent(newValue)
}
)
)
}
}
.defaultSize(width: 480, height: 550)
.windowIdealSize(.fitToContent)
.restorationBehavior(.disabled)
Window("About Near Future", id: "about") {

View File

@@ -14,7 +14,7 @@ struct ContentView: View {
@State private var showAddEventView: Bool = false
@State private var symbolSearchInput: String = ""
var body: some View {
var body: some View {
NavigationSplitView {
List {
NavigationLink {
@@ -37,7 +37,8 @@ struct ContentView: View {
}
NavigationLink {
SymbolsPicker(
selection: $symbolSearchInput
selection: .constant(""),
browsing: true
)
} label: {
Image(systemName: "star.circle")
@@ -65,9 +66,7 @@ struct ContentView: View {
}
.sheet(isPresented: $showAddEventView) {
AddEventView(
viewModel: viewModel,
event: $viewModel.editableTemplate,
adding: true
viewModel: viewModel
)
.presentationSizing(.page)
}
@@ -75,11 +74,10 @@ struct ContentView: View {
Button() {
showAddEventView.toggle()
} label: {
Image(systemName: "plus")
Text("New")
Label("New", systemImage: "plus")
}
}
}
}
}
#Preview {

View File

@@ -1,180 +0,0 @@
//
// EventListView.swift
// MacNearFuture
//
// Created by neon443 on 21/05/2025.
//
import SwiftUI
struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
@State var largeTick: Bool = false
@State var hovering: Bool = false
@Environment(\.openWindow) var openWindow
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)
}
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()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.bold()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.onHover() { hovering in
withAnimation {
largeTick.toggle()
}
}
.buttonStyle(.borderless)
.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")
}
}
}
}
#Preview("EventListView") {
let vm = dummyEventViewModel()
ZStack {
Color.black
VStack {
ForEach(0..<50) { _ in
Rectangle()
.foregroundStyle(randomColor().opacity(0.5))
.padding(-10)
}
.ignoresSafeArea(.all)
.blur(radius: 5)
}
VStack {
ForEach(vm.events) { event in
EventListView(
viewModel: vm,
event: event
)
}
}
.padding(.horizontal, 10)
}
}
#Preview {
EventListView(
viewModel: dummyEventViewModel(),
event: dummyEventViewModel().template
)
}

View File

@@ -70,15 +70,12 @@
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 */; };
A9C769A22DFDD1FC00082FFF /* SymbolsPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A9C769A02DFDD1FC00082FFF /* SymbolsPicker.storyboard */; };
A9C769A32DFDD1FC00082FFF /* SymbolsPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A9C769A02DFDD1FC00082FFF /* SymbolsPicker.storyboard */; };
A9C769A52DFDD27500082FFF /* SymbolsPickerStoryboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C769A42DFDD27500082FFF /* SymbolsPickerStoryboard.swift */; };
A9C769A62DFDD27500082FFF /* SymbolsPickerStoryboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C769A42DFDD27500082FFF /* SymbolsPickerStoryboard.swift */; };
A9C769A72DFDD27500082FFF /* SymbolsPickerStoryboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C769A42DFDD27500082FFF /* SymbolsPickerStoryboard.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 */
@@ -159,12 +156,10 @@
A979F6092D270AF00094C0B3 /* NearFutureWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsBundle.swift; sourceTree = "<group>"; };
A979F60B2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsLiveActivity.swift; sourceTree = "<group>"; };
A979F60F2D270AF80094C0B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A98C20CA2DE730740008D61C /* EventListViewMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListViewMac.swift; sourceTree = "<group>"; };
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; };
A98C20CF2DE731BD0008D61C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
A98C20D32DE7339E0008D61C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
A9C769A02DFDD1FC00082FFF /* SymbolsPicker.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SymbolsPicker.storyboard; sourceTree = "<group>"; };
A9C769A42DFDD27500082FFF /* SymbolsPickerStoryboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolsPickerStoryboard.swift; sourceTree = "<group>"; };
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteEventButton.swift; sourceTree = "<group>"; };
A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -222,7 +217,6 @@
children = (
A90D49332DDE0FAF00781124 /* ContentViewMac.swift */,
A98C20CF2DE731BD0008D61C /* HomeView.swift */,
A98C20CA2DE730740008D61C /* EventListViewMac.swift */,
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */,
A91EF80F2DFCB66C00B8463D /* SettingsView.swift */,
);
@@ -236,6 +230,7 @@
A90D49512DDE2D0000781124 /* Extensions.swift */,
A90D49202DDE0A3B00781124 /* Model */,
A91EF80A2DFC910000B8463D /* ViewModifiers.swift */,
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */,
);
path = Shared;
sourceTree = "<group>";
@@ -252,8 +247,6 @@
children = (
A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */,
A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */,
A9C769A02DFDD1FC00082FFF /* SymbolsPicker.storyboard */,
A9C769A42DFDD27500082FFF /* SymbolsPickerStoryboard.swift */,
);
path = SymbolsPicker;
sourceTree = "<group>";
@@ -516,7 +509,6 @@
files = (
A90D49442DDE1C7600781124 /* Tints.xcassets in Resources */,
A920C2922D24011A00E4F9B1 /* Preview Assets.xcassets in Resources */,
A9C769A22DFDD1FC00082FFF /* SymbolsPicker.storyboard in Resources */,
A949F8322DCAAA8A0064DCA0 /* NearFutureIcon.png in Resources */,
A920C28E2D24011A00E4F9B1 /* Assets.xcassets in Resources */,
);
@@ -527,7 +519,6 @@
buildActionMask = 2147483647;
files = (
A90D49462DDE1C7A00781124 /* Tints.xcassets in Resources */,
A9C769A32DFDD1FC00082FFF /* SymbolsPicker.storyboard in Resources */,
A979F6102D270AF90094C0B3 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -539,11 +530,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A9C769A52DFDD27500082FFF /* SymbolsPickerStoryboard.swift in Sources */,
A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */,
A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */,
A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */,
A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */,
A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */,
A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */,
@@ -551,6 +541,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 */,
@@ -588,8 +579,8 @@
A949F8512DCAABE00064DCA0 /* ExportView.swift in Sources */,
A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */,
A90D49532DDE2D0000781124 /* Extensions.swift in Sources */,
A9C769A62DFDD27500082FFF /* SymbolsPickerStoryboard.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 */,
@@ -609,7 +600,6 @@
A95E9EDA2DFC742B00ED655F /* AccentIcon.swift in Sources */,
A91EF80D2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A91EF8182DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A9C769A72DFDD27500082FFF /* SymbolsPickerStoryboard.swift in Sources */,
A91EF8072DFC8B8B00B8463D /* ColorCodable.swift in Sources */,
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */,
A979F60C2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift in Sources */,

View File

@@ -23,18 +23,31 @@ struct ArchiveView: View {
} else {
ScrollView {
ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: event)
.transition(.moveAndFadeReversed)
.id(event.complete)
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: Binding(
get: { event },
set: { newValue in
viewModel.editEvent(newValue)
}
)
)
} label: {
EventListView(viewModel: viewModel, event: event)
.id(event.complete)
}
.transition(.moveAndFadeReversed)
}
.padding(.horizontal)
}
.animation(.default, value: filteredEvents)
}
}
.transition(.opacity)
.scrollContentBackground(.hidden)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
AddEventButton(showingAddEventView: $showAddEvent)
}
}
@@ -43,9 +56,7 @@ struct ArchiveView: View {
}
.sheet(isPresented: $showAddEvent) {
AddEventView(
viewModel: viewModel,
event: $viewModel.editableTemplate,
adding: true
viewModel: viewModel
)
}
}

View File

@@ -12,6 +12,7 @@ import SwiftData
enum Tab {
case home
case archive
case symbols
case stats
case settings
}
@@ -33,6 +34,14 @@ struct ContentView: View {
Label("Archive", systemImage: "tray.full")
}
.tag(Tab.archive)
SymbolsPicker(
selection: .constant(""),
browsing: true
)
.tabItem {
Label("Symbols", systemImage: "star.circle")
}
.tag(Tab.symbols)
StatsView(viewModel: viewModel)
// SymbolsPickerStoryboardUIViewRepresentable()
.tabItem {

View File

@@ -10,9 +10,9 @@ import SwiftUI
struct AddEventView: View {
@ObservedObject var viewModel: EventViewModel
@Binding var event: Event
@State var event: Event = dummyEventViewModel().template
@State var adding: Bool
@State var adding: Bool = true
@State var showNeedsNameAlert: Bool = false
@State var isSymbolPickerPresented: Bool = false
@@ -25,6 +25,14 @@ struct AddEventView: View {
@Environment(\.dismiss) var dismiss
var isMac: Bool {
if #available(iOS 1, *) {
return false
} else {
return true
}
}
var body: some View {
ZStack {
if !adding {
@@ -75,6 +83,7 @@ struct AddEventView: View {
}
}
ColorPicker("Event Color", selection: $event.color.colorBind)
// date picker
HStack {
@@ -82,6 +91,8 @@ struct AddEventView: View {
DatePicker("", selection: $event.date, displayedComponents: .date)
#if os(iOS)
.datePickerStyle(.wheel)
#else
.datePickerStyle(.graphical)
#endif
Spacer()
Button() {
@@ -100,6 +111,9 @@ struct AddEventView: View {
selection: $event.date,
displayedComponents: .hourAndMinute
)
#if os(macOS)
.datePickerStyle(.stepperField)
#endif
// re-ocurrence Picker
Picker("Recurrence", selection: $event.recurrence) {
@@ -141,10 +155,7 @@ struct AddEventView: View {
bye.toggle()
resetAddEventView()
} label: {
Text("Save")
.font(.headline)
.cornerRadius(10)
.buttonStyle(BorderedProminentButtonStyle())
Label("Save", systemImage: "checkmark")
}
.tint(.accent)
.modifier(hapticSuccess(trigger: bye))
@@ -164,9 +175,21 @@ struct AddEventView: View {
}
}
}
ToolbarItem(placement: .confirmationAction) {
if !adding {
Button() {
viewModel.editEvent(event)
dismiss()
} label: {
Label("Done", systemImage: "checkmark")
}
.disabled(event.name == "")
}
}
}
.navigationTitle("Editing \(event.name) - Ne")
}
.scrollContentBackground(.hidden)
.scrollContentBackground(isMac ? .automatic : .hidden)
.presentationDragIndicator(.visible)
}
}
@@ -185,7 +208,6 @@ struct AddEventView: View {
.sheet(isPresented: .constant(true)) {
AddEventView(
viewModel: vm,
event: .constant(vm.template),
adding: true
)
}

View File

@@ -12,37 +12,13 @@ struct EditEventView: View {
@ObservedObject var viewModel: EventViewModel
@Binding var event: Event
fileprivate func saveEdits() {
//if there is an event in vM.events with the id of the event we r editing,
//firstindex - loops through the arr and finds first element where that events id matches editing event's id
if let index = viewModel.events.firstIndex(where: { xEvent in
xEvent.id == event.id
}) {
viewModel.events[index] = event
}
viewModel.saveEvents()
dismiss()
}
var body: some View {
AddEventView(
viewModel: viewModel,
event: $event,
event: event,
adding: false //bc we editing existing event
)
.navigationTitle("Edit Event")
.toolbar {
ToolbarItem(/*placement: .topBarTrailing*/) {
Button() {
saveEdits()
} label: {
Text("Done")
.bold()
}
.disabled(event.name == "")
}
}
}
}

View File

@@ -12,137 +12,185 @@ struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
@Environment(\.openWindow) var openWindow
@State var hovering: Bool = false
#if canImport(AppKit)
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
)
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
)
)
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
)
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
)
.font(.subheadline)
)
.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)
}
CompleteEventButton(
viewModel: viewModel,
event: $event
)
}
.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 {
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
)
)
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() {
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()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.bold()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.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
)
}
.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
}
#Preview("EventListView") {

View File

@@ -12,7 +12,6 @@ struct HomeView: View {
@ObservedObject var viewModel: EventViewModel
@ObservedObject var settingsModel: SettingsViewModel
@State private var event: Event = dummyEventViewModel().template
@State private var showingAddEventView: Bool = false
@State private var searchInput: String = ""
@Environment(\.colorScheme) var appearance
@@ -48,9 +47,21 @@ struct HomeView: View {
ScrollView {
// LazyVStack {
ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: event)
.transition(.moveAndFade)
.id(event.complete)
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: Binding(
get: { event },
set: { newValue in
viewModel.editEvent(newValue)
}
)
)
} label: {
EventListView(viewModel: viewModel, event: event)
.id(event.complete)
}
.transition(.moveAndFade)
}
.padding(.horizontal)
// }
@@ -70,13 +81,11 @@ struct HomeView: View {
.modifier(navigationInlineLarge())
.sheet(isPresented: $showingAddEventView) {
AddEventView(
viewModel: viewModel,
event: $event,
adding: true //adding event
viewModel: viewModel
)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
AddEventButton(showingAddEventView: $showingAddEventView)
}
}

View File

@@ -0,0 +1,129 @@
//
// 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 {
withAnimation { completeInProgress = false }
viewModel.completeEvent(&event)
#if canImport(UIKit)
UINotificationFeedbackGenerator().notificationOccurred(.success)
#endif
timer.invalidate()
progress = 0
}
}
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))
}

View File

@@ -222,6 +222,24 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
saveEvents() //sync with icloud
}
func editEvent(_ editedEvent: Event) {
if let index = events.firstIndex(where: { editedEvent.id == $0.id }) {
self.events[index] = editedEvent
saveEvents()
}
}
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

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController storyboardIdentifier="ViewController" id="Y6W-OH-hqX" customClass="ViewController" customModule="NearFuture" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="xBf-3w-4Ao">
<rect key="frame" x="0.0" y="63" width="393" height="666"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="d2N-B5-D5N">
<size key="itemSize" width="100" height="100"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="symbolCell" id="nKx-2O-Nq5" customClass="SymbolCell" customModule="NearFuture" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="3DG-aV-ZH4">
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AaY-Po-OoS">
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gGM-kQ-cA7">
<rect key="frame" x="29" y="79" width="42" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</collectionViewCellContentView>
<connections>
<outlet property="imageView" destination="AaY-Po-OoS" id="Swa-vO-Mtz"/>
<outlet property="textLabel" destination="gGM-kQ-cA7" id="Qei-Qd-rn5"/>
</connections>
</collectionViewCell>
</cells>
</collectionView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<connections>
<outlet property="collectionView" destination="xBf-3w-4Ao" id="c9h-Ew-aOX"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139.69465648854961" y="65.492957746478879"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -14,6 +14,8 @@ struct SymbolsPicker: View {
@FocusState var searchfocuesd: Bool
@State var searchInput: String = ""
@State var browsing: Bool = false
@Environment(\.dismiss) var dismiss
var symbols: [String] {
return symbolsLoader.getSymbols(searchInput)
@@ -45,6 +47,8 @@ struct SymbolsPicker: View {
ForEach(symbols, id: \.self) { symbol in
Button() {
selection = symbol
searchInput = ""
dismiss()
} label: {
VStack {
Image(systemName: symbol)
@@ -62,7 +66,19 @@ struct SymbolsPicker: View {
}
}
}
.searchable(text: $searchInput)
}
.searchable(text: $searchInput)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
if !browsing {
Button() {
searchInput = ""
dismiss()
} label: {
Label("Cancel", systemImage: "xmark")
}
}
}
}
}
}

View File

@@ -1,92 +0,0 @@
//
// SymbolsPickerStoryboard.swift
// NearFuture
//
// Created by neon443 on 14/06/2025.
//
import Foundation
import SwiftUI
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
var symbolLoader: SymbolsLoader = SymbolsLoader()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(
width: 100,
height: 100
)
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
symbolLoader.allSymbols.count
}
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
section
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "symbolCell", for: indexPath) as! SymbolCell
let imageView = cell.imageView
imageView?.image = UIImage(systemName: symbolLoader.allSymbols[indexPath.item])!
cell.textLabel?.text = "hi\(indexPath.row)"
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.item)
}
}
class SymbolCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var textLabel: UILabel!
}
struct SymbolsPickerStoryboardUIViewRepresentable: UIViewRepresentable {
class Coordinator {
var viewController: ViewController?
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> some UIView {
let storyboard = UIStoryboard(name: "SymbolsPicker", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
context.coordinator.viewController = viewController
return viewController.view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print()
}
}