Compare commits
96 Commits
feat-accen
...
b7ef7b4e19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ef7b4e19 | ||
|
|
7727f14ad4 | ||
|
|
e4107a2faa | ||
|
|
5ec16dd67a | ||
|
|
0cffe243eb | ||
|
|
6533fb85ed | ||
|
|
5c667679d5 | ||
|
|
5dd25f1ede | ||
|
|
d80011ea27 | ||
|
|
a40d4f4300 | ||
|
|
22af9fc060 | ||
|
|
2dd2c51059 | ||
|
|
4c9e72fad2 | ||
|
|
cb26c69492 | ||
|
|
13ef94ea3e | ||
|
|
6e5b7adbc4 | ||
|
|
659f14d5a3 | ||
|
|
0663ba9e59 | ||
|
|
6e7bc6c2d1 | ||
|
|
3f21074091 | ||
|
|
ee2e05c523 | ||
|
|
e9e19e2659 | ||
|
|
5ce48a4bc2 | ||
|
|
a7e09692f6 | ||
|
|
559f891719 | ||
|
|
2ef000458f | ||
|
|
d5580e52f5 | ||
|
|
84a7091e05 | ||
|
|
69047010d7 | ||
|
|
dfb1dae545 | ||
|
|
0f3b86fe8c | ||
|
|
1902c5102c | ||
|
|
1179810ac2 | ||
|
|
dfc3a9290a | ||
|
|
7236f0640e | ||
|
|
44b40894e4 | ||
|
|
e4842bd29a | ||
|
|
b378a831be | ||
|
|
2ff96a7093 | ||
|
|
4f5e31a6f3 | ||
|
|
266b27d817 | ||
|
|
d1c1144ad8 | ||
|
|
bcfe23a17e | ||
|
|
74ef5db2cf | ||
|
|
389fdf6812 | ||
|
|
b153d127a6 | ||
|
|
62318c5ddc | ||
|
|
773a083682 | ||
|
|
e051d73b7e | ||
|
|
4bf33d50d6 | ||
|
|
74603cc588 | ||
|
|
ed016217db | ||
|
|
a64eb03cad | ||
|
|
7c7f72a5d4 | ||
|
|
831305ba55 | ||
|
|
69856e2857 | ||
|
|
7be2e3848a | ||
|
|
db05da3043 | ||
|
|
940a8f4ebd | ||
|
|
48071c0791 | ||
|
|
3a94dc7675 | ||
|
|
08bf56ecbd | ||
|
|
008eecb52c | ||
|
|
c640c77600 | ||
|
|
e9d49cc137 | ||
|
|
565341c3d0 | ||
|
|
f9c50df47f | ||
|
|
bb598250a6 | ||
|
|
c7fddda601 | ||
|
|
033a01463f | ||
|
|
87209f38f3 | ||
|
|
7c852fc8ec | ||
|
|
73b3d9b234 | ||
|
|
0a17ec6292 | ||
|
|
bb67b0ce98 | ||
|
|
82341f40ef | ||
|
|
f9f030c8b3 | ||
|
|
3ca5017b61 | ||
|
|
0efcc9e183 | ||
|
|
595c2b74c5 | ||
|
|
f252359eb1 | ||
|
|
2b266e03ca | ||
|
|
c935d4b1f0 | ||
|
|
c82ce5fa17 | ||
|
|
7bef3bc4e6 | ||
|
|
025dbf22cd | ||
|
|
da65c3ac69 | ||
|
|
5f52b423db | ||
|
|
7af96834d6 | ||
|
|
17ef4f1478 | ||
|
|
785dda0e2b | ||
|
|
5f5702d958 | ||
|
|
b88bc11b36 | ||
|
|
f8f5cdde26 | ||
|
|
169d51e6b9 | ||
|
|
afd6d23838 |
@@ -12,6 +12,6 @@ TEAM_ID = 8JGND254B7
|
||||
BUNDLE_ID = com.neon443.NearFuture
|
||||
BUNDLE_ID_WIDGETS = com.neon443.NearFuture.widgets
|
||||
GROUP_ID = group.NearFuture
|
||||
VERSION = 3.2.1
|
||||
VERSION = 5
|
||||
NAME = Near Future
|
||||
BUILD_NUMBER = 5
|
||||
BUILD_NUMBER = 1
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB |
40
MacNearFuture/AboutView.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// AboutView.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 28/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Image(nsImage: #imageLiteral(resourceName: "NearFutureIcon.png"))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25))
|
||||
Text("Near Future")
|
||||
.bold()
|
||||
.monospaced()
|
||||
.font(.title)
|
||||
Text("Version " + getVersion() + " (\(getBuildID()))")
|
||||
.padding(.bottom)
|
||||
Text("© 2024-2025 neon443, Inc")
|
||||
.padding(.bottom)
|
||||
Link("Developer Website", destination: URL(string: "https://neon443.xyz")!)
|
||||
}
|
||||
.padding()
|
||||
.padding()
|
||||
.containerBackground(.ultraThinMaterial, for: .window)
|
||||
.toolbar(removing: .title)
|
||||
.toolbarBackground(.hidden, for: .windowToolbar)
|
||||
.windowMinimizeBehavior(.disabled)
|
||||
.windowFullScreenBehavior(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AboutView()
|
||||
}
|
||||
18
MacNearFuture/MacNearFuture.entitlements
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.NearFuture</string>
|
||||
</array>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
72
MacNearFuture/MacNearFutureApp.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// MacNearFutureApp.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 21/05/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct NearFutureApp: App {
|
||||
@Environment(\.openWindow) var openWindow
|
||||
@StateObject var viewModel: EventViewModel = EventViewModel()
|
||||
@StateObject var settingsModel: SettingsViewModel = SettingsViewModel()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
}
|
||||
.defaultSize(width: 550, height: 650)
|
||||
.commands {
|
||||
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
|
||||
Button("About Near Future") {
|
||||
openWindow(id: "about")
|
||||
}
|
||||
}
|
||||
NearFutureCommands()
|
||||
}
|
||||
|
||||
WindowGroup("Edit Event", for: Event.ID.self) { $eventID in
|
||||
if viewModel.events.first(where: {$0.id == eventID}) == nil {
|
||||
AddEventView(
|
||||
viewModel: viewModel
|
||||
)
|
||||
} else {
|
||||
EditEventView(
|
||||
viewModel: viewModel,
|
||||
event: Binding(
|
||||
get: {
|
||||
viewModel.events.first(where: {$0.id == eventID}) ?? viewModel.template
|
||||
},
|
||||
set: { newValue in
|
||||
viewModel.editEvent(newValue)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.defaultSize(width: 480, height: 550)
|
||||
.windowIdealSize(.fitToContent)
|
||||
.restorationBehavior(.disabled)
|
||||
|
||||
Window("About Near Future", id: "about") {
|
||||
AboutView()
|
||||
}
|
||||
.windowBackgroundDragBehavior(.enabled)
|
||||
.windowResizability(.contentSize)
|
||||
.restorationBehavior(.disabled)
|
||||
.defaultPosition(UnitPoint.center)
|
||||
|
||||
Settings {
|
||||
SettingsView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
MacNearFuture/NFCommands.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// NFCommands.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 21/05/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct NearFutureCommands: Commands {
|
||||
var body: some Commands {
|
||||
CommandGroup(after: CommandGroupPlacement.appInfo) {
|
||||
// Text("hi")
|
||||
}
|
||||
}
|
||||
}
|
||||
33
MacNearFuture/Views/ArchiveView.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ArchiveView.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 28/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ArchiveView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
|
||||
var filteredEvents: [Event] {
|
||||
return viewModel.events.filter { $0.complete }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
ForEach(filteredEvents) { event in
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ArchiveView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
88
MacNearFuture/Views/ContentViewMac.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 21/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var showAddEventView: Bool = false
|
||||
@State private var symbolSearchInput: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List {
|
||||
NavigationLink {
|
||||
HomeView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "house")
|
||||
Text("Home")
|
||||
}
|
||||
NavigationLink {
|
||||
ArchiveView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "tray.full")
|
||||
Text("Archive")
|
||||
}
|
||||
NavigationLink {
|
||||
SymbolsPicker(
|
||||
selection: .constant(""),
|
||||
browsing: true
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "star.circle")
|
||||
Text("Symbols")
|
||||
}
|
||||
NavigationLink {
|
||||
SettingsView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
Text("Settings")
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
Text("Welcome to Near Future")
|
||||
}
|
||||
.tint(settingsModel.settings.tint.color)
|
||||
.frame(minWidth: 450, minHeight: 550)
|
||||
.containerBackground(.regularMaterial, for: .window)
|
||||
.sheet(isPresented: $settingsModel.settings.showWhatsNew) {
|
||||
WhatsNewView(settingsModel: settingsModel)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
.sheet(isPresented: $showAddEventView) {
|
||||
AddEventView(
|
||||
viewModel: viewModel
|
||||
)
|
||||
.presentationSizing(.page)
|
||||
}
|
||||
.toolbar {
|
||||
Button() {
|
||||
showAddEventView.toggle()
|
||||
} label: {
|
||||
Label("New", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
50
MacNearFuture/Views/HomeView.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// HomeView.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 28/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var searchInput: String = ""
|
||||
|
||||
var filteredEvents: [Event] {
|
||||
if searchInput.isEmpty {
|
||||
if settingsModel.settings.showCompletedInHome {
|
||||
return viewModel.events
|
||||
} else {
|
||||
return viewModel.events.filter() { !$0.complete }
|
||||
}
|
||||
} else {
|
||||
return viewModel.events.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(searchInput) ||
|
||||
$0.notes.localizedCaseInsensitiveContains(searchInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
ForEach(viewModel.events) { event in
|
||||
if filteredEvents.contains(event) {
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.id(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchInput)
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HomeView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
155
MacNearFuture/Views/SettingsView.swift
Normal file
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 13/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var importStr: String = ""
|
||||
|
||||
func changeIcon(to toIcon: String) {
|
||||
if let nsimage = NSImage(named: toIcon) {
|
||||
let nsImageView = NSImageView(image: nsimage)
|
||||
nsImageView.frame = NSRect(x: 0, y: 0, width: 128, height: 128)
|
||||
NSApplication.shared.dockTile.contentView = nsImageView
|
||||
NSApplication.shared.dockTile.display()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(settingsModel.accentChoices, id: \.self) { choice in
|
||||
let color = Color(nsColor: NSColor(named: "uiColors/\(choice)")!)
|
||||
ZStack {
|
||||
Button() {
|
||||
settingsModel.changeTint(to: choice)
|
||||
changeIcon(to: choice)
|
||||
} label: {
|
||||
Circle()
|
||||
.foregroundStyle(color)
|
||||
.frame(width: 30)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
if ColorCodable(color) == settingsModel.settings.tint {
|
||||
let needContrast: Bool = ColorCodable(color) == settingsModel.settings.tint
|
||||
Circle()
|
||||
.foregroundStyle(needContrast ? .two : .one)
|
||||
.frame(width: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Button("Show What's New") {
|
||||
settingsModel.settings.showWhatsNew = true
|
||||
}
|
||||
Toggle("Show completed Events in Home", isOn: $settingsModel.settings.showCompletedInHome)
|
||||
.onChange(of: settingsModel.settings.showCompletedInHome) { _ in
|
||||
settingsModel.saveSettings()
|
||||
}
|
||||
NavigationLink() {
|
||||
List {
|
||||
if !settingsModel.notifsGranted {
|
||||
Text("\(Image(systemName: "xmark")) Notifications disabled for Near Future")
|
||||
.foregroundStyle(.red)
|
||||
Button("Request Notifications") {
|
||||
Task.detached {
|
||||
let requestNotifsResult = await requestNotifs()
|
||||
await MainActor.run {
|
||||
settingsModel.notifsGranted = requestNotifsResult
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("\(Image(systemName: "checkmark")) Notifications enabled for Near Future")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "bell.badge.fill")
|
||||
Text("Notifications")
|
||||
}
|
||||
NavigationLink() {
|
||||
iCloudSettingsView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "icloud.fill")
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(viewModel.iCloudStatusColor)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
NavigationLink() {
|
||||
ImportView(viewModel: viewModel, importStr: $importStr)
|
||||
} label: {
|
||||
Label("Import Events", systemImage: "tray.and.arrow.down.fill")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
NavigationLink() {
|
||||
ExportView(viewModel: viewModel)
|
||||
} label: {
|
||||
Label("Export Events", systemImage: "square.and.arrow.up")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
|
||||
Text("Tip")
|
||||
.font(.subheadline)
|
||||
Text("Near Future has Widgets!")
|
||||
|
||||
Text("Danger Zone")
|
||||
.foregroundStyle(.red)
|
||||
.font(.subheadline)
|
||||
Button("Delete local data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
}
|
||||
Button("Delete iCloud data", role: .destructive) {
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
Button("Delete all data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
|
||||
Text("Debug")
|
||||
.foregroundStyle(.red)
|
||||
.font(.subheadline)
|
||||
Button("Reset UserDefaults", role: .destructive) {
|
||||
viewModel.dangerResetLocalData()
|
||||
}
|
||||
Button("Reset iCloud", role: .destructive) {
|
||||
viewModel.dangerResetiCloud()
|
||||
}
|
||||
|
||||
// AboutView()
|
||||
|
||||
.modifier(navigationInlineLarge())
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"originHash" : "19df39f99b22f4ef95b73ed292ffb0c8d7694dd4c9db2b96ea73b091b7b1a026",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "sfsymbolspicker",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/alessiorubicini/SFSymbolsPicker",
|
||||
"state" : {
|
||||
"revision" : "73c909b8a7fc77a30dd04208e33f759f8b52c4c8",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1630"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A920C2832D24011300E4F9B1"
|
||||
BuildableName = "NearFuture.app"
|
||||
BlueprintName = "NearFuture"
|
||||
BlueprintIdentifier = "A90D49252DDE0FA400781124"
|
||||
BuildableName = "Near Future.app"
|
||||
BlueprintName = "MacNearFuture"
|
||||
ReferencedContainer = "container:NearFuture.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -32,8 +32,8 @@
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -44,9 +44,9 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A920C2832D24011300E4F9B1"
|
||||
BuildableName = "NearFuture.app"
|
||||
BlueprintName = "NearFuture"
|
||||
BlueprintIdentifier = "A90D49252DDE0FA400781124"
|
||||
BuildableName = "Near Future.app"
|
||||
BlueprintName = "MacNearFuture"
|
||||
ReferencedContainer = "container:NearFuture.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -61,9 +61,9 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A920C2832D24011300E4F9B1"
|
||||
BuildableName = "NearFuture.app"
|
||||
BlueprintName = "NearFuture"
|
||||
BlueprintIdentifier = "A90D49252DDE0FA400781124"
|
||||
BuildableName = "Near Future.app"
|
||||
BlueprintName = "MacNearFuture"
|
||||
ReferencedContainer = "container:NearFuture.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -4,24 +4,29 @@
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>NearFuture nodebug.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>NearFuture.xcscheme_^#shared#^_</key>
|
||||
<key>MacNearFuture.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>NearFutureWidgetsExtension.xcscheme_^#shared#^_</key>
|
||||
<key>NearFuture.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>NearFutureWidgetsExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>A90D49252DDE0FA400781124</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>A920C2832D24011300E4F9B1</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
//
|
||||
// AddEventView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 25/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSymbolsPicker
|
||||
|
||||
struct AddEventView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
|
||||
@Binding var eventName: String
|
||||
@Binding var eventComplete: Bool
|
||||
@Binding var eventCompleteDesc: String
|
||||
@Binding var eventSymbol: String
|
||||
@Binding var eventColor: Color
|
||||
@Binding var eventNotes: String
|
||||
@Binding var eventDate: Date
|
||||
@Binding var eventRecurrence: Event.RecurrenceType
|
||||
|
||||
@State var adding: Bool
|
||||
@State var showNeedsNameAlert: Bool = false
|
||||
@State var isSymbolPickerPresented = false
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
private enum Field {
|
||||
case Name, Notes
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(
|
||||
header:
|
||||
Text("Event Details")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
) {
|
||||
// name & symbol
|
||||
HStack(spacing: 5) {
|
||||
Button() {
|
||||
isSymbolPickerPresented.toggle()
|
||||
} label: {
|
||||
Image(systemName: eventSymbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(eventColor)
|
||||
}
|
||||
.frame(width: 20)
|
||||
.buttonStyle(.borderless)
|
||||
.sheet(isPresented: $isSymbolPickerPresented) {
|
||||
SymbolsPicker(
|
||||
selection: $eventSymbol,
|
||||
title: "Choose a Symbol",
|
||||
searchLabel: "Search...",
|
||||
autoDismiss: true)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
ColorPicker("", selection: $eventColor, supportsOpacity: false)
|
||||
.fixedSize()
|
||||
ZStack {
|
||||
TextField("Event Name", text: $eventName)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding(.trailing, eventName.isEmpty ? 0 : 30)
|
||||
.animation(.spring, value: eventName)
|
||||
.focused($focusedField, equals: Field.Name)
|
||||
.submitLabel(.next)
|
||||
.onSubmit {
|
||||
focusedField = .Notes
|
||||
}
|
||||
MagicClearButton(text: $eventName)
|
||||
}
|
||||
}
|
||||
|
||||
// dscription
|
||||
ZStack {
|
||||
TextField("Event Notes", text: $eventNotes)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding(.trailing, eventNotes.isEmpty ? 0 : 30)
|
||||
.animation(.spring, value: eventNotes)
|
||||
.focused($focusedField, equals: Field.Notes)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
focusedField = nil
|
||||
}
|
||||
MagicClearButton(text: $eventNotes)
|
||||
}
|
||||
|
||||
|
||||
// date picker
|
||||
HStack {
|
||||
Spacer()
|
||||
DatePicker("", selection: $eventDate, displayedComponents: .date)
|
||||
.datePickerStyle(WheelDatePickerStyle())
|
||||
Spacer()
|
||||
Button() {
|
||||
eventDate = Date()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.left")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.frame(width: 20)
|
||||
}
|
||||
|
||||
DatePicker(
|
||||
"",
|
||||
selection: $eventDate,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
|
||||
// re-ocurrence Picker
|
||||
Picker("Recurrence", selection: $eventRecurrence) {
|
||||
ForEach(Event.RecurrenceType.allCases, id: \.self) { recurrence in
|
||||
Text(recurrence.rawValue.capitalized)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
Text(
|
||||
describeOccurrence(
|
||||
date: eventDate,
|
||||
recurrence: eventRecurrence
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("\(adding ? "Add Event" : "")")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if adding {
|
||||
Button() {
|
||||
resetAddEventView()
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
if adding {
|
||||
Button {
|
||||
viewModel.addEvent(
|
||||
newEvent: Event(
|
||||
name: eventName,
|
||||
complete: eventComplete,
|
||||
completeDesc: eventCompleteDesc,
|
||||
symbol: eventSymbol,
|
||||
color: ColorCodable(eventColor),
|
||||
notes: eventNotes,
|
||||
date: eventDate,
|
||||
recurrence: eventRecurrence
|
||||
)
|
||||
)
|
||||
resetAddEventView()
|
||||
} label: {
|
||||
Text("Save")
|
||||
.font(.headline)
|
||||
.cornerRadius(10)
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
}
|
||||
.disabled(eventName.isEmpty)
|
||||
.onTapGesture {
|
||||
if eventName.isEmpty {
|
||||
showNeedsNameAlert.toggle()
|
||||
}
|
||||
}
|
||||
.alert("Missing Name", isPresented: $showNeedsNameAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
showNeedsNameAlert.toggle()
|
||||
focusedField = .Name
|
||||
}
|
||||
} message: {
|
||||
Text("Give your Event a name before saving.")
|
||||
}
|
||||
if eventName.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark")
|
||||
.foregroundStyle(.red)
|
||||
Text("Give your event a name.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func resetAddEventView() {
|
||||
//reset addeventView
|
||||
eventName = viewModel.template.name
|
||||
eventComplete = viewModel.template.complete
|
||||
eventCompleteDesc = viewModel.template.completeDesc
|
||||
eventSymbol = viewModel.template.symbol
|
||||
eventColor = randomColor()
|
||||
eventNotes = viewModel.template.notes
|
||||
eventDate = viewModel.template.date
|
||||
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()
|
||||
Color.orange
|
||||
.ignoresSafeArea(.all)
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddEventView(
|
||||
viewModel: vm,
|
||||
eventName: .constant(vm.template.notes),
|
||||
eventComplete: .constant(vm.template.complete),
|
||||
eventCompleteDesc: .constant(vm.template.completeDesc),
|
||||
eventSymbol: .constant(vm.template.symbol),
|
||||
eventColor: .constant(vm.template.color.color),
|
||||
eventNotes: .constant(vm.template.notes),
|
||||
eventDate: .constant(vm.template.date),
|
||||
eventRecurrence: .constant(vm.template.recurrence),
|
||||
adding: true
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// ArchiveView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 25/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ArchiveView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@State var showAddEvent: Bool = false
|
||||
@State var hey: UUID = UUID()
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
if viewModel.events.filter({$0.complete}).isEmpty {
|
||||
HelpView(showAddEvent: $showAddEvent)
|
||||
} else {
|
||||
ScrollView {
|
||||
ForEach(viewModel.events.filter({$0.complete})) { event in
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.id(hey)
|
||||
.onReceive(viewModel.objectWillChange) {
|
||||
hey = UUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
AddEventButton(showingAddEventView: $showAddEvent)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Archive")
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddEvent) {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
eventName: $viewModel.editableTemplate.name,
|
||||
eventComplete: $viewModel.editableTemplate.complete,
|
||||
eventCompleteDesc: $viewModel.editableTemplate.completeDesc,
|
||||
eventSymbol: $viewModel.editableTemplate.symbol,
|
||||
eventColor: $viewModel.editableTemplate.color.colorBind,
|
||||
eventNotes: $viewModel.editableTemplate.notes,
|
||||
eventDate: $viewModel.editableTemplate.date,
|
||||
eventRecurrence: $viewModel.editableTemplate.recurrence,
|
||||
adding: true
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ArchiveView(viewModel: dummyEventViewModel())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 24/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
enum Field {
|
||||
case Search
|
||||
}
|
||||
enum Tab {
|
||||
case Home
|
||||
case Archive
|
||||
case Statistics
|
||||
case Settings
|
||||
case Gradient
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
@State private var eventName = ""
|
||||
@State private var eventComplete = false
|
||||
@State private var eventCompleteDesc = ""
|
||||
@State private var eventSymbol = "star"
|
||||
@State private var eventColor: Color = randomColor()
|
||||
@State private var eventNotes = ""
|
||||
@State private var eventDate = Date()
|
||||
@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.filter() {!$0.complete}
|
||||
} else {
|
||||
return viewModel.events.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(searchInput) ||
|
||||
$0.notes.localizedCaseInsensitiveContains(searchInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var noEvents: Bool {
|
||||
if viewModel.events.count == 0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
@FocusState private var focusedTab: Tab?
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
VStack {
|
||||
ZStack {
|
||||
SearchBar(searchInput: $searchInput)
|
||||
.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 {
|
||||
ScrollView {
|
||||
ForEach(filteredEvents) { event in
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
}
|
||||
.onDelete(perform: viewModel.removeEvent)
|
||||
.id(hey)
|
||||
.onReceive(viewModel.objectWillChange) {
|
||||
hey = UUID()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
if /*!searchInput.isEmpty && */filteredEvents.isEmpty {
|
||||
HelpView(
|
||||
searchInput: $searchInput,
|
||||
focusedField: focusedField
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Near Future")
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddEventView) {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
eventName: $eventName,
|
||||
eventComplete: $eventComplete,
|
||||
eventCompleteDesc: $eventCompleteDesc,
|
||||
eventSymbol: $eventSymbol,
|
||||
eventColor: $eventColor,
|
||||
eventNotes: $eventNotes,
|
||||
eventDate: $eventDate,
|
||||
eventRecurrence: $eventRecurrence,
|
||||
adding: true //adding event
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.apply {
|
||||
if #available(iOS 16.4, *) {
|
||||
$0.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
AddEventButton(showingAddEventView: $showingAddEventView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
.focused($focusedTab, equals: Tab.Home)
|
||||
ArchiveView(viewModel: viewModel)
|
||||
.tabItem() {
|
||||
Label("Archive", systemImage: "tray.full")
|
||||
}
|
||||
.focused($focusedTab, equals: Tab.Archive)
|
||||
StatsView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label("Statistics", systemImage: "chart.pie")
|
||||
}
|
||||
.focused($focusedTab, equals: Tab.Statistics)
|
||||
SettingsView(viewModel: viewModel, settingsModel: settingsModel)
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
.focused($focusedTab, equals: Tab.Settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
var appearance: ColorScheme {
|
||||
return UITraitCollection.current.userInterfaceStyle == .dark ? .dark : .light
|
||||
}
|
||||
var backgroundGradient: some View {
|
||||
return LinearGradient(
|
||||
gradient: Gradient(colors: [.bgTop, .two]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// EditEventView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditEventView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@Binding var event: Event
|
||||
|
||||
fileprivate func saveEdits() {
|
||||
//if there is an event in vM.events with the id of the event we r editing,
|
||||
//firstindex - loops through the arr and finds first element where that events id matches editing event's id
|
||||
if let index = viewModel.events.firstIndex(where: { xEvent in
|
||||
xEvent.id == event.id
|
||||
}) {
|
||||
viewModel.events[index] = event
|
||||
}
|
||||
viewModel.saveEvents()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
eventName: $event.name,
|
||||
eventComplete: $event.complete,
|
||||
eventCompleteDesc: $event.completeDesc,
|
||||
eventSymbol: $event.symbol,
|
||||
eventColor: $event.color.colorBind,
|
||||
eventNotes: $event.notes,
|
||||
eventDate: $event.date,
|
||||
eventRecurrence: $event.recurrence,
|
||||
adding: false //bc we editing existing event
|
||||
)
|
||||
.navigationTitle("Edit Event")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button() {
|
||||
saveEdits()
|
||||
} label: {
|
||||
Text("Done")
|
||||
.bold()
|
||||
}
|
||||
.disabled(event.name == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let vm = dummyEventViewModel()
|
||||
EditEventView(
|
||||
viewModel: vm,
|
||||
event: .constant(vm.example)
|
||||
)
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// 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;==")
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -1,425 +0,0 @@
|
||||
//
|
||||
// Item.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 24/12/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
//@Model
|
||||
//final class Item {
|
||||
// var timestamp: Date
|
||||
//
|
||||
// init(timestamp: Date) {
|
||||
// self.timestamp = timestamp
|
||||
// }
|
||||
//}
|
||||
|
||||
struct Event: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
var complete: Bool
|
||||
var completeDesc: String
|
||||
var symbol: String
|
||||
var color: ColorCodable
|
||||
var notes: String
|
||||
var date: Date
|
||||
var recurrence: RecurrenceType
|
||||
|
||||
enum RecurrenceType: String, Codable, CaseIterable {
|
||||
case none, daily, weekly, monthly, yearly
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var color: Color {
|
||||
Color(red: red, green: green, blue: blue)
|
||||
}
|
||||
var colorBind: Color {
|
||||
get {
|
||||
return Color(
|
||||
red: red,
|
||||
green: green,
|
||||
blue: blue
|
||||
)
|
||||
} set {
|
||||
let cc = ColorCodable(newValue)
|
||||
self.red = cc.red
|
||||
self.green = cc.green
|
||||
self.blue = cc.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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EventViewModel: ObservableObject {
|
||||
@Published var events: [Event] = []
|
||||
@Published var icloudData: [Event] = []
|
||||
|
||||
public let template: Event = Event(
|
||||
name: "",
|
||||
complete: false,
|
||||
completeDesc: "",
|
||||
symbol: "star",
|
||||
color: ColorCodable(randomColor()),
|
||||
notes: "",
|
||||
date: Date(),
|
||||
recurrence: .none
|
||||
)
|
||||
@Published var editableTemplate: Event
|
||||
@Published var example: Event = Event(
|
||||
name: "event",
|
||||
complete: false,
|
||||
completeDesc: "dofajiof",
|
||||
symbol: "star",
|
||||
color: ColorCodable(.orange),
|
||||
notes: "lksdjfakdflkasjlkjl",
|
||||
date: Date(),
|
||||
recurrence: .daily
|
||||
)
|
||||
|
||||
@Published var lastSync: Date? = nil
|
||||
@Published var icloudEventCount: Int = 0
|
||||
@Published var localEventCount: Int = 0
|
||||
@Published var syncStatus: String = "Not Synced"
|
||||
|
||||
init(load: Bool = true) {
|
||||
self.editableTemplate = template
|
||||
if load {
|
||||
loadEvents()
|
||||
}
|
||||
}
|
||||
|
||||
//appgroup or regular userdefaults
|
||||
let appGroupUserDefaults = UserDefaults(suiteName: "group.NearFuture") ?? UserDefaults.standard
|
||||
|
||||
//icloud store
|
||||
let icloudStore = NSUbiquitousKeyValueStore.default
|
||||
|
||||
// load from icloud or local
|
||||
func loadEvents() {
|
||||
//load icloud 1st
|
||||
if let icData = icloudStore.data(forKey: "events") {
|
||||
let decoder = JSONDecoder()
|
||||
if let decodedIcEvents = try? decoder.decode([Event].self, from: icData) {
|
||||
self.icloudData = decodedIcEvents
|
||||
self.events = decodedIcEvents
|
||||
}
|
||||
}
|
||||
|
||||
if events.isEmpty, let savedData = appGroupUserDefaults.data(forKey: "events") {
|
||||
let decoder = JSONDecoder()
|
||||
if let decodedEvents = try? decoder.decode([Event].self, from: savedData) {
|
||||
self.events = decodedEvents
|
||||
}
|
||||
}
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
// save to local and icloud
|
||||
func saveEvents() {
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(events) {
|
||||
appGroupUserDefaults.set(encoded, forKey: "events")
|
||||
|
||||
//sync
|
||||
icloudStore.set(encoded, forKey: "events")
|
||||
icloudStore.synchronize()
|
||||
|
||||
updateSyncStatus()
|
||||
loadEvents()
|
||||
WidgetCenter.shared.reloadAllTimelines()//reload all widgets when saving events
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSyncStatus() {
|
||||
lastSync = Date()
|
||||
icloudEventCount = icloudData.count
|
||||
localEventCount = events.count
|
||||
|
||||
if icloudEventCount == localEventCount {
|
||||
syncStatus = "Successful"
|
||||
} else {
|
||||
syncStatus = "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
func addEvent(newEvent: Event) {
|
||||
events.append(newEvent)
|
||||
saveEvents() //sync with icloud
|
||||
}
|
||||
|
||||
func removeEvent(at index: IndexSet) {
|
||||
events.remove(atOffsets: index)
|
||||
saveEvents() //sync local and icl
|
||||
}
|
||||
|
||||
func hasUbiquitousKeyValueStore() -> Bool {
|
||||
let icloud = NSUbiquitousKeyValueStore.default
|
||||
|
||||
let key = "com.neon443.NearFuture.testkey"
|
||||
let value = "testValue"
|
||||
|
||||
icloud.set(value, forKey: key)
|
||||
icloud.synchronize()
|
||||
|
||||
if icloud.string(forKey: key) != nil {
|
||||
icloud.removeObject(forKey: key)
|
||||
icloud.synchronize()
|
||||
return true
|
||||
} else {
|
||||
print("!has UbiquitousKeyValueStore")
|
||||
icloud.removeObject(forKey: key)
|
||||
icloud.synchronize()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func sync() {
|
||||
NSUbiquitousKeyValueStore.default.synchronize()
|
||||
loadEvents()
|
||||
}
|
||||
|
||||
func replaceLocalWithiCloudData() {
|
||||
icloudStore.synchronize()
|
||||
self.events = self.icloudData
|
||||
saveEvents()
|
||||
}
|
||||
|
||||
func replaceiCloudWithLocalData() {
|
||||
icloudStore.synchronize()
|
||||
self.icloudData = self.events
|
||||
saveEvents()
|
||||
}
|
||||
|
||||
func exportEvents() -> String {
|
||||
let encoder = JSONEncoder()
|
||||
if let json = try? encoder.encode(self.events) {
|
||||
return "\(json.base64EncodedString())"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func importEvents(_ imported: String) throws {
|
||||
guard let data = Data(base64Encoded: imported) else {
|
||||
throw importError.invalidB64
|
||||
}
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
let decoded = try decoder.decode([Event].self, from: data)
|
||||
self.events = decoded
|
||||
saveEvents()
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Danger Zone
|
||||
func dangerClearLocalData() {
|
||||
UserDefaults.standard.removeObject(forKey: "events")
|
||||
appGroupUserDefaults.removeObject(forKey: "events")
|
||||
events.removeAll()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
func dangerCleariCloudData() {
|
||||
icloudStore.removeObject(forKey: "events")
|
||||
icloudStore.synchronize()
|
||||
icloudData.removeAll()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
func dangerResetLocalData() {
|
||||
let userDFDict = UserDefaults.standard.dictionaryRepresentation()
|
||||
for key in userDFDict.keys {
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
let appGUSDDict = appGroupUserDefaults.dictionaryRepresentation()
|
||||
for key in appGUSDDict.keys {
|
||||
appGroupUserDefaults.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
events.removeAll()
|
||||
updateSyncStatus()
|
||||
}
|
||||
|
||||
func dangerResetiCloud() {
|
||||
let icloudDict = icloudStore.dictionaryRepresentation
|
||||
for key in icloudDict.keys {
|
||||
icloudStore.removeObject(forKey: key)
|
||||
}
|
||||
icloudStore.synchronize()
|
||||
icloudData.removeAll()
|
||||
updateSyncStatus()
|
||||
}
|
||||
}
|
||||
|
||||
class dummyEventViewModel: EventViewModel {
|
||||
override init(load: Bool = false) {
|
||||
super.init(load: false)
|
||||
self.events = [self.example, self.template, self.example, self.template]
|
||||
self.events[0].complete.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
switch recurrence {
|
||||
case .none:
|
||||
recurrenceDescription = "Occurs once on"
|
||||
case .daily:
|
||||
recurrenceDescription = "Repeats every day from"
|
||||
case .weekly:
|
||||
recurrenceDescription = "Repeats every week from"
|
||||
case .monthly:
|
||||
recurrenceDescription = "Repeats every month from"
|
||||
case .yearly:
|
||||
recurrenceDescription = "Repeats every year from"
|
||||
}
|
||||
|
||||
return "\(recurrenceDescription) \(dateString)"
|
||||
}
|
||||
|
||||
func randomRainbowColor() -> Color {
|
||||
return [
|
||||
Color.red,
|
||||
Color.orange,
|
||||
Color.yellow,
|
||||
Color.green,
|
||||
Color.blue,
|
||||
Color.indigo,
|
||||
Color.purple
|
||||
].randomElement()!
|
||||
}
|
||||
|
||||
func randomColor() -> Color {
|
||||
let r = Double.random(in: 0...1)
|
||||
let g = Double.random(in: 0...1)
|
||||
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
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// NearFutureApp.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 24/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct NearFutureApp: App {
|
||||
// var sharedModelContainer: ModelContainer = {
|
||||
// let schema = Schema([
|
||||
// Item.self,
|
||||
// ])
|
||||
// let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
//
|
||||
// do {
|
||||
// return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
// } catch {
|
||||
// fatalError("Could not create ModelContainer: \(error)")
|
||||
// }
|
||||
// }()
|
||||
@StateObject var settingsModel: SettingsViewModel = SettingsViewModel()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(
|
||||
viewModel: EventViewModel(),
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
.tint(settingsModel.settings.tint.color)
|
||||
}
|
||||
// .modelContainer(sharedModelContainer)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// AppIntent.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource = "Configuration"
|
||||
static var description = IntentDescription("This is an example widget.")
|
||||
|
||||
// An example configurable parameter.
|
||||
@Parameter(title: "Favorite Emoji", default: "😃")
|
||||
var favoriteEmoji: String
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,84 +0,0 @@
|
||||
//
|
||||
// NearFutureWidgets.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: AppIntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
|
||||
}
|
||||
|
||||
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), configuration: configuration)
|
||||
}
|
||||
|
||||
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, configuration: configuration)
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let configuration: ConfigurationAppIntent
|
||||
}
|
||||
|
||||
struct NearFutureWidgetsEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Favorite Emoji:")
|
||||
Text(entry.configuration.favoriteEmoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NearFutureWidgets: Widget {
|
||||
let kind: String = "NearFutureWidgets"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
|
||||
NearFutureWidgetsEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConfigurationAppIntent {
|
||||
fileprivate static var smiley: ConfigurationAppIntent {
|
||||
let intent = ConfigurationAppIntent()
|
||||
intent.favoriteEmoji = "😀"
|
||||
return intent
|
||||
}
|
||||
|
||||
fileprivate static var starEyes: ConfigurationAppIntent {
|
||||
let intent = ConfigurationAppIntent()
|
||||
intent.favoriteEmoji = "🤩"
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
NearFutureWidgets()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, configuration: .smiley)
|
||||
SimpleEntry(date: .now, configuration: .starEyes)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// NearFutureWidgetsBundle.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct NearFutureWidgetsBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
NearFutureWidgets()
|
||||
NearFutureWidgetsLiveActivity()
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// NearFutureWidgetsLiveActivity.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
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,194 +0,0 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 29/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var hasUbiquitous: Bool = false
|
||||
@State private var lastSyncWasSuccessful: Bool = false
|
||||
@State private var lastSyncWasNormalAgo: Bool = false
|
||||
@State private var localCountEqualToiCloud: Bool = false
|
||||
@State private var icloudCountEqualToLocal: Bool = false
|
||||
@State private var importStr: String = ""
|
||||
|
||||
func updateStatus() {
|
||||
let vm = viewModel
|
||||
hasUbiquitous = vm.hasUbiquitousKeyValueStore()
|
||||
lastSyncWasSuccessful = vm.syncStatus.contains("Success")
|
||||
lastSyncWasNormalAgo = vm.lastSync?.timeIntervalSinceNow.isNormal ?? false
|
||||
localCountEqualToiCloud = vm.localEventCount == vm.icloudEventCount
|
||||
icloudCountEqualToLocal = vm.icloudEventCount == vm.localEventCount
|
||||
}
|
||||
|
||||
var iCloudStatusColor: Color {
|
||||
let allTrue = hasUbiquitous && lastSyncWasSuccessful && lastSyncWasNormalAgo && localCountEqualToiCloud && icloudCountEqualToLocal
|
||||
let someTrue = hasUbiquitous || lastSyncWasSuccessful || lastSyncWasNormalAgo || localCountEqualToiCloud || icloudCountEqualToLocal
|
||||
|
||||
if allTrue {
|
||||
return .green
|
||||
} else if someTrue {
|
||||
return .orange
|
||||
} else {
|
||||
return .red
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
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() {
|
||||
iCloudSettingsView(
|
||||
viewModel: viewModel,
|
||||
hasUbiquitous: $hasUbiquitous,
|
||||
lastSyncWasSuccessful: $lastSyncWasSuccessful,
|
||||
lastSyncWasNormalAgo: $lastSyncWasNormalAgo,
|
||||
localCountEqualToiCloud: $localCountEqualToiCloud,
|
||||
icloudCountEqualToLocal: $icloudCountEqualToLocal,
|
||||
updateStatus: updateStatus
|
||||
)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "icloud.fill")
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(iCloudStatusColor)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
}
|
||||
NavigationLink() {
|
||||
ImportView(viewModel: viewModel, importStr: $importStr)
|
||||
} label: {
|
||||
Label("Import Events", systemImage: "tray.and.arrow.down.fill")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
NavigationLink() {
|
||||
ExportView(viewModel: viewModel)
|
||||
} label: {
|
||||
Label("Export Events", systemImage: "square.and.arrow.up")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
|
||||
Section("Tip") {
|
||||
Text("Near Future has Widgets!")
|
||||
}
|
||||
|
||||
Section("Danger Zone") {
|
||||
Button("Delete local data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
}
|
||||
Button("Delete iCloud data", role: .destructive) {
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
Button("Delete all data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
}
|
||||
Section("Debug") {
|
||||
Button("Reset UserDefaults", role: .destructive) {
|
||||
viewModel.dangerResetLocalData()
|
||||
}
|
||||
Button("Reset iCloud", role: .destructive) {
|
||||
viewModel.dangerResetiCloud()
|
||||
}
|
||||
}
|
||||
Section("About") {
|
||||
VStack {
|
||||
if let image = UIImage(named: getAppIcon()) {
|
||||
Image(uiImage: image)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
|
||||
func getAppIcon() -> String {
|
||||
let bundle = Bundle.main
|
||||
guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
|
||||
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
|
||||
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
|
||||
let iconFileName = iconFiles.last else {
|
||||
fatalError("hell na where ur icon")
|
||||
}
|
||||
return iconFileName
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
67
NearFuture/Views/Archive/ArchiveView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// ArchiveView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 25/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ArchiveView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@State var showAddEvent: Bool = false
|
||||
var filteredEvents: [Event] {
|
||||
let filteredEvents = viewModel.events.filter({$0.complete})
|
||||
return filteredEvents.reversed()
|
||||
}
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
if viewModel.events.filter({$0.complete}).isEmpty {
|
||||
HelpView(showAddEvent: $showAddEvent)
|
||||
} else {
|
||||
ScrollView {
|
||||
ForEach(filteredEvents) { event in
|
||||
NavigationLink() {
|
||||
EditEventView(
|
||||
viewModel: viewModel,
|
||||
event: Binding(
|
||||
get: { event },
|
||||
set: { newValue in
|
||||
viewModel.editEvent(newValue)
|
||||
}
|
||||
)
|
||||
)
|
||||
} label: {
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.id(event.complete)
|
||||
}
|
||||
.transition(.moveAndFadeReversed)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.animation(.default, value: filteredEvents)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.scrollContentBackground(.hidden)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AddEventButton(showingAddEventView: $showAddEvent)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Archive")
|
||||
.modifier(navigationInlineLarge())
|
||||
}
|
||||
.sheet(isPresented: $showAddEvent) {
|
||||
AddEventView(
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ArchiveView(viewModel: dummyEventViewModel())
|
||||
}
|
||||
69
NearFuture/Views/ContentView.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 24/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import SwiftData
|
||||
|
||||
enum Tab {
|
||||
case home
|
||||
case archive
|
||||
case symbols
|
||||
case stats
|
||||
case settings
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject var viewModel: EventViewModel
|
||||
@StateObject var settingsModel: SettingsViewModel
|
||||
@State var tabSelection: Tab = .home
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $tabSelection) {
|
||||
HomeView(viewModel: viewModel, settingsModel: settingsModel)
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
.tag(Tab.home)
|
||||
ArchiveView(viewModel: viewModel)
|
||||
.tabItem() {
|
||||
Label("Archive", systemImage: "tray.full")
|
||||
}
|
||||
.tag(Tab.archive)
|
||||
SymbolsPicker(
|
||||
selection: .constant(""),
|
||||
browsing: true
|
||||
)
|
||||
.tabItem {
|
||||
Label("Symbols", systemImage: "star.circle")
|
||||
}
|
||||
.tag(Tab.symbols)
|
||||
StatsView(viewModel: viewModel)
|
||||
// SymbolsPickerStoryboardUIViewRepresentable()
|
||||
.tabItem {
|
||||
Label("Statistics", systemImage: "chart.pie")
|
||||
}
|
||||
.tag(Tab.stats)
|
||||
SettingsView(viewModel: viewModel, settingsModel: settingsModel)
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
.tag(Tab.settings)
|
||||
}
|
||||
.modifier(hapticHeavy(trigger: tabSelection))
|
||||
.sheet(isPresented: $settingsModel.settings.showWhatsNew) {
|
||||
WhatsNewView(settingsModel: settingsModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
214
NearFuture/Views/Events/AddEventView.swift
Normal file
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// AddEventView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 25/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddEventView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
|
||||
@State var event: Event = dummyEventViewModel().template
|
||||
|
||||
@State var adding: Bool = true
|
||||
@State var showNeedsNameAlert: Bool = false
|
||||
@State var isSymbolPickerPresented: Bool = false
|
||||
|
||||
@State private var bye: Bool = false
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
private enum Field {
|
||||
case Name, Notes
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var isMac: Bool {
|
||||
if #available(iOS 1, *) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !adding {
|
||||
backgroundGradient
|
||||
}
|
||||
NavigationStack {
|
||||
List {
|
||||
Section(
|
||||
header:
|
||||
Text("Event Details")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
) {
|
||||
// name & symbol
|
||||
HStack(spacing: 5) {
|
||||
Button() {
|
||||
isSymbolPickerPresented.toggle()
|
||||
} label: {
|
||||
Image(systemName: event.symbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(event.color.color)
|
||||
}
|
||||
.frame(width: 20)
|
||||
.buttonStyle(.borderless)
|
||||
.sheet(isPresented: $isSymbolPickerPresented) {
|
||||
SymbolsPicker(
|
||||
selection: $event.symbol
|
||||
)
|
||||
.presentationDetents([.medium])
|
||||
.modifier(presentationSizeForm())
|
||||
}
|
||||
TextField("Event Name", text: $event.name)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
|
||||
// dscription
|
||||
ZStack {
|
||||
TextField("Event Notes", text: $event.notes)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding(.trailing, event.notes.isEmpty ? 0 : 30)
|
||||
.animation(.spring, value: event.notes)
|
||||
.focused($focusedField, equals: Field.Notes)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
focusedField = nil
|
||||
}
|
||||
}
|
||||
|
||||
ColorPicker("Event Color", selection: $event.color.colorBind)
|
||||
|
||||
// date picker
|
||||
HStack {
|
||||
Spacer()
|
||||
DatePicker("", selection: $event.date, displayedComponents: .date)
|
||||
#if os(iOS)
|
||||
.datePickerStyle(.wheel)
|
||||
#else
|
||||
.datePickerStyle(.graphical)
|
||||
#endif
|
||||
Spacer()
|
||||
Button() {
|
||||
event.date = Date()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.left")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.frame(width: 20)
|
||||
}
|
||||
|
||||
DatePicker(
|
||||
"",
|
||||
selection: $event.date,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
#if os(macOS)
|
||||
.datePickerStyle(.stepperField)
|
||||
#endif
|
||||
|
||||
// re-ocurrence Picker
|
||||
Picker("Recurrence", selection: $event.recurrence) {
|
||||
ForEach(Event.RecurrenceType.allCases, id: \.self) { recurrence in
|
||||
Text(recurrence.rawValue.capitalized)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
Text(
|
||||
describeOccurrence(
|
||||
date: event.date,
|
||||
recurrence: event.recurrence
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("\(adding ? "Add Event" : "")")
|
||||
.modifier(navigationInlineLarge())
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if adding {
|
||||
Button() {
|
||||
resetAddEventView()
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.tint(.one)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem() {
|
||||
if adding {
|
||||
Button {
|
||||
viewModel.addEvent(
|
||||
newEvent: event
|
||||
)
|
||||
bye.toggle()
|
||||
resetAddEventView()
|
||||
} label: {
|
||||
Label("Save", systemImage: "checkmark")
|
||||
}
|
||||
.tint(.accent)
|
||||
.modifier(hapticSuccess(trigger: bye))
|
||||
.disabled(event.name.isEmpty)
|
||||
.onTapGesture {
|
||||
if event.name.isEmpty {
|
||||
showNeedsNameAlert.toggle()
|
||||
}
|
||||
}
|
||||
.alert("Missing Name", isPresented: $showNeedsNameAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
showNeedsNameAlert.toggle()
|
||||
focusedField = .Name
|
||||
}
|
||||
} message: {
|
||||
Text("Give your Event a name before saving.")
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
if !adding {
|
||||
Button() {
|
||||
viewModel.editEvent(event)
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Done", systemImage: "checkmark")
|
||||
}
|
||||
.disabled(event.name == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Editing \(event.name) - Ne")
|
||||
}
|
||||
.scrollContentBackground(isMac ? .automatic : .hidden)
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
func resetAddEventView() {
|
||||
//reset addeventView
|
||||
event = viewModel.template
|
||||
dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let vm = dummyEventViewModel()
|
||||
Color.orange
|
||||
.ignoresSafeArea(.all)
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddEventView(
|
||||
viewModel: vm,
|
||||
adding: true
|
||||
)
|
||||
}
|
||||
}
|
||||
31
NearFuture/Views/Events/EditEventView.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// EditEventView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 02/01/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditEventView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@Binding var event: Event
|
||||
|
||||
var body: some View {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
event: event,
|
||||
adding: false //bc we editing existing event
|
||||
)
|
||||
.navigationTitle("Edit Event")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let vm = dummyEventViewModel()
|
||||
EditEventView(
|
||||
viewModel: vm,
|
||||
event: .constant(vm.example)
|
||||
)
|
||||
}
|
||||
219
NearFuture/Views/Home/EventListView.swift
Normal file
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@Environment(\.openWindow) var openWindow
|
||||
|
||||
@State var hovering: Bool = false
|
||||
|
||||
#if canImport(AppKit)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(hovering ? 0.5 : 0.0)
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.frame(width: 7)
|
||||
.foregroundStyle(
|
||||
event.color.color.opacity(
|
||||
event.complete ? 0.5 : 1
|
||||
)
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(systemName: event.symbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.shadow(radius: 5)
|
||||
.foregroundStyle(
|
||||
.one.opacity(
|
||||
event.complete ? 0.5 : 1
|
||||
)
|
||||
)
|
||||
Text("\(event.name)")
|
||||
.bold()
|
||||
.foregroundStyle(.one)
|
||||
.strikethrough(event.complete)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
if !event.notes.isEmpty {
|
||||
Text(event.notes)
|
||||
.foregroundStyle(.one.opacity(0.8))
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
Text(
|
||||
event.date.formatted(
|
||||
date: .long,
|
||||
time: .shortened
|
||||
)
|
||||
)
|
||||
.foregroundStyle(
|
||||
.one.opacity(
|
||||
event.complete ? 0.5 : 1
|
||||
)
|
||||
)
|
||||
if event.recurrence != .none {
|
||||
Text("Occurs \(event.recurrence.rawValue)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(
|
||||
.one.opacity(event.complete ? 0.5 : 1))
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
Text("\(daysUntilEvent(event.date).long)")
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
|
||||
}
|
||||
CompleteEventButton(
|
||||
viewModel: viewModel,
|
||||
event: $event
|
||||
)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.onHover { isHovering in
|
||||
withAnimation {
|
||||
hovering.toggle()
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
openWindow(value: event.id)
|
||||
}
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
let eventToModify = viewModel.events.firstIndex() { currEvent in
|
||||
currEvent.id == event.id
|
||||
}
|
||||
if let eventToModify = eventToModify {
|
||||
viewModel.events.remove(at: eventToModify)
|
||||
viewModel.saveEvents()
|
||||
}
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
var body: some View {
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.frame(width: 7)
|
||||
.foregroundStyle(
|
||||
event.color.color.opacity(
|
||||
event.complete ? 0.5 : 1
|
||||
)
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(systemName: event.symbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.shadow(radius: 5)
|
||||
.foregroundStyle(
|
||||
.one.opacity(
|
||||
event.complete ? 0.5 : 1
|
||||
)
|
||||
)
|
||||
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(event.date.timeIntervalSinceNow < 0 ? .red : .one)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
CompleteEventButton(
|
||||
viewModel: viewModel,
|
||||
event: $event
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.one.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
let eventToModify = viewModel.events.firstIndex() { currEvent in
|
||||
currEvent.id == event.id
|
||||
}
|
||||
if let eventToModify = eventToModify {
|
||||
viewModel.events.remove(at: eventToModify)
|
||||
viewModel.saveEvents()
|
||||
}
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#Preview("EventListView") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ enum HelpType {
|
||||
case Archive
|
||||
}
|
||||
|
||||
enum Field {
|
||||
case Search
|
||||
}
|
||||
|
||||
struct HelpView: View {
|
||||
/// initialises a Search HelpView
|
||||
///
|
||||
@@ -74,7 +78,7 @@ struct HelpView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
ZStack {
|
||||
Color(.tintColor)
|
||||
Color(.accent)
|
||||
.opacity(0.4)
|
||||
.padding(.horizontal, -15)
|
||||
.blur(radius: 5)
|
||||
103
NearFuture/Views/Home/HomeView.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// HomeView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 12/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
struct HomeView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var showingAddEventView: Bool = false
|
||||
@State private var searchInput: String = ""
|
||||
@Environment(\.colorScheme) var appearance
|
||||
var darkMode: Bool {
|
||||
return appearance == .dark
|
||||
}
|
||||
var filteredEvents: [Event] {
|
||||
if searchInput.isEmpty {
|
||||
if settingsModel.settings.showCompletedInHome {
|
||||
return viewModel.events
|
||||
} else {
|
||||
return viewModel.events.filter() {!$0.complete}
|
||||
}
|
||||
} else {
|
||||
return viewModel.events.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(searchInput) ||
|
||||
$0.notes.localizedCaseInsensitiveContains(searchInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
@State private var focusedTab: Tab = .home
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
VStack {
|
||||
if filteredEvents.isEmpty && !searchInput.isEmpty {
|
||||
HelpView(searchInput: $searchInput, focusedField: focusedField)
|
||||
} else {
|
||||
ScrollView {
|
||||
// LazyVStack {
|
||||
ForEach(filteredEvents) { event in
|
||||
NavigationLink() {
|
||||
EditEventView(
|
||||
viewModel: viewModel,
|
||||
event: Binding(
|
||||
get: { event },
|
||||
set: { newValue in
|
||||
viewModel.editEvent(newValue)
|
||||
}
|
||||
)
|
||||
)
|
||||
} label: {
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.id(event.complete)
|
||||
}
|
||||
.transition(.moveAndFade)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
// }
|
||||
if filteredEvents.isEmpty {
|
||||
HelpView(
|
||||
searchInput: $searchInput,
|
||||
focusedField: focusedField
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.animation(.default, value: filteredEvents)
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchInput)
|
||||
.navigationTitle("Near Future")
|
||||
.modifier(navigationInlineLarge())
|
||||
.sheet(isPresented: $showingAddEventView) {
|
||||
AddEventView(
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AddEventButton(showingAddEventView: $showingAddEventView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
HomeView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
31
NearFuture/Views/Misc/AboutView.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// AboutView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 12/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
var body: some View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AboutView()
|
||||
}
|
||||
53
NearFuture/Views/Misc/Buttons.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// 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: {
|
||||
Image(systemName: "plus")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15)
|
||||
.bold()
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MagicClearButton(text: .constant("s"))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddEventButton(showingAddEventView: .constant(false))
|
||||
}
|
||||
@@ -12,7 +12,11 @@ struct ExportView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
Button() {
|
||||
#if canImport(UIKit)
|
||||
UIPasteboard.general.string = viewModel.exportEvents()
|
||||
#else
|
||||
NSPasteboard.general.setString(viewModel.exportEvents(), forType: .string)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Copy Events", systemImage: "document.on.clipboard")
|
||||
}
|
||||
137
NearFuture/Views/Settings/ImportView.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@State private var showAlert: Bool = false
|
||||
|
||||
@State private var replaceCurrentEvents: Bool = false
|
||||
|
||||
fileprivate func importEvents() {
|
||||
do throws {
|
||||
try viewModel.importEvents(importStr, replace: replaceCurrentEvents)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
List {
|
||||
Section("Status") {
|
||||
Label(text, systemImage: image)
|
||||
.contentTransition(.numericText())
|
||||
.foregroundStyle(fgColor)
|
||||
}
|
||||
TextField("", text: $importStr)
|
||||
Button() {
|
||||
withAnimation {
|
||||
showAlert.toggle()
|
||||
}
|
||||
} label: {
|
||||
Label("Import", systemImage: "tray.and.arrow.down.fill")
|
||||
}
|
||||
.disabled(importStr.isEmpty)
|
||||
.onAppear() {
|
||||
importStr = ""
|
||||
image = "clock.fill"
|
||||
text = "Ready..."
|
||||
fgColor = .yellow
|
||||
}
|
||||
}
|
||||
.blur(radius: showAlert ? 2 : 0)
|
||||
Group {
|
||||
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()
|
||||
} label: {
|
||||
Text("cancel")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button() {
|
||||
withAnimation {
|
||||
showAlert.toggle()
|
||||
}
|
||||
importEvents()
|
||||
} label: {
|
||||
Text("yes")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: 250, maxHeight: 250)
|
||||
}
|
||||
.opacity(showAlert ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ImportView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
importStr: .constant("kljadfskljafdlkj;==")
|
||||
)
|
||||
}
|
||||
163
NearFuture/Views/Settings/SettingsView.swift
Normal file
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 29/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var importStr: String = ""
|
||||
|
||||
func changeIcon(to toIcon: String) {
|
||||
guard UIApplication.shared.supportsAlternateIcons else {
|
||||
print("doesnt tsupport alternate icons")
|
||||
return
|
||||
}
|
||||
guard toIcon != "orange" else {
|
||||
UIApplication.shared.setAlternateIconName(nil) { error in
|
||||
print(error as Any)
|
||||
}
|
||||
return
|
||||
}
|
||||
UIApplication.shared.setAlternateIconName(toIcon) { error in
|
||||
print(error as Any)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
backgroundGradient
|
||||
List {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(settingsModel.accentChoices, id: \.self) { choice in
|
||||
let color = Color(uiColor: UIColor(named: "uiColors/\(choice)")!)
|
||||
ZStack {
|
||||
Button() {
|
||||
settingsModel.changeTint(to: choice)
|
||||
changeIcon(to: choice)
|
||||
} label: {
|
||||
Circle()
|
||||
.foregroundStyle(color)
|
||||
.frame(width: 30)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
if ColorCodable(color) == settingsModel.settings.tint {
|
||||
let needContrast: Bool = ColorCodable(color) == settingsModel.settings.tint
|
||||
Circle()
|
||||
.foregroundStyle(needContrast ? .two : .one)
|
||||
.frame(width: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Button("Show What's New") {
|
||||
settingsModel.settings.showWhatsNew = true
|
||||
}
|
||||
Toggle("Show completed Events in Home", isOn: $settingsModel.settings.showCompletedInHome)
|
||||
.onChange(of: settingsModel.settings.showCompletedInHome) { _ in
|
||||
settingsModel.saveSettings()
|
||||
}
|
||||
NavigationLink() {
|
||||
List {
|
||||
if !settingsModel.notifsGranted {
|
||||
Text("\(Image(systemName: "xmark")) Notifications disabled for Near Future")
|
||||
.foregroundStyle(.red)
|
||||
Button("Request Notifications") {
|
||||
Task.detached {
|
||||
let requestNotifsResult = await requestNotifs()
|
||||
await MainActor.run {
|
||||
settingsModel.notifsGranted = requestNotifsResult
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("\(Image(systemName: "checkmark")) Notifications enabled for Near Future")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "bell.badge.fill")
|
||||
Text("Notifications")
|
||||
}
|
||||
NavigationLink() {
|
||||
iCloudSettingsView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "icloud.fill")
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(viewModel.iCloudStatusColor)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
NavigationLink() {
|
||||
ImportView(viewModel: viewModel, importStr: $importStr)
|
||||
} label: {
|
||||
Label("Import Events", systemImage: "tray.and.arrow.down.fill")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
NavigationLink() {
|
||||
ExportView(viewModel: viewModel)
|
||||
} label: {
|
||||
Label("Export Events", systemImage: "square.and.arrow.up")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
|
||||
Section("Tip") {
|
||||
Text("Near Future has Widgets!")
|
||||
}
|
||||
|
||||
Section("Danger Zone") {
|
||||
Button("Delete local data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
}
|
||||
Button("Delete iCloud data", role: .destructive) {
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
Button("Delete all data", role: .destructive) {
|
||||
viewModel.dangerClearLocalData()
|
||||
viewModel.dangerCleariCloudData()
|
||||
}
|
||||
}
|
||||
Section("Debug") {
|
||||
Button("Reset UserDefaults", role: .destructive) {
|
||||
viewModel.dangerResetLocalData()
|
||||
}
|
||||
Button("Reset iCloud", role: .destructive) {
|
||||
viewModel.dangerResetiCloud()
|
||||
}
|
||||
}
|
||||
Section("About") {
|
||||
AboutView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.modifier(navigationInlineLarge())
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
132
NearFuture/Views/Settings/WhatsNewView.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// WhatsNewView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 12/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WhatsNewView: View {
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
struct WhatsNewChunk: Identifiable {
|
||||
var id: UUID = UUID()
|
||||
var symbol: String
|
||||
var title: String
|
||||
var subtitle: String
|
||||
}
|
||||
@State var bye: Bool = false
|
||||
var whatsNewChunks: [WhatsNewChunk] {
|
||||
return [
|
||||
WhatsNewChunk(
|
||||
symbol: "desktopcomputer",
|
||||
title: "Mac Native App",
|
||||
subtitle: "New Mac native app (Intel too!)"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: "iphone.radiowaves.left.and.right",
|
||||
title: "Haptic Feedback",
|
||||
subtitle: "Lovely haptic feedback when completing and adding events, and selecting tabs"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: "app",
|
||||
title: "App Icons",
|
||||
subtitle: "You now get a special app icon that matches the color you choose in settings!"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: "apps.iphone",
|
||||
title: "Widgets Day Count Fix",
|
||||
subtitle: "The day count for widgets are now red for overdue events, just like the app, and are now more readable"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: settingsModel.device.sf,
|
||||
title: "This Screen",
|
||||
subtitle: "This update add a Whats New page that will tell you (suprise!) What's New"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: "bell.badge.fill",
|
||||
title: "Notifications",
|
||||
subtitle: "Events now have notifications, reminding you to complete them!"
|
||||
),
|
||||
WhatsNewChunk(
|
||||
symbol: "list.bullet.indent",
|
||||
title: "Animations!",
|
||||
subtitle: "I added animations for adding, removing and ticking events - animations are definitely the most important change"
|
||||
)
|
||||
]
|
||||
}
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
Text("What's New")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(whatsNewChunks) { new in
|
||||
WhatsNewChunkView(
|
||||
symbol: new.symbol,
|
||||
title: new.title,
|
||||
subtitle: new.subtitle
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
Button() {
|
||||
bye.toggle()
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.frame(height: 40)
|
||||
.bold()
|
||||
// .frame(maxWidth: .infinity)
|
||||
}
|
||||
.foregroundStyle(.orange)
|
||||
.modifier(glassButton())
|
||||
.modifier(hapticHeavy(trigger: bye))
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.presentationDragIndicator(.visible)
|
||||
.onDisappear {
|
||||
settingsModel.settings.prevAppVersion = getVersion()+getBuildID()
|
||||
settingsModel.saveSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Color.accent
|
||||
.ignoresSafeArea(.all)
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
WhatsNewView(settingsModel: dummySettingsViewModel())
|
||||
}
|
||||
}
|
||||
|
||||
struct WhatsNewChunkView: View {
|
||||
@State var symbol: String
|
||||
@State var title: String
|
||||
@State var subtitle: String
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: symbol)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 30)
|
||||
.foregroundStyle(Color.orange)
|
||||
.padding(.trailing, 15)
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.bold()
|
||||
Text(subtitle)
|
||||
.foregroundStyle(.gray)
|
||||
.font(.subheadline)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,18 @@
|
||||
import SwiftUI
|
||||
|
||||
struct iCloudSettingsView: View {
|
||||
@State var viewModel: EventViewModel
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
@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
|
||||
// @Binding var hasUbiquitous: Bool
|
||||
// @Binding var lastSyncWasSuccessful: Bool
|
||||
// @Binding var lastSyncWasNormalAgo: Bool
|
||||
// @Binding var localCountEqualToiCloud: Bool
|
||||
// @Binding var icloudCountEqualToLocal: Bool
|
||||
//
|
||||
// var updateStatus: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -67,7 +55,7 @@ struct iCloudSettingsView: View {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceiCloudWithLocalData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
@@ -76,7 +64,7 @@ struct iCloudSettingsView: View {
|
||||
|
||||
Button() {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
viewModel.updateiCStatus()
|
||||
} label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.resizable()
|
||||
@@ -99,7 +87,7 @@ struct iCloudSettingsView: View {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.replaceLocalWithiCloudData()
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
@@ -107,7 +95,7 @@ struct iCloudSettingsView: View {
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
Image(systemName: device.sf)
|
||||
Image(systemName: settingsModel.device.sf)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 75, height: 75)
|
||||
@@ -117,30 +105,30 @@ struct iCloudSettingsView: View {
|
||||
.monospaced()
|
||||
.bold()
|
||||
}
|
||||
Text(device.label)
|
||||
Text(settingsModel.device.label)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(hasUbiquitous ? .green : .red)
|
||||
.foregroundStyle(viewModel.hasUbiquitous ? .green : .red)
|
||||
Text("iCloud")
|
||||
Spacer()
|
||||
Text("\(hasUbiquitous ? "" : "Not ")Working")
|
||||
Text("\(viewModel.hasUbiquitous ? "" : "Not ")Working")
|
||||
.bold()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasSuccessful ? .green : .red)
|
||||
.foregroundStyle(viewModel.lastSyncWasSuccessful ? .green : .red)
|
||||
Text("Sync Status")
|
||||
Spacer()
|
||||
Text("\(viewModel.syncStatus)")
|
||||
@@ -150,7 +138,7 @@ struct iCloudSettingsView: View {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(lastSyncWasNormalAgo ? .green : .red)
|
||||
.foregroundStyle(viewModel.lastSyncWasNormalAgo ? .green : .red)
|
||||
Text("Last Sync")
|
||||
Spacer()
|
||||
Text("\(viewModel.lastSync?.formatted(date: .long, time: .standard) ?? "Never")")
|
||||
@@ -160,7 +148,7 @@ struct iCloudSettingsView: View {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(localCountEqualToiCloud ? .green : .red)
|
||||
.foregroundStyle(viewModel.localCountEqualToiCloud ? .green : .red)
|
||||
Text("Local Events")
|
||||
Spacer()
|
||||
Text("\(viewModel.localEventCount)")
|
||||
@@ -170,7 +158,7 @@ struct iCloudSettingsView: View {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(icloudCountEqualToLocal ? .green : .red)
|
||||
.foregroundStyle(viewModel.icloudCountEqualToLocal ? .green : .red)
|
||||
Text("Events in iCloud")
|
||||
Spacer()
|
||||
Text("\(viewModel.icloudEventCount)")
|
||||
@@ -184,11 +172,10 @@ struct iCloudSettingsView: View {
|
||||
}
|
||||
.refreshable {
|
||||
viewModel.sync()
|
||||
updateStatus()
|
||||
viewModel.updateiCStatus()
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("iCloud")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,11 +183,6 @@ struct iCloudSettingsView: View {
|
||||
#Preview("iCloudSettingsView") {
|
||||
iCloudSettingsView(
|
||||
viewModel: dummyEventViewModel(),
|
||||
hasUbiquitous: .constant(true),
|
||||
lastSyncWasSuccessful: .constant(true),
|
||||
lastSyncWasNormalAgo: .constant(true),
|
||||
localCountEqualToiCloud: .constant(true),
|
||||
icloudCountEqualToLocal: .constant(true),
|
||||
updateStatus: {}
|
||||
settingsModel: dummySettingsViewModel()
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// StatsView.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by Nihaal Sharma on 05/01/2025.
|
||||
// Created by neon443 on 05/01/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@@ -58,13 +58,7 @@ struct StatsView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("Statistics")
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.toolbarTitleDisplayMode(.inlineLarge)
|
||||
} else {
|
||||
$0.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
.modifier(navigationInlineLarge())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// NearFutureTests.swift
|
||||
// NearFutureTests
|
||||
//
|
||||
// Created by neon443 on 26/03/2025.
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
struct NearFutureTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
// NearFutureWidgets.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
// Created by neon443 on 02/01/2025.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
// Timeline Entry for Widget
|
||||
struct EventWidgetEntry: TimelineEntry {
|
||||
@@ -46,6 +47,24 @@ struct EventWidgetProvider: TimelineProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Event Widget View
|
||||
struct EventWidgetView: View {
|
||||
var entry: EventWidgetEntry
|
||||
@@ -128,17 +147,36 @@ struct EventWidgetView: View {
|
||||
Spacer()
|
||||
|
||||
//short days till if not large widget
|
||||
if isLarge {
|
||||
Text(daysUntilEvent(event.date).long)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.color.color)
|
||||
.padding(.trailing, -12)
|
||||
} else {
|
||||
if !isLarge {
|
||||
Text(daysUntilEvent(event.date).short)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.color.color)
|
||||
.foregroundColor(event.date < Date() ? .red : .primary)
|
||||
.padding(.trailing, -12)
|
||||
} else {
|
||||
Button(
|
||||
intent: CompleteEvent(
|
||||
eventID: IntentParameter(
|
||||
title: LocalizedStringResource(
|
||||
stringLiteral: event.id.uuidString
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
if event.complete {
|
||||
Circle()
|
||||
.frame(width: 10)
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Circle()
|
||||
.frame(width: 10)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
Text(daysUntilEvent(event.date).long)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundColor(event.date < Date() ? .red : .primary)
|
||||
.padding(.trailing, -12)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// NearFutureWidgetsBundle.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
// Created by neon443 on 02/01/2025.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
|
||||
@@ -2,79 +2,79 @@
|
||||
// NearFutureWidgetsLiveActivity.swift
|
||||
// NearFutureWidgets
|
||||
//
|
||||
// Created by Nihaal Sharma on 02/01/2025.
|
||||
// Created by neon443 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
|
||||
//}
|
||||
|
||||
33
README.md
@@ -1,6 +1,24 @@
|
||||
# NearFuture
|
||||
|
||||
[App Store](https://apps.apple.com/us/app/near-future-event-tracker/id6744963429)
|
||||
<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>
|
||||
|
||||
**Near Future** is a SwiftUI App to help people track upcoming events - Holidays, Trips, Birthdays, Weddings, Anniversaries.
|
||||
|
||||
@@ -11,17 +29,18 @@
|
||||
- [x] Event colors
|
||||
- [x] Recurrence
|
||||
- [x] Search
|
||||
- [ ] Notifications
|
||||
- [x] Notifications
|
||||
- [ ] Mac App
|
||||
- [ ] Apple Watch App
|
||||
- [x] Home Screen Widgets
|
||||
- [ ] Lock Screen Widgets
|
||||
- [ ] Later Box
|
||||
- [ ] Sort by
|
||||
- [ ] Reorder Events
|
||||
- [ ] Archive
|
||||
- [x] Archive
|
||||
- [ ] Collaboration
|
||||
- [ ] Autocomplete tasks
|
||||
- [ ] Settings
|
||||
- [x] Settings
|
||||
|
||||
## Features
|
||||
- **Event Creation**: Create events with a name, description, date, recurrence, and an icon
|
||||
@@ -53,4 +72,4 @@ Contributions are welcome! Just follow these steps:
|
||||
|
||||
## Used Tools/Frameworks
|
||||
- Swift & SwiftUI by Apple
|
||||
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].
|
||||
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].
|
||||
|
||||
@@ -31,54 +31,70 @@
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "MacNearFutureIcon.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "NearFutureIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
After Width: | Height: | Size: 893 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 729 KiB After Width: | Height: | Size: 729 KiB |
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 632 KiB |
44
Resources/Assets.xcassets/bloo.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bloo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "blooDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "bloo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Resources/Assets.xcassets/bloo.appiconset/NearFutureIconTint.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/bloo.appiconset/bloo.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Resources/Assets.xcassets/bloo.appiconset/blooDark.png
Normal file
|
After Width: | Height: | Size: 712 KiB |
44
Resources/Assets.xcassets/blue.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "blue.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "blueDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "blue.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Resources/Assets.xcassets/blue.appiconset/NearFutureIconTint.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/blue.appiconset/blue.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Resources/Assets.xcassets/blue.appiconset/blueDark.png
Normal file
|
After Width: | Height: | Size: 743 KiB |
44
Resources/Assets.xcassets/green.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "green.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "greenDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "green.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/green.appiconset/green.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Resources/Assets.xcassets/green.appiconset/greenDark.png
Normal file
|
After Width: | Height: | Size: 734 KiB |
44
Resources/Assets.xcassets/pink.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "pink.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "pinkDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "pink.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Resources/Assets.xcassets/pink.appiconset/NearFutureIconTint.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/pink.appiconset/pink.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Resources/Assets.xcassets/pink.appiconset/pinkDark.png
Normal file
|
After Width: | Height: | Size: 728 KiB |
44
Resources/Assets.xcassets/purple.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "purple.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "purpleDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "purple.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/purple.appiconset/purple.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Resources/Assets.xcassets/purple.appiconset/purpleDark.png
Normal file
|
After Width: | Height: | Size: 746 KiB |
44
Resources/Assets.xcassets/red.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "red.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "redDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "red.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Resources/Assets.xcassets/red.appiconset/NearFutureIconTint.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/red.appiconset/red.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Resources/Assets.xcassets/red.appiconset/redDark.png
Normal file
|
After Width: | Height: | Size: 751 KiB |
44
Resources/Assets.xcassets/yellow.appiconset/Contents.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "yellow.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "yellowDark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "NearFutureIconTint.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "yellow.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 632 KiB |
BIN
Resources/Assets.xcassets/yellow.appiconset/yellow.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Resources/Assets.xcassets/yellow.appiconset/yellowDark.png
Normal file
|
After Width: | Height: | Size: 743 KiB |