Compare commits
22 Commits
rewrite
...
feat-notif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f52b423db | ||
|
|
7af96834d6 | ||
|
|
17ef4f1478 | ||
|
|
785dda0e2b | ||
|
|
5f5702d958 | ||
|
|
b88bc11b36 | ||
|
|
f8f5cdde26 | ||
|
|
169d51e6b9 | ||
|
|
afd6d23838 | ||
|
|
5877f3b525 | ||
|
|
4157520ff4 | ||
|
|
f03ea73813 | ||
|
|
dafd2ae0e8 | ||
|
|
1bbacd0caa | ||
|
|
29be23efc2 | ||
|
|
ffbd17fad8 | ||
|
|
c97d37711c | ||
|
|
80b193f449 | ||
|
|
48d086602e | ||
|
|
3450e8bf1c | ||
|
|
a08642bcb5 | ||
|
|
8ea1eda8f4 |
@@ -10,7 +10,8 @@
|
||||
|
||||
TEAM_ID = 8JGND254B7
|
||||
BUNDLE_ID = com.neon443.NearFuture
|
||||
BUNDLE_ID_WIDGETS = com.neon443.NearFuture.widgets
|
||||
GROUP_ID = group.NearFuture
|
||||
VERSION = 3.0.1
|
||||
VERSION = 4.0.0
|
||||
NAME = Near Future
|
||||
BUILD_NUMBER = 1
|
||||
BUILD_NUMBER = 0
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 609 KiB |
|
Before Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 70;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -11,14 +11,22 @@
|
||||
A920C28C2D24011400E4F9B1 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Item.swift */; };
|
||||
A920C28E2D24011A00E4F9B1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A920C28D2D24011A00E4F9B1 /* Assets.xcassets */; };
|
||||
A920C2922D24011A00E4F9B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A920C2912D24011A00E4F9B1 /* Preview Assets.xcassets */; };
|
||||
A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2B42D2401A100E4F9B1 /* SettingsView.swift */; };
|
||||
A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2B72D2401A300E4F9B1 /* AddEventView.swift */; };
|
||||
A920C2BE2D24021A00E4F9B1 /* SFSymbolsPicker in Frameworks */ = {isa = PBXBuildFile; productRef = A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */; };
|
||||
A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2C02D2403CA00E4F9B1 /* ContentView.swift */; };
|
||||
A93BC0942D2B18A3002E8BBD /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93BC0932D2B18A3002E8BBD /* StatsView.swift */; };
|
||||
A977CC922DBBB48000DED8C0 /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977CC912DBBB48000DED8C0 /* ArchiveView.swift */; };
|
||||
A977CC9A2DBD74FE00DED8C0 /* HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977CC992DBD74FE00DED8C0 /* HelpView.swift */; };
|
||||
A979F57F2D26B1300094C0B3 /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F57E2D26B1300094C0B3 /* EditEventView.swift */; };
|
||||
A949F8322DCAAA8A0064DCA0 /* NearFutureIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = A949F8312DCAAA8A0064DCA0 /* NearFutureIcon.png */; };
|
||||
A949F84B2DCAABE00064DCA0 /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83A2DCAABE00064DCA0 /* ArchiveView.swift */; };
|
||||
A949F84C2DCAABE00064DCA0 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83C2DCAABE00064DCA0 /* AddEventView.swift */; };
|
||||
A949F84D2DCAABE00064DCA0 /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83D2DCAABE00064DCA0 /* EditEventView.swift */; };
|
||||
A949F84E2DCAABE00064DCA0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83F2DCAABE00064DCA0 /* ContentView.swift */; };
|
||||
A949F84F2DCAABE00064DCA0 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8402DCAABE00064DCA0 /* EventListView.swift */; };
|
||||
A949F8502DCAABE00064DCA0 /* HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8412DCAABE00064DCA0 /* HelpView.swift */; };
|
||||
A949F8512DCAABE00064DCA0 /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8432DCAABE00064DCA0 /* ExportView.swift */; };
|
||||
A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8442DCAABE00064DCA0 /* iCloudSettingsView.swift */; };
|
||||
A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8452DCAABE00064DCA0 /* ImportView.swift */; };
|
||||
A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8462DCAABE00064DCA0 /* SettingsView.swift */; };
|
||||
A949F8552DCAABE00064DCA0 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8482DCAABE00064DCA0 /* StatsView.swift */; };
|
||||
A949F8562DCAABE00064DCA0 /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8432DCAABE00064DCA0 /* ExportView.swift */; };
|
||||
A949F8592DCAAD670064DCA0 /* NearFutureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8572DCAAD670064DCA0 /* NearFutureTests.swift */; };
|
||||
A949F85F2DCABB420064DCA0 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F85D2DCABB420064DCA0 /* Buttons.swift */; };
|
||||
A979F6052D270AF00094C0B3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A979F6042D270AF00094C0B3 /* WidgetKit.framework */; };
|
||||
A979F6072D270AF00094C0B3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A979F6062D270AF00094C0B3 /* SwiftUI.framework */; };
|
||||
A979F60A2D270AF00094C0B3 /* NearFutureWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F6092D270AF00094C0B3 /* NearFutureWidgetsBundle.swift */; };
|
||||
@@ -26,8 +34,6 @@
|
||||
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 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Item.swift */; };
|
||||
A985104E2DB256430013D5FF /* iCloudSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985104D2DB256430013D5FF /* iCloudSettingsView.swift */; };
|
||||
A98510502DB263F00013D5FF /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985104F2DB263F00013D5FF /* EventListView.swift */; };
|
||||
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -70,13 +76,23 @@
|
||||
A920C28D2D24011A00E4F9B1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A920C28F2D24011A00E4F9B1 /* NearFuture.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NearFuture.entitlements; sourceTree = "<group>"; };
|
||||
A920C2912D24011A00E4F9B1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
A920C2B42D2401A100E4F9B1 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A920C2B72D2401A300E4F9B1 /* AddEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEventView.swift; sourceTree = "<group>"; };
|
||||
A920C2C02D2403CA00E4F9B1 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
A93BC0932D2B18A3002E8BBD /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
||||
A977CC912DBBB48000DED8C0 /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; };
|
||||
A977CC992DBD74FE00DED8C0 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = "<group>"; };
|
||||
A979F57E2D26B1300094C0B3 /* EditEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventView.swift; sourceTree = "<group>"; };
|
||||
A949F82E2DCAAA640064DCA0 /* NearFutureIcon.pxd */ = {isa = PBXFileReference; lastKnownFileType = file; path = NearFutureIcon.pxd; sourceTree = "<group>"; };
|
||||
A949F82F2DCAAA640064DCA0 /* NearFutureIconDark.pxd */ = {isa = PBXFileReference; lastKnownFileType = file; path = NearFutureIconDark.pxd; sourceTree = "<group>"; };
|
||||
A949F8302DCAAA640064DCA0 /* NearFutureIconTint.pxd */ = {isa = PBXFileReference; lastKnownFileType = file; path = NearFutureIconTint.pxd; sourceTree = "<group>"; };
|
||||
A949F8312DCAAA8A0064DCA0 /* NearFutureIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = NearFutureIcon.png; path = Assets.xcassets/AppIcon.appiconset/NearFutureIcon.png; sourceTree = "<group>"; };
|
||||
A949F83A2DCAABE00064DCA0 /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; };
|
||||
A949F83C2DCAABE00064DCA0 /* AddEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEventView.swift; sourceTree = "<group>"; };
|
||||
A949F83D2DCAABE00064DCA0 /* EditEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventView.swift; sourceTree = "<group>"; };
|
||||
A949F83F2DCAABE00064DCA0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
A949F8402DCAABE00064DCA0 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
|
||||
A949F8412DCAABE00064DCA0 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = "<group>"; };
|
||||
A949F8432DCAABE00064DCA0 /* ExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportView.swift; sourceTree = "<group>"; };
|
||||
A949F8442DCAABE00064DCA0 /* iCloudSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudSettingsView.swift; sourceTree = "<group>"; };
|
||||
A949F8452DCAABE00064DCA0 /* ImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportView.swift; sourceTree = "<group>"; };
|
||||
A949F8462DCAABE00064DCA0 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A949F8482DCAABE00064DCA0 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
||||
A949F8572DCAAD670064DCA0 /* NearFutureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureTests.swift; sourceTree = "<group>"; };
|
||||
A949F85D2DCABB420064DCA0 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
|
||||
A979F58B2D2700680094C0B3 /* NearFutureWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsBundle.swift; sourceTree = "<group>"; };
|
||||
A979F58D2D2700680094C0B3 /* NearFutureWidgetsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsLiveActivity.swift; sourceTree = "<group>"; };
|
||||
A979F58F2D2700680094C0B3 /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
|
||||
@@ -92,16 +108,10 @@
|
||||
A979F6112D270AF90094C0B3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A980FC302D920097006A778F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A980FC372D93FB2B006A778F /* NearFutureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NearFutureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A985104D2DB256430013D5FF /* iCloudSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudSettingsView.swift; sourceTree = "<group>"; };
|
||||
A985104F2DB263F00013D5FF /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
|
||||
A9C05E412D2805D7007DC497 /* NearFutureWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NearFutureWidgetsExtension.entitlements; sourceTree = "<group>"; };
|
||||
A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
A980FC382D93FB2B006A778F /* NearFutureTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NearFutureTests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A920C2812D24011300E4F9B1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@@ -134,9 +144,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A90FDE222DC0D4310012790C /* Config.xcconfig */,
|
||||
A949F8002DCAA0340064DCA0 /* Resources */,
|
||||
A920C2862D24011400E4F9B1 /* NearFuture */,
|
||||
A979F6082D270AF00094C0B3 /* NearFutureWidgets */,
|
||||
A980FC382D93FB2B006A778F /* NearFutureTests */,
|
||||
A949F8582DCAAD670064DCA0 /* NearFutureTests */,
|
||||
A979F6032D270AF00094C0B3 /* Frameworks */,
|
||||
A920C2852D24011400E4F9B1 /* Products */,
|
||||
);
|
||||
@@ -157,17 +168,8 @@
|
||||
children = (
|
||||
A920C2872D24011400E4F9B1 /* NearFutureApp.swift */,
|
||||
A920C28B2D24011400E4F9B1 /* Item.swift */,
|
||||
A920C2C02D2403CA00E4F9B1 /* ContentView.swift */,
|
||||
A985104F2DB263F00013D5FF /* EventListView.swift */,
|
||||
A920C2B72D2401A300E4F9B1 /* AddEventView.swift */,
|
||||
A979F57E2D26B1300094C0B3 /* EditEventView.swift */,
|
||||
A977CC912DBBB48000DED8C0 /* ArchiveView.swift */,
|
||||
A93BC0932D2B18A3002E8BBD /* StatsView.swift */,
|
||||
A977CC992DBD74FE00DED8C0 /* HelpView.swift */,
|
||||
A920C2B42D2401A100E4F9B1 /* SettingsView.swift */,
|
||||
A985104D2DB256430013D5FF /* iCloudSettingsView.swift */,
|
||||
A949F84A2DCAABE00064DCA0 /* Views */,
|
||||
A980FC302D920097006A778F /* Info.plist */,
|
||||
A920C28D2D24011A00E4F9B1 /* Assets.xcassets */,
|
||||
A920C28F2D24011A00E4F9B1 /* NearFuture.entitlements */,
|
||||
A920C2902D24011A00E4F9B1 /* Preview Content */,
|
||||
);
|
||||
@@ -183,6 +185,93 @@
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F8002DCAA0340064DCA0 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F8312DCAAA8A0064DCA0 /* NearFutureIcon.png */,
|
||||
A920C28D2D24011A00E4F9B1 /* Assets.xcassets */,
|
||||
A949F82E2DCAAA640064DCA0 /* NearFutureIcon.pxd */,
|
||||
A949F82F2DCAAA640064DCA0 /* NearFutureIconDark.pxd */,
|
||||
A949F8302DCAAA640064DCA0 /* NearFutureIconTint.pxd */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F83B2DCAABE00064DCA0 /* Archive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F83A2DCAABE00064DCA0 /* ArchiveView.swift */,
|
||||
);
|
||||
path = Archive;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F83E2DCAABE00064DCA0 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F83C2DCAABE00064DCA0 /* AddEventView.swift */,
|
||||
A949F83D2DCAABE00064DCA0 /* EditEventView.swift */,
|
||||
);
|
||||
path = Events;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F8422DCAABE00064DCA0 /* Home */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F83F2DCAABE00064DCA0 /* ContentView.swift */,
|
||||
A949F8402DCAABE00064DCA0 /* EventListView.swift */,
|
||||
A949F8412DCAABE00064DCA0 /* HelpView.swift */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F8472DCAABE00064DCA0 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F8432DCAABE00064DCA0 /* ExportView.swift */,
|
||||
A949F8442DCAABE00064DCA0 /* iCloudSettingsView.swift */,
|
||||
A949F8452DCAABE00064DCA0 /* ImportView.swift */,
|
||||
A949F8462DCAABE00064DCA0 /* SettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F8492DCAABE00064DCA0 /* Stats */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F8482DCAABE00064DCA0 /* StatsView.swift */,
|
||||
);
|
||||
path = Stats;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F84A2DCAABE00064DCA0 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F83B2DCAABE00064DCA0 /* Archive */,
|
||||
A949F83E2DCAABE00064DCA0 /* Events */,
|
||||
A949F8422DCAABE00064DCA0 /* Home */,
|
||||
A949F8472DCAABE00064DCA0 /* Settings */,
|
||||
A949F8492DCAABE00064DCA0 /* Stats */,
|
||||
A949F85E2DCABB420064DCA0 /* Misc */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F8582DCAAD670064DCA0 /* NearFutureTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F8572DCAAD670064DCA0 /* NearFutureTests.swift */,
|
||||
);
|
||||
path = NearFutureTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A949F85E2DCABB420064DCA0 /* Misc */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A949F85D2DCABB420064DCA0 /* Buttons.swift */,
|
||||
);
|
||||
path = Misc;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A979F58A2D2700680094C0B3 /* NearFutureWidgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -273,9 +362,6 @@
|
||||
dependencies = (
|
||||
A980FC3C2D93FB2B006A778F /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
A980FC382D93FB2B006A778F /* NearFutureTests */,
|
||||
);
|
||||
name = NearFutureTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
@@ -334,6 +420,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A920C2922D24011A00E4F9B1 /* Preview Assets.xcassets in Resources */,
|
||||
A949F8322DCAAA8A0064DCA0 /* NearFutureIcon.png in Resources */,
|
||||
A920C28E2D24011A00E4F9B1 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -360,17 +447,20 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */,
|
||||
A98510502DB263F00013D5FF /* EventListView.swift in Sources */,
|
||||
A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */,
|
||||
A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */,
|
||||
A977CC9A2DBD74FE00DED8C0 /* HelpView.swift in Sources */,
|
||||
A920C28C2D24011400E4F9B1 /* Item.swift in Sources */,
|
||||
A949F84B2DCAABE00064DCA0 /* ArchiveView.swift in Sources */,
|
||||
A949F84C2DCAABE00064DCA0 /* AddEventView.swift in Sources */,
|
||||
A949F84D2DCAABE00064DCA0 /* EditEventView.swift in Sources */,
|
||||
A949F84E2DCAABE00064DCA0 /* ContentView.swift in Sources */,
|
||||
A949F84F2DCAABE00064DCA0 /* EventListView.swift in Sources */,
|
||||
A949F8502DCAABE00064DCA0 /* HelpView.swift in Sources */,
|
||||
A949F85F2DCABB420064DCA0 /* Buttons.swift in Sources */,
|
||||
A949F8512DCAABE00064DCA0 /* ExportView.swift in Sources */,
|
||||
A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */,
|
||||
A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */,
|
||||
A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */,
|
||||
A949F8552DCAABE00064DCA0 /* StatsView.swift in Sources */,
|
||||
A920C2882D24011400E4F9B1 /* NearFutureApp.swift in Sources */,
|
||||
A93BC0942D2B18A3002E8BBD /* StatsView.swift in Sources */,
|
||||
A977CC922DBBB48000DED8C0 /* ArchiveView.swift in Sources */,
|
||||
A979F57F2D26B1300094C0B3 /* EditEventView.swift in Sources */,
|
||||
A985104E2DB256430013D5FF /* iCloudSettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -389,6 +479,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A949F8592DCAAD670064DCA0 /* NearFutureTests.swift in Sources */,
|
||||
A949F8562DCAABE00064DCA0 /* ExportView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -561,7 +653,7 @@
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13;
|
||||
@@ -572,10 +664,10 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = NO;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -612,7 +704,7 @@
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13;
|
||||
@@ -620,10 +712,10 @@
|
||||
OTHER_LDFLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = NO;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -640,7 +732,8 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = NearFutureWidgets/NearFutureWidgetsExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "$(TEAM_ID)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = NearFutureWidgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = NearFutureWidgets;
|
||||
@@ -651,20 +744,23 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14;
|
||||
MARKETING_VERSION = "$(VERSION)";
|
||||
OTHER_LDFLAGS = (
|
||||
"-Xlinker",
|
||||
"-interposable",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.neon443.NearFuture.widgets;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_WIDGETS)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSION = "$(VERSION)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -676,7 +772,8 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = NearFutureWidgets/NearFutureWidgetsExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "$(TEAM_ID)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = NearFutureWidgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = NearFutureWidgets;
|
||||
@@ -687,17 +784,20 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.neon443.NearFuture.widgets;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14;
|
||||
MARKETING_VERSION = "$(VERSION)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_WIDGETS)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSION = "$(VERSION)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 609 KiB |
|
Before Width: | Height: | Size: 534 KiB |
@@ -44,16 +44,18 @@ struct EventListView: View {
|
||||
.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: event.time ? .shortened : .omitted
|
||||
time: .shortened
|
||||
)
|
||||
)
|
||||
.font(.subheadline)
|
||||
@@ -71,7 +73,7 @@ struct EventListView: View {
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
Text("\(daysUntilEvent(event.date, short: false))")
|
||||
Text("\(daysUntilEvent(event.date).long)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import UserNotifications
|
||||
|
||||
//@Model
|
||||
//final class Item {
|
||||
@@ -28,7 +29,6 @@ struct Event: Identifiable, Codable {
|
||||
var color: ColorCodable
|
||||
var notes: String
|
||||
var date: Date
|
||||
var time: Bool
|
||||
var recurrence: RecurrenceType
|
||||
|
||||
enum RecurrenceType: String, Codable, CaseIterable {
|
||||
@@ -36,7 +36,29 @@ struct Event: Identifiable, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ColorCodable: Codable {
|
||||
struct ColorCodable: Codable, Equatable {
|
||||
init(_ color: Color) {
|
||||
let uiColor = UIColor(color)
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0
|
||||
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
|
||||
self.red = Double(r)
|
||||
self.green = Double(g)
|
||||
self.blue = Double(b)
|
||||
}
|
||||
init(uiColor: UIColor) {
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0
|
||||
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
self.red = Double(r)
|
||||
self.green = Double(g)
|
||||
self.blue = Double(b)
|
||||
}
|
||||
init(red: Double, green: Double, blue: Double) {
|
||||
self.red = red
|
||||
self.green = green
|
||||
self.blue = blue
|
||||
}
|
||||
|
||||
var red: Double
|
||||
var green: Double
|
||||
var blue: Double
|
||||
@@ -58,42 +80,88 @@ struct ColorCodable: Codable {
|
||||
self.blue = cc.blue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ color: Color) {
|
||||
let uiColor = UIColor(color)
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0
|
||||
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
|
||||
self.red = Double(r)
|
||||
self.green = Double(g)
|
||||
self.blue = Double(b)
|
||||
}
|
||||
init(red: Double, green: Double, blue: Double) {
|
||||
self.red = red
|
||||
self.green = green
|
||||
self.blue = blue
|
||||
func daysUntilEvent(_ eventDate: Date) -> (long: String, short: String) {
|
||||
let calendar = Calendar.current
|
||||
let startOfDayNow = calendar.startOfDay(for: Date())
|
||||
let startOfDayEvent = calendar.startOfDay(for: eventDate)
|
||||
let components = calendar.dateComponents([.day], from: startOfDayNow, to: startOfDayEvent)
|
||||
guard let days = components.day else { return ("N/A", "N/A") }
|
||||
guard days != 0 else { return ("Today", "Today") }
|
||||
if days < 0 {
|
||||
//past
|
||||
return (
|
||||
"\(-days) day\(plu(days)) ago",
|
||||
"\(days)d"
|
||||
)
|
||||
} else {
|
||||
//future
|
||||
return (
|
||||
"\(days) day\(plu(days))",
|
||||
"\(days)d"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func daysUntilEvent(_ eventDate: Date, short: Bool, sepLines: Bool = false) -> String {
|
||||
let calendar = Calendar.current
|
||||
let currentDate = Date()
|
||||
let components = calendar.dateComponents([.day], from: currentDate, to: eventDate)
|
||||
guard let days = components.day else { return "N/A" }
|
||||
guard days >= 0 else {
|
||||
if short {
|
||||
return "\(days)d"
|
||||
} else {
|
||||
return "\(-days)\(sepLines ? "\n" : " ")day\(-days == 1 ? "" : "s") ago"
|
||||
struct Settings: Codable, Equatable {
|
||||
var showCompletedInHome: Bool
|
||||
var tint: ColorCodable
|
||||
}
|
||||
|
||||
class SettingsViewModel: ObservableObject {
|
||||
@Published var settings: Settings = Settings(
|
||||
showCompletedInHome: false,
|
||||
tint: ColorCodable(uiColor: UIColor(named: "AccentColor")!)
|
||||
)
|
||||
@Published var notifsGranted: Bool = false
|
||||
|
||||
@Published var accentChoices: [Color] = [
|
||||
Color(UIColor(named: "uiColors/red")!),
|
||||
Color(UIColor(named: "uiColors/orange")!),
|
||||
Color(UIColor(named: "uiColors/yellow")!),
|
||||
Color(UIColor(named: "uiColors/green")!),
|
||||
Color(UIColor(named: "uiColors/blue")!),
|
||||
Color(UIColor(named: "uiColors/indigo")!),
|
||||
Color(UIColor(named: "uiColors/basic")!)
|
||||
]
|
||||
|
||||
init(load: Bool = true) {
|
||||
if load {
|
||||
loadSettings()
|
||||
Task {
|
||||
let requestResult = await requestNotifs()
|
||||
await MainActor.run {
|
||||
self.notifsGranted = requestResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
guard days != 0 else {
|
||||
return "Today"
|
||||
|
||||
let appGroupSettingsStore = UserDefaults(suiteName: "group.NearFuture") ?? UserDefaults.standard
|
||||
let icSettStore = NSUbiquitousKeyValueStore.default
|
||||
|
||||
func loadSettings() {
|
||||
let decoder = JSONDecoder()
|
||||
if let icSettings = icSettStore.data(forKey: "settings") {
|
||||
if let decodedSetts = try? decoder.decode(Settings.self, from: icSettings) {
|
||||
self.settings = decodedSetts
|
||||
}
|
||||
} else if let savedData = appGroupSettingsStore.data(forKey: "settings") {
|
||||
if let decodedSetts = try? decoder.decode(Settings.self, from: savedData) {
|
||||
self.settings = decodedSetts
|
||||
}
|
||||
}
|
||||
}
|
||||
if short {
|
||||
return "\(days)d"
|
||||
} else {
|
||||
return "\(days)\(sepLines ? "\n" : " ")day\(days == 1 ? "" : "s")"
|
||||
|
||||
func saveSettings() {
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(settings) {
|
||||
appGroupSettingsStore.set(encoded, forKey: "settings")
|
||||
icSettStore.set(encoded, forKey: "settings")
|
||||
icSettStore.synchronize()
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,20 +177,9 @@ class EventViewModel: ObservableObject {
|
||||
color: ColorCodable(randomColor()),
|
||||
notes: "",
|
||||
date: Date(),
|
||||
time: false,
|
||||
recurrence: .none
|
||||
)
|
||||
@Published var editableTemplate: Event = Event(
|
||||
name: "",
|
||||
complete: false,
|
||||
completeDesc: "",
|
||||
symbol: "star",
|
||||
color: ColorCodable(randomColor()),
|
||||
notes: "",
|
||||
date: Date(),
|
||||
time: false,
|
||||
recurrence: .none
|
||||
)
|
||||
@Published var editableTemplate: Event
|
||||
@Published var example: Event = Event(
|
||||
name: "event",
|
||||
complete: false,
|
||||
@@ -131,7 +188,6 @@ class EventViewModel: ObservableObject {
|
||||
color: ColorCodable(.orange),
|
||||
notes: "lksdjfakdflkasjlkjl",
|
||||
date: Date(),
|
||||
time: true,
|
||||
recurrence: .daily
|
||||
)
|
||||
|
||||
@@ -141,6 +197,7 @@ class EventViewModel: ObservableObject {
|
||||
@Published var syncStatus: String = "Not Synced"
|
||||
|
||||
init(load: Bool = true) {
|
||||
self.editableTemplate = template
|
||||
if load {
|
||||
loadEvents()
|
||||
}
|
||||
@@ -172,6 +229,43 @@ class EventViewModel: ObservableObject {
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
func getNotifs() async -> [UNNotificationRequest] {
|
||||
return await UNUserNotificationCenter.current().pendingNotificationRequests()
|
||||
}
|
||||
|
||||
func checkPendingNotifs(_ pending: [UNNotificationRequest]) {
|
||||
var eventUUIDs = events.map({$0.id.uuidString})
|
||||
for req in pending {
|
||||
//match the notif to an event
|
||||
if let index = events.firstIndex(where: {$0.id.uuidString == req.identifier}) {
|
||||
if let remove = eventUUIDs.firstIndex(where: {$0 == req.identifier}) {
|
||||
eventUUIDs.remove(at: remove)
|
||||
}
|
||||
let components = getDateComponents(events[index].date)
|
||||
//check the notif matches event details
|
||||
if req.content.title == events[index].name,
|
||||
req.content.subtitle == events[index].notes,
|
||||
req.trigger == UNCalendarNotificationTrigger(dateMatching: components, repeats: false) {
|
||||
//if it does, make sure the notif delets if u complete the veent
|
||||
if events[index].complete {
|
||||
cancelNotif(req.identifier)
|
||||
}
|
||||
} else {
|
||||
cancelNotif(req.identifier)
|
||||
scheduleEventNotif(events[index])
|
||||
}
|
||||
} else {
|
||||
//cancel if the event is deleted
|
||||
cancelNotif(req.identifier)
|
||||
}
|
||||
}
|
||||
for uuid in eventUUIDs {
|
||||
if let event = events.first(where: {$0.id.uuidString == uuid}) {
|
||||
scheduleEventNotif(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save to local and icloud
|
||||
func saveEvents() {
|
||||
let encoder = JSONEncoder()
|
||||
@@ -184,6 +278,9 @@ class EventViewModel: ObservableObject {
|
||||
|
||||
updateSyncStatus()
|
||||
loadEvents()
|
||||
Task {
|
||||
await checkPendingNotifs(getNotifs())
|
||||
}
|
||||
WidgetCenter.shared.reloadAllTimelines()//reload all widgets when saving events
|
||||
objectWillChange.send()
|
||||
}
|
||||
@@ -203,6 +300,7 @@ class EventViewModel: ObservableObject {
|
||||
|
||||
func addEvent(newEvent: Event) {
|
||||
events.append(newEvent)
|
||||
scheduleEventNotif(newEvent)
|
||||
saveEvents() //sync with icloud
|
||||
}
|
||||
|
||||
@@ -249,57 +347,25 @@ class EventViewModel: ObservableObject {
|
||||
saveEvents()
|
||||
}
|
||||
|
||||
func exportEvents() -> String? {
|
||||
func exportEvents() -> String {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
// Custom date encoding strategy to handle date formatting
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
encoder.dateEncodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
do {
|
||||
// Encode the events array to JSON data
|
||||
let encodedData = try encoder.encode(events)
|
||||
|
||||
// Convert the JSON data to a string
|
||||
if let jsonString = String(data: encodedData, encoding: .utf8) {
|
||||
return jsonString
|
||||
} else {
|
||||
print("Failed to convert encoded data to string")
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
print("Failed to encode events: \(error.localizedDescription)")
|
||||
return nil
|
||||
if let json = try? encoder.encode(self.events) {
|
||||
return "\(json.base64EncodedString())"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func importEvents(_ imp: String) {
|
||||
guard let impData = imp.data(using: .utf8) else {
|
||||
print("Failed to convert string to data")
|
||||
return
|
||||
func importEvents(_ imported: String) throws {
|
||||
guard let data = Data(base64Encoded: imported) else {
|
||||
throw importError.invalidB64
|
||||
}
|
||||
|
||||
// Create a JSONDecoder
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
// Add a custom date formatter for decoding the date string
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // Adjust this to the date format you're using
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
do {
|
||||
// Attempt to decode the events from the provided data
|
||||
let decoded = try decoder.decode([Event].self, from: impData)
|
||||
print("Successfully decoded events: \(decoded)")
|
||||
|
||||
// Save and reload after importing events
|
||||
let decoded = try decoder.decode([Event].self, from: data)
|
||||
self.events = decoded
|
||||
saveEvents()
|
||||
loadEvents()
|
||||
} catch {
|
||||
// Print error if decoding fails
|
||||
print("Failed to decode events: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +374,7 @@ class EventViewModel: ObservableObject {
|
||||
UserDefaults.standard.removeObject(forKey: "events")
|
||||
appGroupUserDefaults.removeObject(forKey: "events")
|
||||
events.removeAll()
|
||||
cancelAllNotifs()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
@@ -315,6 +382,7 @@ class EventViewModel: ObservableObject {
|
||||
icloudStore.removeObject(forKey: "events")
|
||||
icloudStore.synchronize()
|
||||
icloudData.removeAll()
|
||||
cancelAllNotifs()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
@@ -330,6 +398,7 @@ class EventViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
events.removeAll()
|
||||
cancelAllNotifs()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
@@ -340,6 +409,7 @@ class EventViewModel: ObservableObject {
|
||||
}
|
||||
icloudStore.synchronize()
|
||||
icloudData.removeAll()
|
||||
cancelAllNotifs()
|
||||
updateSyncStatus()
|
||||
}
|
||||
}
|
||||
@@ -352,6 +422,12 @@ class dummyEventViewModel: EventViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
class dummySettingsViewModel: SettingsViewModel {
|
||||
override init(load: Bool = false) {
|
||||
super.init(load: false)
|
||||
}
|
||||
}
|
||||
|
||||
func describeOccurrence(date: Date, recurrence: Event.RecurrenceType) -> String {
|
||||
let dateString = date.formatted(date: .long, time: .omitted)
|
||||
let recurrenceDescription: String
|
||||
@@ -390,3 +466,53 @@ func randomColor() -> Color {
|
||||
let b = Double.random(in: 0...1)
|
||||
return Color(red: r, green: g, blue: b)
|
||||
}
|
||||
|
||||
func plu(_ inp: Int) -> String {
|
||||
var input = inp
|
||||
if inp < 0 { input.negate() }
|
||||
return "\(input == 1 ? "" : "s")"
|
||||
}
|
||||
|
||||
public enum importError: Error {
|
||||
case invalidB64
|
||||
}
|
||||
|
||||
func requestNotifs() async -> Bool {
|
||||
let result = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .carPlay, .sound])
|
||||
return result ?? false
|
||||
}
|
||||
|
||||
func scheduleNotif(title: String, sub: String, date: Date, id: String = UUID().uuidString) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.subtitle = sub
|
||||
content.sound = .default
|
||||
|
||||
let identifier = id
|
||||
let dateComponents = getDateComponents(date)
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
func scheduleEventNotif(_ event: Event) {
|
||||
scheduleNotif(
|
||||
title: event.name,
|
||||
sub: event.notes,
|
||||
date: event.date,
|
||||
id: event.id.uuidString
|
||||
)
|
||||
}
|
||||
|
||||
func getDateComponents(_ date: Date) -> DateComponents {
|
||||
return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date)
|
||||
}
|
||||
|
||||
func cancelNotif(_ id: String) {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
|
||||
}
|
||||
|
||||
func cancelAllNotifs() {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ struct HelpView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
ZStack {
|
||||
Color(.accent)
|
||||
Color(.tintColor)
|
||||
.opacity(0.4)
|
||||
.padding(.horizontal, -15)
|
||||
.blur(radius: 5)
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
|
||||
@@ -22,10 +22,14 @@ struct NearFutureApp: App {
|
||||
// fatalError("Could not create ModelContainer: \(error)")
|
||||
// }
|
||||
// }()
|
||||
|
||||
@StateObject var settingsModel: SettingsViewModel = SettingsViewModel()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(viewModel: EventViewModel())
|
||||
ContentView(
|
||||
viewModel: EventViewModel(),
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
.tint(settingsModel.settings.tint.color)
|
||||
}
|
||||
// .modelContainer(sharedModelContainer)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,6 @@ struct ArchiveView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@State var showAddEvent: Bool = false
|
||||
@State var hey: UUID = UUID()
|
||||
init(viewModel: EventViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
@@ -39,6 +36,14 @@ struct ArchiveView: View {
|
||||
AddEventButton(showingAddEventView: $showAddEvent)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Archive")
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddEvent) {
|
||||
AddEventView(
|
||||
@@ -50,11 +55,15 @@ struct ArchiveView: View {
|
||||
eventColor: $viewModel.editableTemplate.color.colorBind,
|
||||
eventNotes: $viewModel.editableTemplate.notes,
|
||||
eventDate: $viewModel.editableTemplate.date,
|
||||
eventTime: $viewModel.editableTemplate.time,
|
||||
eventRecurrence: $viewModel.editableTemplate.recurrence,
|
||||
adding: true
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ struct AddEventView: View {
|
||||
@Binding var eventColor: Color
|
||||
@Binding var eventNotes: String
|
||||
@Binding var eventDate: Date
|
||||
@Binding var eventTime: Bool
|
||||
@Binding var eventRecurrence: Event.RecurrenceType
|
||||
|
||||
@State var adding: Bool
|
||||
@@ -60,6 +59,12 @@ struct AddEventView: View {
|
||||
title: "Choose a Symbol",
|
||||
searchLabel: "Search...",
|
||||
autoDismiss: true)
|
||||
.presentationDetents([.medium])
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
ColorPicker("", selection: $eventColor, supportsOpacity: false)
|
||||
.fixedSize()
|
||||
@@ -94,8 +99,10 @@ struct AddEventView: View {
|
||||
|
||||
// date picker
|
||||
HStack {
|
||||
Spacer()
|
||||
DatePicker("", selection: $eventDate, displayedComponents: .date)
|
||||
.datePickerStyle(WheelDatePickerStyle())
|
||||
Spacer()
|
||||
Button() {
|
||||
eventDate = Date()
|
||||
} label: {
|
||||
@@ -107,14 +114,11 @@ struct AddEventView: View {
|
||||
.frame(width: 20)
|
||||
}
|
||||
|
||||
Toggle("Schedule a Time", isOn: $eventTime)
|
||||
if eventTime {
|
||||
DatePicker(
|
||||
"",
|
||||
selection: $eventDate,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
}
|
||||
|
||||
// re-ocurrence Picker
|
||||
Picker("Recurrence", selection: $eventRecurrence) {
|
||||
@@ -161,7 +165,6 @@ struct AddEventView: View {
|
||||
color: ColorCodable(eventColor),
|
||||
notes: eventNotes,
|
||||
date: eventDate,
|
||||
time: eventTime,
|
||||
recurrence: eventRecurrence
|
||||
)
|
||||
)
|
||||
@@ -207,32 +210,11 @@ struct AddEventView: View {
|
||||
eventColor = randomColor()
|
||||
eventNotes = viewModel.template.notes
|
||||
eventDate = viewModel.template.date
|
||||
eventTime = viewModel.template.time
|
||||
eventRecurrence = viewModel.template.recurrence
|
||||
dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
struct MagicClearButton: View {
|
||||
@Binding var text: String
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
text = ""
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: text.isEmpty ? 0 : 25)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.padding(.trailing, -5)
|
||||
.animation(.spring, value: text.isEmpty)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let vm = dummyEventViewModel()
|
||||
@@ -248,11 +230,14 @@ struct MagicClearButton: View {
|
||||
eventColor: .constant(vm.template.color.color),
|
||||
eventNotes: .constant(vm.template.notes),
|
||||
eventDate: .constant(vm.template.date),
|
||||
eventTime: .constant(vm.template.time),
|
||||
eventRecurrence: .constant(vm.template.recurrence),
|
||||
adding: true
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ struct EditEventView: View {
|
||||
eventColor: $event.color.colorBind,
|
||||
eventNotes: $event.notes,
|
||||
eventDate: $event.date,
|
||||
eventTime: $event.time,
|
||||
eventRecurrence: $event.recurrence,
|
||||
adding: false //bc we editing existing event
|
||||
)
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import SwiftData
|
||||
|
||||
enum Field {
|
||||
@@ -21,6 +22,7 @@ enum Tab {
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
@State private var eventName = ""
|
||||
@State private var eventComplete = false
|
||||
@State private var eventCompleteDesc = ""
|
||||
@@ -28,14 +30,13 @@ struct ContentView: View {
|
||||
@State private var eventColor: Color = randomColor()
|
||||
@State private var eventNotes = ""
|
||||
@State private var eventDate = Date()
|
||||
@State private var eventTime = false
|
||||
@State private var eventRecurrence: Event.RecurrenceType = .none
|
||||
@State var hey: UUID = UUID()
|
||||
@State private var showingAddEventView = false
|
||||
@State private var searchInput: String = ""
|
||||
var filteredEvents: [Event] {
|
||||
if searchInput.isEmpty {
|
||||
return viewModel.events
|
||||
return viewModel.events.filter() {!$0.complete}
|
||||
} else {
|
||||
return viewModel.events.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(searchInput) ||
|
||||
@@ -62,17 +63,25 @@ struct ContentView: View {
|
||||
backgroundGradient
|
||||
VStack {
|
||||
ZStack {
|
||||
SearchBar(searchInput: $searchInput)
|
||||
.focused($focusedField, equals: Field.Search)
|
||||
.onSubmit {
|
||||
focusedField = nil
|
||||
}
|
||||
TextField(
|
||||
"\(Image(systemName: "magnifyingglass")) Search",
|
||||
text: $searchInput
|
||||
)
|
||||
.padding(.trailing, searchInput.isEmpty ? 0 : 30)
|
||||
.animation(.spring, value: searchInput)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.submitLabel(.done)
|
||||
.focused($focusedField, equals: Field.Search)
|
||||
.onSubmit {
|
||||
focusedField = nil
|
||||
}
|
||||
MagicClearButton(text: $searchInput)
|
||||
.onTapGesture {
|
||||
focusedField = nil
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if filteredEvents.isEmpty && !searchInput.isEmpty {
|
||||
HelpView(searchInput: $searchInput, focusedField: focusedField)
|
||||
} else {
|
||||
@@ -87,14 +96,23 @@ struct ContentView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
if /*!searchInput.isEmpty && */filteredEvents.isEmpty {
|
||||
HelpView(searchInput: $searchInput, focusedField: focusedField)
|
||||
HelpView(
|
||||
searchInput: $searchInput,
|
||||
focusedField: focusedField
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Near Future")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddEventView) {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
@@ -105,12 +123,15 @@ struct ContentView: View {
|
||||
eventColor: $eventColor,
|
||||
eventNotes: $eventNotes,
|
||||
eventDate: $eventDate,
|
||||
eventTime: $eventTime,
|
||||
eventRecurrence: $eventRecurrence,
|
||||
adding: true //adding event
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@@ -133,7 +154,7 @@ struct ContentView: View {
|
||||
Label("Statistics", systemImage: "chart.pie")
|
||||
}
|
||||
.focused($focusedTab, equals: Tab.Statistics)
|
||||
SettingsView(viewModel: viewModel)
|
||||
SettingsView(viewModel: viewModel, settingsModel: settingsModel)
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
@@ -143,43 +164,10 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView(viewModel: dummyEventViewModel())
|
||||
}
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var searchInput: String
|
||||
|
||||
var body: some View {
|
||||
TextField(
|
||||
"\(Image(systemName: "magnifyingglass")) Search",
|
||||
text: $searchInput
|
||||
)
|
||||
.padding(.trailing, searchInput.isEmpty ? 0 : 30)
|
||||
.animation(.spring, value: searchInput)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.submitLabel(.done)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddEventButton: View {
|
||||
@Binding var showingAddEventView: Bool
|
||||
var body: some View {
|
||||
Button() {
|
||||
showingAddEventView.toggle()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: 33)
|
||||
.foregroundStyle(.one)
|
||||
Image(systemName: "plus")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15)
|
||||
.bold()
|
||||
.foregroundStyle(.two)
|
||||
}
|
||||
}
|
||||
}
|
||||
ContentView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
|
||||
extension View {
|
||||
@@ -195,3 +183,7 @@ extension View {
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
|
||||
}
|
||||
169
NearFuture/Views/Home/EventListView.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// EventListView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 18/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct EventListView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@State var event: Event
|
||||
|
||||
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 {
|
||||
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)
|
||||
.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(.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)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.frame(maxWidth: 25, maxHeight: 25)
|
||||
.shadow(radius: 5)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.background(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(
|
||||
.one.opacity(appearance == .dark ? 0.5 : 1),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.clipShape(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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)
|
||||
}
|
||||
}
|
||||
112
NearFuture/Views/Home/HelpView.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// ArchiveHelp.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 26/04/2025.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum HelpType {
|
||||
case Search
|
||||
case Archive
|
||||
}
|
||||
|
||||
struct HelpView: View {
|
||||
/// initialises a Search HelpView
|
||||
///
|
||||
init(searchInput: Binding<String>, focusedField: Field?) {
|
||||
_searchInput = searchInput
|
||||
self.helpType = .Search
|
||||
_showAddEvent = .constant(false)
|
||||
}
|
||||
|
||||
/// initialises an Archive HelpView
|
||||
///
|
||||
init(showAddEvent: Binding<Bool>) {
|
||||
_showAddEvent = showAddEvent
|
||||
self.helpType = .Archive
|
||||
_searchInput = .constant("")
|
||||
self.focusedField = nil
|
||||
}
|
||||
|
||||
@Binding var searchInput: String
|
||||
@FocusState var focusedField: Field?
|
||||
|
||||
@Binding var showAddEvent: Bool
|
||||
|
||||
var helpType: HelpType
|
||||
var details: (
|
||||
symbol: String,
|
||||
title: String,
|
||||
body: String,
|
||||
buttonAction: () -> (),
|
||||
buttonSymbol: String,
|
||||
buttonText: String
|
||||
) {
|
||||
switch helpType {
|
||||
case .Search:
|
||||
return (
|
||||
symbol: "questionmark.app.dashed",
|
||||
title: "Looking for something?",
|
||||
body: "Tip: The Search bar searches event names and notes.",
|
||||
buttonAction: {
|
||||
searchInput = ""
|
||||
focusedField = nil
|
||||
},
|
||||
buttonSymbol: "xmark",
|
||||
buttonText: "Clear Filters"
|
||||
)
|
||||
case .Archive:
|
||||
return (
|
||||
symbol: "eyes",
|
||||
title: "Nothing to see here...",
|
||||
body: "The Archive contains events that have been marked as complete.",
|
||||
buttonAction: {
|
||||
showAddEvent.toggle()
|
||||
},
|
||||
buttonSymbol: "plus",
|
||||
buttonText: "Create an event"
|
||||
)
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
List {
|
||||
ZStack {
|
||||
Color(.tintColor)
|
||||
.opacity(0.4)
|
||||
.padding(.horizontal, -15)
|
||||
.blur(radius: 5)
|
||||
HStack {
|
||||
Image(systemName: details.symbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.trailing)
|
||||
Text(details.title)
|
||||
.bold()
|
||||
.font(.title2)
|
||||
}
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text(details.body)
|
||||
Button() {
|
||||
details.buttonAction()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: details.buttonSymbol)
|
||||
.bold()
|
||||
Text(details.buttonText)
|
||||
}
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HelpView(searchInput: .constant(""), focusedField: nil)
|
||||
HelpView(showAddEvent: .constant(false))
|
||||
}
|
||||
58
NearFuture/Views/Misc/Buttons.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// MagicClearButton.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 06/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MagicClearButton: View {
|
||||
@Binding var text: String
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
text = ""
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: text.isEmpty ? 0 : 25)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.padding(.trailing, -5)
|
||||
.animation(.spring, value: text.isEmpty)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddEventButton: View {
|
||||
@Binding var showingAddEventView: Bool
|
||||
var body: some View {
|
||||
Button() {
|
||||
showingAddEventView.toggle()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: 33)
|
||||
.foregroundStyle(.one)
|
||||
Image(systemName: "plus")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15)
|
||||
.bold()
|
||||
.foregroundStyle(.two)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MagicClearButton(text: .constant("s"))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddEventButton(showingAddEventView: .constant(false))
|
||||
}
|
||||
27
NearFuture/Views/Settings/ExportView.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// ExportView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 02/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ExportView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
var body: some View {
|
||||
List {
|
||||
Button() {
|
||||
UIPasteboard.general.string = viewModel.exportEvents()
|
||||
} label: {
|
||||
Label("Copy Events", systemImage: "document.on.clipboard")
|
||||
}
|
||||
Text(viewModel.exportEvents())
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExportView(viewModel: dummyEventViewModel())
|
||||
}
|
||||
67
NearFuture/Views/Settings/ImportView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// ImportView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 02/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImportView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@Binding var importStr: String
|
||||
|
||||
@State private var image: String = "clock.fill"
|
||||
@State private var text: String = "Ready..."
|
||||
@State private var fgColor: Color = .yellow
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Status") {
|
||||
Label(text, systemImage: image)
|
||||
.contentTransition(.numericText())
|
||||
.foregroundStyle(fgColor)
|
||||
}
|
||||
TextField("", text: $importStr)
|
||||
Button() {
|
||||
do throws {
|
||||
try viewModel.importEvents(importStr)
|
||||
withAnimation {
|
||||
image = "checkmark.circle.fill"
|
||||
text = "Complete"
|
||||
fgColor = .green
|
||||
}
|
||||
} catch importError.invalidB64 {
|
||||
withAnimation {
|
||||
image = "xmark.app.fill"
|
||||
text = "Invalid base64 input."
|
||||
fgColor = .red
|
||||
}
|
||||
} catch {
|
||||
withAnimation {
|
||||
image = "xmark.app.fill"
|
||||
text = error.localizedDescription
|
||||
fgColor = .red
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Import", systemImage: "tray.and.arrow.down.fill")
|
||||
}
|
||||
.disabled(importStr.isEmpty)
|
||||
.onAppear() {
|
||||
importStr = ""
|
||||
image = "clock.fill"
|
||||
text = "Ready..."
|
||||
fgColor = .yellow
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ImportView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
importStr: .constant("kljadfskljafdlkj;==")
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@State var viewModel: EventViewModel
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var hasUbiquitous: Bool = false
|
||||
@State private var lastSyncWasSuccessful: Bool = false
|
||||
@@ -44,6 +45,47 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
List {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(settingsModel.accentChoices, id: \.self) { color in
|
||||
ZStack {
|
||||
Button() {
|
||||
settingsModel.settings.tint.colorBind = color
|
||||
settingsModel.saveSettings()
|
||||
} label: {
|
||||
Circle()
|
||||
.foregroundStyle(color)
|
||||
.frame(width: 30)
|
||||
}
|
||||
if ColorCodable(color) == settingsModel.settings.tint {
|
||||
let needContrast: Bool = ColorCodable(color) == settingsModel.settings.tint
|
||||
Circle()
|
||||
.foregroundStyle(needContrast ? .two : .one)
|
||||
.frame(width: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigationLink() {
|
||||
List {
|
||||
if !settingsModel.notifsGranted {
|
||||
Button("Request Notifications") {
|
||||
Task {
|
||||
settingsModel.notifsGranted = await requestNotifs()
|
||||
}
|
||||
}
|
||||
Text("\(Image(systemName: "xmark")) Notifications disabled for Near Future")
|
||||
.foregroundStyle(.red)
|
||||
} else {
|
||||
Text("\(Image(systemName: "checkmark")) Notifications enabled for Near Future")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "bell.badge.fill")
|
||||
Text("Notifications")
|
||||
}
|
||||
NavigationLink() {
|
||||
iCloudSettingsView(
|
||||
viewModel: viewModel,
|
||||
@@ -68,47 +110,17 @@ struct SettingsView: View {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
NavigationLink() {
|
||||
NavigationStack() {
|
||||
Button() {
|
||||
UIPasteboard.general.string = "\(viewModel.exportEvents() ?? "")"
|
||||
print(viewModel.exportEvents() as Any)
|
||||
} label: {
|
||||
Text("copy")
|
||||
}
|
||||
Text("\(viewModel.exportEvents() ?? "")")
|
||||
}
|
||||
ImportView(viewModel: viewModel, importStr: $importStr)
|
||||
} label: {
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
Text("Export events")
|
||||
Label("Import Events", systemImage: "tray.and.arrow.down.fill")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
NavigationLink() {
|
||||
NavigationStack() {
|
||||
VStack {
|
||||
TextEditor(text: $importStr)
|
||||
.foregroundStyle(.foreground, .gray)
|
||||
.background(.gray)
|
||||
.frame(width: 200, height: 400)
|
||||
.shadow(radius: 5)
|
||||
Button() {
|
||||
viewModel.importEvents(importStr)
|
||||
} label: {
|
||||
Text("import events")
|
||||
}
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
Button() {
|
||||
if let pb = UIPasteboard.general.string {
|
||||
print(pb)
|
||||
}
|
||||
} label: {
|
||||
Text("print pb")
|
||||
}
|
||||
}
|
||||
}
|
||||
ExportView(viewModel: viewModel)
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
Text("Import events")
|
||||
Label("Export Events", systemImage: "square.and.arrow.up")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
|
||||
Section("Tip") {
|
||||
@@ -135,19 +147,54 @@ struct SettingsView: View {
|
||||
viewModel.dangerResetiCloud()
|
||||
}
|
||||
}
|
||||
Section("About") {
|
||||
VStack {
|
||||
Image(uiImage: #imageLiteral(resourceName: "NearFutureIcon.png"))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25))
|
||||
Text("Near Future")
|
||||
.bold()
|
||||
.monospaced()
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity)
|
||||
Text("Version " + getVersion() + " (\(getBuildID()))")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("Settings")
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView(viewModel: dummyEventViewModel())
|
||||
SettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
|
||||
func test() -> Void {
|
||||
|
||||
func getVersion() -> String {
|
||||
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] else {
|
||||
fatalError("no bundle id wtf lol")
|
||||
}
|
||||
return "\(version)"
|
||||
}
|
||||
|
||||
func getBuildID() -> String {
|
||||
guard let build = Bundle.main.infoDictionary?["CFBundleVersion"] else {
|
||||
fatalError("wtf did u do w the build number")
|
||||
}
|
||||
return "\(build)"
|
||||
}
|
||||
206
NearFuture/Views/Settings/iCloudSettingsView.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// iCloudSettingsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 18/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct iCloudSettingsView: View {
|
||||
@State var viewModel: EventViewModel
|
||||
@State var showPushAlert: Bool = false
|
||||
@State var showPullAlert: Bool = false
|
||||
|
||||
@Binding var hasUbiquitous: Bool
|
||||
@Binding var lastSyncWasSuccessful: Bool
|
||||
@Binding var lastSyncWasNormalAgo: Bool
|
||||
@Binding var localCountEqualToiCloud: Bool
|
||||
@Binding var icloudCountEqualToLocal: Bool
|
||||
|
||||
let asi = ProcessInfo().isiOSAppOnMac
|
||||
let model = UIDevice().model
|
||||
var device: (sf: String, label: String) {
|
||||
if asi {
|
||||
return (sf: "laptopcomputer", label: "Computer")
|
||||
} else if model == "iPhone" {
|
||||
return (sf: model.lowercased(), label: model)
|
||||
} else if model == "iPad" {
|
||||
return (sf: model.lowercased(), label: model)
|
||||
}
|
||||
return (sf: "iphone", label: "iPhone")
|
||||
}
|
||||
|
||||
var updateStatus: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ZStack {
|
||||
Image(systemName: "icloud")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 55)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
Text("\(viewModel.icloudEventCount)")
|
||||
.font(.title2)
|
||||
.monospaced()
|
||||
.bold()
|
||||
}
|
||||
Text("iCloud")
|
||||
HStack {
|
||||
Button(role: .destructive) {
|
||||
showPushAlert.toggle()
|
||||
} label: {
|
||||
Image(systemName: "arrow.up")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
.alert("Warning", isPresented: $showPushAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceiCloudWithLocalData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This will replace Events stored in iCloud with Events stored locally.")
|
||||
}
|
||||
|
||||
Button() {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
} label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
|
||||
Button(role: .destructive) {
|
||||
showPullAlert.toggle()
|
||||
} label: {
|
||||
Image(systemName: "arrow.down")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
.alert("Warning", isPresented: $showPullAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceLocalWithiCloudData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This will replace Events stored locally with Events stored in iCloud.")
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
Image(systemName: device.sf)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 75)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
Text("\(viewModel.localEventCount)")
|
||||
.font(.title2)
|
||||
.monospaced()
|
||||
.bold()
|
||||
}
|
||||
Text(device.label)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(hasUbiquitous ? .green : .red)
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Text("\(hasUbiquitous ? "" : "Not ")Working")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasSuccessful ? .green : .red)
|
||||
Text("Sync Status")
|
||||
Spacer()
|
||||
Text("\(viewModel.syncStatus)")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasNormalAgo ? .green : .red)
|
||||
Text("Last Sync")
|
||||
Spacer()
|
||||
Text("\(viewModel.lastSync?.formatted(date: .long, time: .standard) ?? "Never")")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(localCountEqualToiCloud ? .green : .red)
|
||||
Text("Local Events")
|
||||
Spacer()
|
||||
Text("\(viewModel.localEventCount)")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(icloudCountEqualToLocal ? .green : .red)
|
||||
Text("Events in iCloud")
|
||||
Spacer()
|
||||
Text("\(viewModel.icloudEventCount)")
|
||||
.bold()
|
||||
}
|
||||
} header: {
|
||||
Text("Sync Status")
|
||||
} footer: {
|
||||
Text("Pull to sync\nOr use the arrows to force push/pull")
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("iCloud")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("iCloudSettingsView") {
|
||||
iCloudSettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
hasUbiquitous: .constant(true),
|
||||
lastSyncWasSuccessful: .constant(true),
|
||||
lastSyncWasNormalAgo: .constant(true),
|
||||
localCountEqualToiCloud: .constant(true),
|
||||
icloudCountEqualToLocal: .constant(true),
|
||||
updateStatus: {}
|
||||
)
|
||||
}
|
||||
@@ -58,7 +58,13 @@ struct StatsView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("Statistics")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
//
|
||||
// iCloudSettingsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 18/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct iCloudSettingsView: View {
|
||||
@State var viewModel: EventViewModel
|
||||
@State var showPushAlert: Bool = false
|
||||
@State var showPullAlert: Bool = false
|
||||
|
||||
@Binding var hasUbiquitous: Bool
|
||||
@Binding var lastSyncWasSuccessful: Bool
|
||||
@Binding var lastSyncWasNormalAgo: Bool
|
||||
@Binding var localCountEqualToiCloud: Bool
|
||||
@Binding var icloudCountEqualToLocal: Bool
|
||||
|
||||
let asi = ProcessInfo().isiOSAppOnMac
|
||||
let model = UIDevice().model
|
||||
var device: (sf: String, label: String) {
|
||||
if asi {
|
||||
return (sf: "laptopcomputer", label: "Computer")
|
||||
} else if model == "iPhone" {
|
||||
return (sf: model.lowercased(), label: model)
|
||||
} else if model == "iPad" {
|
||||
return (sf: model.lowercased(), label: model)
|
||||
}
|
||||
return (sf: "iphone", label: "iPhone")
|
||||
}
|
||||
|
||||
var updateStatus: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
List {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ZStack {
|
||||
Image(systemName: "icloud")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 55)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
Text("\(viewModel.icloudEventCount)")
|
||||
.font(.title2)
|
||||
.monospaced()
|
||||
.bold()
|
||||
}
|
||||
Text("iCloud")
|
||||
HStack {
|
||||
Button(role: .destructive) {
|
||||
showPushAlert.toggle()
|
||||
} label: {
|
||||
Image(systemName: "arrow.up")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
.alert("Warning", isPresented: $showPushAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceiCloudWithLocalData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This will replace Events stored in iCloud with Events stored locally.")
|
||||
}
|
||||
|
||||
Button() {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
} label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
|
||||
Button(role: .destructive) {
|
||||
showPullAlert.toggle()
|
||||
} label: {
|
||||
Image(systemName: "arrow.down")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 40)
|
||||
}
|
||||
.buttonStyle(BorderedButtonStyle())
|
||||
.alert("Warning", isPresented: $showPullAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceLocalWithiCloudData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This will replace Events stored locally with Events stored in iCloud.")
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
Image(systemName: device.sf)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 75)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
Text("\(viewModel.localEventCount)")
|
||||
.font(.title2)
|
||||
.monospaced()
|
||||
.bold()
|
||||
}
|
||||
Text(device.label)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(hasUbiquitous ? .green : .red)
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Text("\(hasUbiquitous ? "" : "Not ")Working")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasSuccessful ? .green : .red)
|
||||
Text("Sync Status")
|
||||
Spacer()
|
||||
Text("\(viewModel.syncStatus)")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasNormalAgo ? .green : .red)
|
||||
Text("Last Sync")
|
||||
Spacer()
|
||||
Text("\(viewModel.lastSync?.formatted() ?? "Never")")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(localCountEqualToiCloud ? .green : .red)
|
||||
Text("Local Events")
|
||||
Spacer()
|
||||
Text("\(viewModel.localEventCount)")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(icloudCountEqualToLocal ? .green : .red)
|
||||
Text("Events in iCloud")
|
||||
Spacer()
|
||||
Text("\(viewModel.icloudEventCount)")
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("iCloud")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("iCloudSettingsView") {
|
||||
iCloudSettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
hasUbiquitous: .constant(true),
|
||||
lastSyncWasSuccessful: .constant(true),
|
||||
lastSyncWasNormalAgo: .constant(true),
|
||||
localCountEqualToiCloud: .constant(true),
|
||||
icloudCountEqualToLocal: .constant(true),
|
||||
updateStatus: test
|
||||
)
|
||||
}
|
||||
@@ -128,11 +128,19 @@ struct EventWidgetView: View {
|
||||
Spacer()
|
||||
|
||||
//short days till if not large widget
|
||||
Text(daysUntilEvent(event.date, short: !isLarge, sepLines: true))
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.color.color)
|
||||
.padding(.trailing, -12)
|
||||
if isLarge {
|
||||
Text(daysUntilEvent(event.date).long)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.color.color)
|
||||
.padding(.trailing, -12)
|
||||
} else {
|
||||
Text(daysUntilEvent(event.date).short)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.color.color)
|
||||
.padding(.trailing, -12)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -5,76 +5,76 @@
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
//import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct NearFutureWidgetsAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
// Dynamic stateful properties about your activity go here!
|
||||
var emoji: String
|
||||
}
|
||||
//struct NearFutureWidgetsAttributes: ActivityAttributes {
|
||||
// public struct ContentState: Codable, Hashable {
|
||||
// // Dynamic stateful properties about your activity go here!
|
||||
// var emoji: String
|
||||
// }
|
||||
//
|
||||
// // Fixed non-changing properties about your activity go here!
|
||||
// var name: String
|
||||
//}
|
||||
|
||||
// Fixed non-changing properties about your activity go here!
|
||||
var name: String
|
||||
}
|
||||
//struct NearFutureWidgetsLiveActivity: Widget {
|
||||
// var body: some WidgetConfiguration {
|
||||
// ActivityConfiguration(for: NearFutureWidgetsAttributes.self) { context in
|
||||
// // Lock screen/banner UI goes here
|
||||
// VStack {
|
||||
// Text("Hello \(context.state.emoji)")
|
||||
// }
|
||||
// .activityBackgroundTint(Color.cyan)
|
||||
// .activitySystemActionForegroundColor(Color.black)
|
||||
//
|
||||
// } dynamicIsland: { context in
|
||||
// DynamicIsland {
|
||||
// // Expanded UI goes here. Compose the expanded UI through
|
||||
// // various regions, like leading/trailing/center/bottom
|
||||
// DynamicIslandExpandedRegion(.leading) {
|
||||
// Text("Leading")
|
||||
// }
|
||||
// DynamicIslandExpandedRegion(.trailing) {
|
||||
// Text("Trailing")
|
||||
// }
|
||||
// DynamicIslandExpandedRegion(.bottom) {
|
||||
// Text("Bottom \(context.state.emoji)")
|
||||
// // more content
|
||||
// }
|
||||
// } compactLeading: {
|
||||
// Text("L")
|
||||
// } compactTrailing: {
|
||||
// Text("T \(context.state.emoji)")
|
||||
// } minimal: {
|
||||
// Text(context.state.emoji)
|
||||
// }
|
||||
// .widgetURL(URL(string: "http://www.apple.com"))
|
||||
// .keylineTint(Color.red)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension NearFutureWidgetsAttributes {
|
||||
// fileprivate static var preview: NearFutureWidgetsAttributes {
|
||||
// NearFutureWidgetsAttributes(name: "World")
|
||||
// }
|
||||
//}
|
||||
|
||||
struct NearFutureWidgetsLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: NearFutureWidgetsAttributes.self) { context in
|
||||
// Lock screen/banner UI goes here
|
||||
VStack {
|
||||
Text("Hello \(context.state.emoji)")
|
||||
}
|
||||
.activityBackgroundTint(Color.cyan)
|
||||
.activitySystemActionForegroundColor(Color.black)
|
||||
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded UI goes here. Compose the expanded UI through
|
||||
// various regions, like leading/trailing/center/bottom
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text("Leading")
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
Text("Trailing")
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
Text("Bottom \(context.state.emoji)")
|
||||
// more content
|
||||
}
|
||||
} compactLeading: {
|
||||
Text("L")
|
||||
} compactTrailing: {
|
||||
Text("T \(context.state.emoji)")
|
||||
} minimal: {
|
||||
Text(context.state.emoji)
|
||||
}
|
||||
.widgetURL(URL(string: "http://www.apple.com"))
|
||||
.keylineTint(Color.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NearFutureWidgetsAttributes {
|
||||
fileprivate static var preview: NearFutureWidgetsAttributes {
|
||||
NearFutureWidgetsAttributes(name: "World")
|
||||
}
|
||||
}
|
||||
|
||||
extension NearFutureWidgetsAttributes.ContentState {
|
||||
fileprivate static var smiley: NearFutureWidgetsAttributes.ContentState {
|
||||
NearFutureWidgetsAttributes.ContentState(emoji: "😀")
|
||||
}
|
||||
|
||||
fileprivate static var starEyes: NearFutureWidgetsAttributes.ContentState {
|
||||
NearFutureWidgetsAttributes.ContentState(emoji: "🤩")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Notification", as: .content, using: NearFutureWidgetsAttributes.preview) {
|
||||
NearFutureWidgetsLiveActivity()
|
||||
} contentStates: {
|
||||
NearFutureWidgetsAttributes.ContentState.smiley
|
||||
NearFutureWidgetsAttributes.ContentState.starEyes
|
||||
}
|
||||
//extension NearFutureWidgetsAttributes.ContentState {
|
||||
// fileprivate static var smiley: NearFutureWidgetsAttributes.ContentState {
|
||||
// NearFutureWidgetsAttributes.ContentState(emoji: "😀")
|
||||
// }
|
||||
//
|
||||
// fileprivate static var starEyes: NearFutureWidgetsAttributes.ContentState {
|
||||
// NearFutureWidgetsAttributes.ContentState(emoji: "🤩")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#Preview("Notification", as: .content, using: NearFutureWidgetsAttributes.preview) {
|
||||
// NearFutureWidgetsLiveActivity()
|
||||
//} contentStates: {
|
||||
// NearFutureWidgetsAttributes.ContentState.smiley
|
||||
// NearFutureWidgetsAttributes.ContentState.starEyes
|
||||
//}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# NearFuture
|
||||
[](https://apps.apple.com/us/app/near-future-event-tracker/id6744963429)
|
||||
|
||||
[App Store](https://apps.apple.com/us/app/near-future-event-tracker/id6744963429)
|
||||
|
||||
**Near Future** is a SwiftUI App to help people track upcoming events - Holidays, Trips, Birthdays, Weddings, Anniversaries.
|
||||
|
||||
|
||||
BIN
Resources/Assets.xcassets/AppIcon.appiconset/NearFutureIcon.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 729 KiB |
|
After Width: | Height: | Size: 632 KiB |
9
Resources/Assets.xcassets/uiColors/Contents.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.961",
|
||||
"green" : "0.510",
|
||||
"red" : "0.227"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.216",
|
||||
"green" : "0.737",
|
||||
"red" : "0.337"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.871",
|
||||
"green" : "0.329",
|
||||
"red" : "0.345"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "extended-srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.376",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.349",
|
||||
"green" : "0.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.242",
|
||||
"green" : "0.197",
|
||||
"red" : "0.910"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.239",
|
||||
"green" : "0.690",
|
||||
"red" : "0.933"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||