mirror of
https://github.com/neon443/NearFuture.git
synced 2026-03-11 06:49:12 +00:00
Compare commits
24 Commits
feat-mac
...
f3501a9379
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3501a9379 | ||
|
|
6f870ffa4f | ||
|
|
9841574d37 | ||
|
|
899304833c | ||
|
|
4629d4f75f | ||
|
|
be68c44ffe | ||
|
|
3fe9077e69 | ||
|
|
121dd79d54 | ||
|
|
01116c7fcb | ||
|
|
3690a9e4d2 | ||
|
|
c393404fec | ||
|
|
3ee22da036 | ||
|
|
2b25ddf9b3 | ||
|
|
01ff82181a | ||
|
|
b7ef7b4e19 | ||
|
|
7727f14ad4 | ||
|
|
d5580e52f5 | ||
|
|
84a7091e05 | ||
|
|
44b40894e4 | ||
|
|
e4842bd29a | ||
|
|
b378a831be | ||
|
|
2ff96a7093 | ||
|
|
4f5e31a6f3 | ||
|
|
266b27d817 |
@@ -12,6 +12,6 @@ TEAM_ID = 8JGND254B7
|
|||||||
BUNDLE_ID = com.neon443.NearFuture
|
BUNDLE_ID = com.neon443.NearFuture
|
||||||
BUNDLE_ID_WIDGETS = com.neon443.NearFuture.widgets
|
BUNDLE_ID_WIDGETS = com.neon443.NearFuture.widgets
|
||||||
GROUP_ID = group.NearFuture
|
GROUP_ID = group.NearFuture
|
||||||
VERSION = 5
|
VERSION = 5.0.1
|
||||||
NAME = Near Future
|
NAME = Near Future
|
||||||
BUILD_NUMBER = 1
|
BUILD_NUMBER = 52
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ struct ArchiveView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
ForEach(filteredEvents) { event in
|
ForEach(filteredEvents) { event in
|
||||||
EventListView(viewModel: viewModel, event: event)
|
EventListView(viewModel: viewModel, event: event)
|
||||||
|
.contextMenu() {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.removeEvent(event)
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
.tint(.red )
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ struct HomeView: View {
|
|||||||
if filteredEvents.contains(event) {
|
if filteredEvents.contains(event) {
|
||||||
EventListView(viewModel: viewModel, event: event)
|
EventListView(viewModel: viewModel, event: event)
|
||||||
.id(event)
|
.id(event)
|
||||||
|
.contextMenu() {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.removeEvent(event)
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,6 +639,7 @@
|
|||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -651,7 +652,7 @@
|
|||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -670,6 +671,7 @@
|
|||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -682,7 +684,7 @@
|
|||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -835,6 +837,7 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
@@ -863,7 +866,8 @@
|
|||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_STRICT_CONCURRENCY = complete;
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -887,6 +891,7 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
@@ -912,7 +917,8 @@
|
|||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_STRICT_CONCURRENCY = complete;
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
@@ -35,9 +35,17 @@ struct ArchiveView: View {
|
|||||||
)
|
)
|
||||||
} label: {
|
} label: {
|
||||||
EventListView(viewModel: viewModel, event: event)
|
EventListView(viewModel: viewModel, event: event)
|
||||||
.id(event.complete)
|
.id(event)
|
||||||
}
|
}
|
||||||
.transition(.moveAndFadeReversed)
|
.transition(.moveAndFadeReversed)
|
||||||
|
.contextMenu() {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.removeEvent(event)
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ struct AddEventView: View {
|
|||||||
@State var showNeedsNameAlert: Bool = false
|
@State var showNeedsNameAlert: Bool = false
|
||||||
@State var isSymbolPickerPresented: Bool = false
|
@State var isSymbolPickerPresented: Bool = false
|
||||||
|
|
||||||
@State private var bye: Bool = false
|
|
||||||
|
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
private enum Field {
|
private enum Field {
|
||||||
case Name, Notes
|
case Name, Notes
|
||||||
@@ -72,16 +70,16 @@ struct AddEventView: View {
|
|||||||
|
|
||||||
// dscription
|
// dscription
|
||||||
ZStack {
|
ZStack {
|
||||||
TextField("Event Notes", text: $event.notes)
|
if event.notes.isEmpty {
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
HStack {
|
||||||
.padding(.trailing, event.notes.isEmpty ? 0 : 30)
|
Text("Event Notes")
|
||||||
.animation(.spring, value: event.notes)
|
.opacity(0.5)
|
||||||
.focused($focusedField, equals: Field.Notes)
|
Spacer()
|
||||||
.submitLabel(.done)
|
|
||||||
.onSubmit {
|
|
||||||
focusedField = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextEditor(text: $event.notes)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
|
|
||||||
ColorPicker("Event Color", selection: $event.color.colorBind)
|
ColorPicker("Event Color", selection: $event.color.colorBind)
|
||||||
|
|
||||||
@@ -152,13 +150,14 @@ struct AddEventView: View {
|
|||||||
viewModel.addEvent(
|
viewModel.addEvent(
|
||||||
newEvent: event
|
newEvent: event
|
||||||
)
|
)
|
||||||
bye.toggle()
|
|
||||||
resetAddEventView()
|
resetAddEventView()
|
||||||
|
#if canImport(UIKit)
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
|
#endif
|
||||||
} label: {
|
} label: {
|
||||||
Label("Save", systemImage: "checkmark")
|
Label("Save", systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
.tint(.accent)
|
.tint(.accent)
|
||||||
.modifier(hapticSuccess(trigger: bye))
|
|
||||||
.disabled(event.name.isEmpty)
|
.disabled(event.name.isEmpty)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if event.name.isEmpty {
|
if event.name.isEmpty {
|
||||||
@@ -180,6 +179,9 @@ struct AddEventView: View {
|
|||||||
Button() {
|
Button() {
|
||||||
viewModel.editEvent(event)
|
viewModel.editEvent(event)
|
||||||
dismiss()
|
dismiss()
|
||||||
|
#if canImport(UIKit)
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
|
#endif
|
||||||
} label: {
|
} label: {
|
||||||
Label("Done", systemImage: "checkmark")
|
Label("Done", systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,19 +90,6 @@ struct EventListView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
openWindow(value: event.id)
|
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
|
#else
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -176,15 +163,9 @@ struct EventListView: View {
|
|||||||
)
|
)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.contextMenu() {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
let eventToModify = viewModel.events.firstIndex() { currEvent in
|
viewModel.removeEvent(event)
|
||||||
currEvent.id == event.id
|
|
||||||
}
|
|
||||||
if let eventToModify = eventToModify {
|
|
||||||
viewModel.events.remove(at: eventToModify)
|
|
||||||
viewModel.saveEvents()
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
@@ -196,16 +177,6 @@ struct EventListView: View {
|
|||||||
#Preview("EventListView") {
|
#Preview("EventListView") {
|
||||||
let vm = dummyEventViewModel()
|
let vm = dummyEventViewModel()
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black
|
|
||||||
VStack {
|
|
||||||
ForEach(0..<50) { _ in
|
|
||||||
Rectangle()
|
|
||||||
.foregroundStyle(randomColor().opacity(0.5))
|
|
||||||
.padding(-10)
|
|
||||||
}
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
.blur(radius: 5)
|
|
||||||
}
|
|
||||||
VStack {
|
VStack {
|
||||||
ForEach(vm.events) { event in
|
ForEach(vm.events) { event in
|
||||||
EventListView(
|
EventListView(
|
||||||
|
|||||||
@@ -59,9 +59,17 @@ struct HomeView: View {
|
|||||||
)
|
)
|
||||||
} label: {
|
} label: {
|
||||||
EventListView(viewModel: viewModel, event: event)
|
EventListView(viewModel: viewModel, event: event)
|
||||||
.id(event.complete)
|
.id(event)
|
||||||
}
|
}
|
||||||
.transition(.moveAndFade)
|
.transition(.moveAndFade)
|
||||||
|
.contextMenu() {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.removeEvent(event)
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -66,65 +66,19 @@ struct ImportView: View {
|
|||||||
fgColor = .yellow
|
fgColor = .yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.blur(radius: showAlert ? 2 : 0)
|
.alert("Are you sure?", isPresented: $showAlert) {
|
||||||
Group {
|
Button(role: .destructive) {
|
||||||
Rectangle()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.foregroundStyle(replaceCurrentEvents ? .red.opacity(0.25) : .black.opacity(0.2))
|
|
||||||
.animation(.default, value: replaceCurrentEvents)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
ZStack {
|
|
||||||
Rectangle()
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 25))
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
Text("Are you sure?")
|
|
||||||
.font(.largeTitle)
|
|
||||||
.bold()
|
|
||||||
.foregroundStyle(replaceCurrentEvents ? .red : .two)
|
|
||||||
.animation(.default, value: replaceCurrentEvents)
|
|
||||||
Text("This will replace your current events!")
|
|
||||||
.lineLimit(nil)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.opacity(replaceCurrentEvents ? 1 : 0)
|
|
||||||
.animation(.default, value: replaceCurrentEvents)
|
|
||||||
.foregroundStyle(.two)
|
|
||||||
Toggle("Replace Events", isOn: $replaceCurrentEvents)
|
|
||||||
.foregroundStyle(.two)
|
|
||||||
Spacer()
|
|
||||||
HStack {
|
|
||||||
Button() {
|
|
||||||
withAnimation {
|
|
||||||
showAlert.toggle()
|
|
||||||
}
|
|
||||||
importEvents()
|
importEvents()
|
||||||
} label: {
|
} label: {
|
||||||
Text("cancel")
|
Text("Replace Events")
|
||||||
.font(.title2)
|
|
||||||
.bold()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(BorderedProminentButtonStyle())
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button() {
|
Button() {
|
||||||
withAnimation {
|
|
||||||
showAlert.toggle()
|
|
||||||
}
|
|
||||||
importEvents()
|
importEvents()
|
||||||
} label: {
|
} label: {
|
||||||
Text("yes")
|
Text("Add to Events")
|
||||||
.font(.title2)
|
.foregroundStyle(.one)
|
||||||
.bold()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(BorderedProminentButtonStyle())
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 250, maxHeight: 250)
|
|
||||||
}
|
|
||||||
.opacity(showAlert ? 1 : 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,25 +154,25 @@ struct EventWidgetView: View {
|
|||||||
.foregroundColor(event.date < Date() ? .red : .primary)
|
.foregroundColor(event.date < Date() ? .red : .primary)
|
||||||
.padding(.trailing, -12)
|
.padding(.trailing, -12)
|
||||||
} else {
|
} else {
|
||||||
Button(
|
// Button(
|
||||||
intent: CompleteEvent(
|
// intent: CompleteEvent(
|
||||||
eventID: IntentParameter(
|
// eventID: IntentParameter(
|
||||||
title: LocalizedStringResource(
|
// title: LocalizedStringResource(
|
||||||
stringLiteral: event.id.uuidString
|
// stringLiteral: event.id.uuidString
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
) {
|
// ) {
|
||||||
if event.complete {
|
// if event.complete {
|
||||||
Circle()
|
// Circle()
|
||||||
.frame(width: 10)
|
// .frame(width: 10)
|
||||||
.foregroundStyle(.green)
|
// .foregroundStyle(.green)
|
||||||
} else {
|
// } else {
|
||||||
Circle()
|
// Circle()
|
||||||
.frame(width: 10)
|
// .frame(width: 10)
|
||||||
.foregroundStyle(.gray)
|
// .foregroundStyle(.gray)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Text(daysUntilEvent(event.date).long)
|
Text(daysUntilEvent(event.date).long)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,6 +1,32 @@
|
|||||||
# NearFuture
|
<div align="center">
|
||||||
|
<br/>
|
||||||
|
<p>
|
||||||
|
<img src="https://github.com/neon443/NearFuture/blob/main/Resources/Assets.xcassets/AppIcon.appiconset/NearFutureIcon.png?raw=true" title="dockphobia" alt="dockphobia icon" width="100" />
|
||||||
|
</p>
|
||||||
|
<h3>Near Future</h3>
|
||||||
|
<p>
|
||||||
|
<a href="https://apps.apple.com/us/app/near-future-event-tracker/id6744963429">
|
||||||
|
download
|
||||||
|
<img alt="GitHub Release" src="https://img.shields.io/itunes/v/6744963429">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
make your Dock scared of the mouse
|
||||||
|
<br/>
|
||||||
|
<a href="https://neon443.github.io">
|
||||||
|
made by neon443
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
|
||||||
[App Store](https://apps.apple.com/us/app/near-future-event-tracker/id6744963429)
|
<div align="center">
|
||||||
|
<a href="https://shipwrecked.hackclub.com/?t=ghrm" target="_blank">
|
||||||
|
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/739361f1d440b17fc9e2f74e49fc185d86cbec14_badge.png"
|
||||||
|
alt="This project is part of Shipwrecked, the world's first hackathon on an island!"
|
||||||
|
style="width: 25%;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
**Near Future** is a SwiftUI App to help people track upcoming events - Holidays, Trips, Birthdays, Weddings, Anniversaries.
|
**Near Future** is a SwiftUI App to help people track upcoming events - Holidays, Trips, Birthdays, Weddings, Anniversaries.
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ struct CompleteEventButton: View {
|
|||||||
@ObservedObject var viewModel: EventViewModel
|
@ObservedObject var viewModel: EventViewModel
|
||||||
@Binding var event: Event
|
@Binding var event: Event
|
||||||
|
|
||||||
@State var timer: Timer?
|
@MainActor @State var timer: Timer?
|
||||||
@State var largeTick: Bool = false
|
@State var largeTick: Bool = false
|
||||||
@State var completeInProgress: Bool = false
|
@State var completeInProgress: Bool = false
|
||||||
@State var completeStartTime: Date = .now
|
@State var completeStartTime: Date = .now
|
||||||
@@ -34,22 +34,30 @@ struct CompleteEventButton: View {
|
|||||||
completeStartTime = .now
|
completeStartTime = .now
|
||||||
progress = 0
|
progress = 0
|
||||||
|
|
||||||
timer = Timer(timeInterval: 0.01, repeats: true) { timer in
|
timer = Timer(timeInterval: 0.02, repeats: true) { timer in
|
||||||
|
DispatchQueue.main.async {
|
||||||
guard completeInProgress else { return }
|
guard completeInProgress else { return }
|
||||||
|
guard let timer = self.timer else { return }
|
||||||
guard timer.isValid else { return }
|
guard timer.isValid else { return }
|
||||||
let elapsed = Date().timeIntervalSince(completeStartTime)
|
let elapsed = Date().timeIntervalSince(completeStartTime)
|
||||||
progress = min(1, elapsed)
|
progress = min(1, elapsed)
|
||||||
|
#if canImport(UIKit)
|
||||||
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
|
#endif
|
||||||
|
|
||||||
if progress >= 1 {
|
if progress >= 1 {
|
||||||
withAnimation { completeInProgress = false }
|
withAnimation { completeInProgress = false }
|
||||||
viewModel.completeEvent(&event)
|
viewModel.completeEvent(&event)
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()+0.02) {
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
progress = 0
|
progress = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
RunLoop.main.add(timer!, forMode: .common)
|
RunLoop.main.add(timer!, forMode: .common)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ extension View {
|
|||||||
extension AnyTransition {
|
extension AnyTransition {
|
||||||
static var moveAndFade: AnyTransition {
|
static var moveAndFade: AnyTransition {
|
||||||
.asymmetric(
|
.asymmetric(
|
||||||
insertion: .move(edge: .leading),
|
insertion: .opacity,
|
||||||
removal: .move(edge: .trailing)
|
removal: .move(edge: .trailing)
|
||||||
)
|
)
|
||||||
.combined(with: .opacity)
|
.combined(with: .opacity)
|
||||||
}
|
}
|
||||||
static var moveAndFadeReversed: AnyTransition {
|
static var moveAndFadeReversed: AnyTransition {
|
||||||
.asymmetric(
|
.asymmetric(
|
||||||
insertion: .move(edge: .trailing),
|
insertion: .opacity,
|
||||||
removal: .move(edge: .leading)
|
removal: .move(edge: .leading)
|
||||||
)
|
)
|
||||||
.combined(with: .opacity)
|
.combined(with: .opacity)
|
||||||
|
|||||||
@@ -160,20 +160,24 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
|||||||
eventUUIDs.remove(at: remove)
|
eventUUIDs.remove(at: remove)
|
||||||
}
|
}
|
||||||
let components = getDateComponents(events[index].date)
|
let components = getDateComponents(events[index].date)
|
||||||
|
|
||||||
//check the notif matches event details
|
//check the notif matches event details
|
||||||
if req.content.title == events[index].name,
|
if req.content.title == events[index].name,
|
||||||
req.content.subtitle == events[index].notes,
|
req.content.subtitle == events[index].notes,
|
||||||
req.trigger == UNCalendarNotificationTrigger(dateMatching: components, repeats: false) {
|
req.trigger == UNCalendarNotificationTrigger(dateMatching: components, repeats: false) {
|
||||||
//if it does, make sure the notif delets if u complete the veent
|
//if it does, make sure the notif delets if u complete the veent or in the past
|
||||||
if events[index].complete {
|
if events[index].complete || events[index].date > .now {
|
||||||
cancelNotif(req.identifier)
|
cancelNotif(req.identifier)
|
||||||
|
} else {
|
||||||
|
//dont cancel the notif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//reschedult it because the event details have changed
|
||||||
cancelNotif(req.identifier)
|
cancelNotif(req.identifier)
|
||||||
scheduleEventNotif(events[index])
|
scheduleEventNotif(events[index])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//cancel if the event is deleted
|
//cancel notif if the event is deleted (doesnt exist/cannot be matched)
|
||||||
cancelNotif(req.identifier)
|
cancelNotif(req.identifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +186,14 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
|||||||
scheduleEventNotif(event)
|
scheduleEventNotif(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task {
|
||||||
|
try? await UNUserNotificationCenter.current().setBadgeCount(await getNotifs().count)
|
||||||
|
}
|
||||||
|
print(eventUUIDs.count)
|
||||||
|
print(events.count(where: {!$0.complete && $0.date < .now}))
|
||||||
|
print(events.count(where: {!$0.complete && $0.date > .now}))
|
||||||
|
print(events.count(where: {!$0.complete}))
|
||||||
|
print(events.count(where: {$0.complete}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// save to local and icloud
|
// save to local and icloud
|
||||||
@@ -240,8 +252,13 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeEvent(at index: IndexSet) {
|
func removeEvent(_ eventToRemove: Event) {
|
||||||
events.remove(atOffsets: index)
|
let eventToModify = self.events.firstIndex() { currEvent in
|
||||||
|
currEvent.id == eventToRemove.id
|
||||||
|
}
|
||||||
|
if let eventToModify = eventToModify {
|
||||||
|
self.events.remove(at: eventToModify)
|
||||||
|
}
|
||||||
saveEvents() //sync local and icl
|
saveEvents() //sync local and icl
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,6 +507,7 @@ func getBuildID() -> String {
|
|||||||
return "\(build)"
|
return "\(build)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func getDevice() -> (sf: String, label: String) {
|
func getDevice() -> (sf: String, label: String) {
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
let asi = ProcessInfo().isiOSAppOnMac
|
let asi = ProcessInfo().isiOSAppOnMac
|
||||||
@@ -507,57 +525,3 @@ func getDevice() -> (sf: String, label: String) {
|
|||||||
return (sf: "desktopcomputer", label: "Mac")
|
return (sf: "desktopcomputer", label: "Mac")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Event: AppEntity {
|
|
||||||
static let defaultQuery = EventQuery()
|
|
||||||
|
|
||||||
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
|
||||||
TypeDisplayRepresentation("skdfj")
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayRepresentation: DisplayRepresentation {
|
|
||||||
DisplayRepresentation("eventsss")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EventQuery: EntityQuery, DynamicOptionsProvider {
|
|
||||||
typealias Entity = Event
|
|
||||||
@Dependency var vm: EventViewModel
|
|
||||||
func results() async throws -> some ResultsCollection {
|
|
||||||
return vm.events
|
|
||||||
}
|
|
||||||
// func defaultResult() async -> DefaultValue? {
|
|
||||||
// return vm.events[0]
|
|
||||||
// }
|
|
||||||
func entities(for identifiers: [Entity.ID]) async throws -> [Entity] {
|
|
||||||
return vm.events
|
|
||||||
}
|
|
||||||
func suggestedEntities() async throws -> some ResultsCollection {
|
|
||||||
return vm.events //lol cba
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompleteEvent: AppIntent {
|
|
||||||
static var title: LocalizedStringResource = "Complete An Event"
|
|
||||||
static var description = IntentDescription("Mark an Event as complete.")
|
|
||||||
|
|
||||||
@Parameter(title: "Event ID")
|
|
||||||
var eventID: String
|
|
||||||
|
|
||||||
func perform() async throws -> some IntentResult {
|
|
||||||
print("s")
|
|
||||||
let viewModel = EventViewModel()
|
|
||||||
print("hip")
|
|
||||||
guard let eventUUID = UUID(uuidString: eventID) else {
|
|
||||||
print(":sdklfajk")
|
|
||||||
return .result()
|
|
||||||
}
|
|
||||||
print("hii")
|
|
||||||
if let eventToModify = viewModel.events.firstIndex(where: { $0.id == eventUUID }) {
|
|
||||||
print("hiii")
|
|
||||||
viewModel.events[eventToModify].complete = true
|
|
||||||
viewModel.saveEvents()
|
|
||||||
}
|
|
||||||
return .result()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct NFSettings: Codable, Equatable {
|
|||||||
var prevAppVersion: String = getVersion()+getBuildID()
|
var prevAppVersion: String = getVersion()+getBuildID()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class SettingsViewModel: ObservableObject {
|
class SettingsViewModel: ObservableObject {
|
||||||
@Published var settings: NFSettings = NFSettings()
|
@Published var settings: NFSettings = NFSettings()
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ class SettingsViewModel: ObservableObject {
|
|||||||
"pink"
|
"pink"
|
||||||
]
|
]
|
||||||
|
|
||||||
@Published var device: (sf: String, label: String)
|
@Published var device: (sf: String, label: String) = ("", "")
|
||||||
|
|
||||||
init(load: Bool = true) {
|
init(load: Bool = true) {
|
||||||
self.device = getDevice()
|
self.device = getDevice()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class SymbolsLoader: ObservableObject {
|
class SymbolsLoader: ObservableObject {
|
||||||
@Published var allSymbols: [String] = []
|
private var allSymbols: [String] = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.allSymbols = getAllSymbols()
|
self.allSymbols = getAllSymbols()
|
||||||
@@ -16,7 +16,7 @@ class SymbolsLoader: ObservableObject {
|
|||||||
|
|
||||||
func getSymbols(_ searched: String) -> [String] {
|
func getSymbols(_ searched: String) -> [String] {
|
||||||
if searched.isEmpty {
|
if searched.isEmpty {
|
||||||
return allSymbols
|
return []
|
||||||
} else {
|
} else {
|
||||||
return allSymbols.filter() { $0.localizedCaseInsensitiveContains(searched) }
|
return allSymbols.filter() { $0.localizedCaseInsensitiveContains(searched) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,17 @@ struct SymbolsPicker: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
if symbols.isEmpty {
|
if searchInput.isEmpty {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.resizable().scaledToFit()
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("Start a Search")
|
||||||
|
.font(.title)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else if symbols.isEmpty {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
.resizable().scaledToFit()
|
.resizable().scaledToFit()
|
||||||
|
|||||||
@@ -8,48 +8,49 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct hapticHeavy: ViewModifier {
|
struct hapticHeavy<T: Equatable>: ViewModifier {
|
||||||
var trigger: any Equatable
|
var trigger: T
|
||||||
|
|
||||||
init(trigger: any Equatable) {
|
init(trigger: T) {
|
||||||
self.trigger = trigger
|
self.trigger = trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(iOS 17, *) {
|
|
||||||
content
|
content
|
||||||
.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: trigger)
|
.onChange(of: trigger) { _ in
|
||||||
} else {
|
#if canImport(UIKit)
|
||||||
|
UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct hapticSuccess<T: Equatable>: ViewModifier {
|
||||||
|
var trigger: T
|
||||||
|
|
||||||
|
init(trigger: T) {
|
||||||
|
self.trigger = trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
|
.onChange(of: trigger) { _ in
|
||||||
|
#if canImport(UIKit)
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct glassButton: ViewModifier {
|
struct glassButton: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(iOS 19, macOS 16, *) {
|
#if swift(>=6.2)
|
||||||
content.buttonStyle(.glass)
|
content.buttonStyle(.glass)
|
||||||
} else {
|
#else
|
||||||
content.buttonStyle(.borderedProminent)
|
content.buttonStyle(.borderedProminent)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||||
.tint(.two)
|
.tint(.two)
|
||||||
}
|
#endif
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct hapticSuccess: ViewModifier {
|
|
||||||
var trigger: any Equatable
|
|
||||||
|
|
||||||
init(trigger: any Equatable) {
|
|
||||||
self.trigger = trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(iOS 17, *) {
|
|
||||||
content.sensoryFeedback(.success, trigger: trigger)
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user