23 Commits

Author SHA1 Message Date
neon443
e4107a2faa add success haptic and indentation 2025-06-15 21:24:15 +01:00
neon443
5ec16dd67a fix editing events on mac and ios
toolbar label()s instead of buton("label")s
fix event add/delete/tick animation
fix event complete progress on ios and mac and that it just spazzed out
browsing true for symbols browsing (not picking)
2025-06-15 21:18:58 +01:00
neon443
0cffe243eb completeevent button has a custo progress bar to work on ios aswell
extracted the com[lete button
2025-06-15 18:55:38 +01:00
neon443
6533fb85ed fix editing events 2025-06-15 15:41:37 +01:00
neon443
5c667679d5 fix "new edit event window" on mac not working
- it would save events with the same id and modify the previous one if u opened another one
add symbols to ios
date picker looks better mac
2025-06-15 15:34:08 +01:00
neon443
5dd25f1ede locked the freak in for this one -
can cancel events being completed!!!!!
added a cancel to symbolpicker
and made it dip after choosing
extracted completevent into viewmodel
eventlistview is pretty full rn -- need to cleana
added color picker ot addeventview wtf why wasnt it there
2025-06-14 21:32:11 +01:00
neon443
d80011ea27 remove my shitty uikit 2025-06-14 18:55:09 +01:00
neon443
a40d4f4300 ui cleanup 2025-06-14 18:53:34 +01:00
neon443
22af9fc060 hell nah fuck this 2025-06-14 18:27:13 +01:00
neon443
2dd2c51059 uikit shenanigans: collectionview for the symbol picker?? 2025-06-14 17:45:38 +01:00
neon443
4c9e72fad2 viewmodifier for backwards compat 2025-06-14 11:20:29 +01:00
neon443
cb26c69492 added a little help fow when nothing matches search 2025-06-14 11:17:41 +01:00
neon443
13ef94ea3e custom symbols picker 2025-06-14 10:59:48 +01:00
neon443
6e5b7adbc4 fix editing events on mac - they now reload on he fly
add search on mac
fix symbolspicker on mac
2025-06-14 10:24:03 +01:00
neon443
659f14d5a3 add events on mac 2025-06-13 22:00:28 +01:00
neon443
0663ba9e59 add settings on mac
add whats new on mac
clean up settings on ios
clean up whats new + liquid glass
2025-06-13 21:28:37 +01:00
neon443
6e7bc6c2d1 add viewmodifiers.siwft to conditionally do viewmodifiers
replaced all the .apply s everywhere
colorcodable into its own file
add icon changing for mac
2025-06-13 18:30:07 +01:00
neon443
3f21074091 moved icloud ui functions into viewModel
- idk why it was in the view
added settings for mac
fix addeventview
export view pasteboard for mac
move accent icon to new file
Archive view reversed - new to old
2025-06-13 17:15:23 +01:00
neon443
ee2e05c523 fix the cooked addeventview 2025-06-11 18:49:09 +01:00
neon443
e9e19e2659 Large rework:
Liquid Glass:
- remove ultrathinmaterial from all sheets
- button color change

remove the eventName eventDate etcs and replaced with an event var

Add addeventview for mac, cleanup #if s

clean up homeview
add .searchable
addeventview is horizontal i have no idea why
2025-06-11 18:34:17 +01:00
neon443
5ce48a4bc2 add new buttons 2025-06-10 15:09:10 +01:00
neon443
a7e09692f6 liquid glass update 1 2025-06-10 13:14:19 +01:00
neon443
559f891719 new liquid glass icon 2025-06-10 12:28:03 +01:00
37 changed files with 1237 additions and 918 deletions

View File

@@ -12,6 +12,6 @@ TEAM_ID = 8JGND254B7
BUNDLE_ID = com.neon443.NearFuture
BUNDLE_ID_WIDGETS = com.neon443.NearFuture.widgets
GROUP_ID = group.NearFuture
VERSION = 4.4.0
VERSION = 5
NAME = Near Future
BUILD_NUMBER = 1

View File

@@ -24,31 +24,35 @@ struct NearFutureApp: App {
.defaultSize(width: 550, height: 650)
.commands {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("about nf") {
Button("About Near Future") {
openWindow(id: "about")
}
}
NearFutureCommands()
}
WindowGroup("edit Event", for: Event.ID.self) { $eventID in
EditEventView(
viewModel: viewModel,
event: Binding(
get: {
viewModel.events.first(where: {$0.id == eventID})!
},
set: { newValue in
if let eventIndex = viewModel.events.firstIndex(where: {
$0.id == eventID
}) {
viewModel.events[eventIndex] = newValue
}
viewModel.saveEvents()
}
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()
@@ -59,7 +63,10 @@ struct NearFutureApp: App {
.defaultPosition(UnitPoint.center)
Settings {
Text("wip")
SettingsView(
viewModel: viewModel,
settingsModel: settingsModel
)
}
}
}

View File

@@ -11,7 +11,7 @@ import SwiftUI
struct NearFutureCommands: Commands {
var body: some Commands {
CommandGroup(after: CommandGroupPlacement.appInfo) {
Text("hi")
// Text("hi")
}
}
}

View File

@@ -11,8 +11,11 @@ struct ContentView: View {
@StateObject var viewModel: EventViewModel
@StateObject var settingsModel: SettingsViewModel
var body: some View {
NavigationSplitView(preferredCompactColumn: .constant(.sidebar)) {
@State private var showAddEventView: Bool = false
@State private var symbolSearchInput: String = ""
var body: some View {
NavigationSplitView {
List {
NavigationLink {
HomeView(
@@ -32,14 +35,49 @@ struct ContentView: View {
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(.ultraThinMaterial, for: .window)
}
.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 {

View File

@@ -1,185 +0,0 @@
//
// EventListView.swift
// MacNearFuture
//
// Created by neon443 on 21/05/2025.
//
import SwiftUI
struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
@State var largeTick: Bool = false
@State var hovering: Bool = false
@Environment(\.openWindow) var openWindow
var body: some View {
ZStack {
Color.black.opacity(hovering ? 0.5 : 0.0)
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 7)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.shadow(radius: 5)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
Text("\(event.name)")
.bold()
.foregroundStyle(.one)
.strikethrough(event.complete)
.multilineTextAlignment(.leading)
}
if !event.notes.isEmpty {
Text(event.notes)
.foregroundStyle(.one.opacity(0.8))
.multilineTextAlignment(.leading)
}
Text(
event.date.formatted(
date: .long,
time: .shortened
)
)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
if event.recurrence != .none {
Text("Occurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.one.opacity(event.complete ? 0.5 : 1))
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date).long)")
.multilineTextAlignment(.trailing)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
}
Button() {
withAnimation {
event.complete.toggle()
}
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events[eventToModify] = event
viewModel.saveEvents()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.bold()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.onHover() { hovering in
withAnimation {
largeTick.toggle()
}
}
.buttonStyle(.borderless)
.scaleEffect(largeTick ? 1.5 : 1)
.frame(maxWidth: 20)
.shadow(radius: 5)
.padding(.trailing, 15)
.animation(
.spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2),
value: largeTick
)
.apply {
if #available(iOS 17, *) {
$0.sensoryFeedback(.success, trigger: event.complete)
}
}
}
.transition(.opacity)
.fixedSize(horizontal: false, vertical: true)
}
.onHover { isHovering in
withAnimation {
hovering.toggle()
}
}
.onTapGesture {
openWindow(value: event.id)
}
.contextMenu() {
Button(role: .destructive) {
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events.remove(at: eventToModify)
viewModel.saveEvents()
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
#Preview("EventListView") {
let vm = dummyEventViewModel()
ZStack {
Color.black
VStack {
ForEach(0..<50) { _ in
Rectangle()
.foregroundStyle(randomColor().opacity(0.5))
.padding(-10)
}
.ignoresSafeArea(.all)
.blur(radius: 5)
}
VStack {
ForEach(vm.events) { event in
EventListView(
viewModel: vm,
event: event
)
}
}
.padding(.horizontal, 10)
}
}
#Preview {
EventListView(
viewModel: dummyEventViewModel(),
event: dummyEventViewModel().template
)
}

View File

@@ -11,20 +11,33 @@ struct HomeView: View {
@StateObject var viewModel: EventViewModel
@StateObject var settingsModel: SettingsViewModel
@State private var searchInput: String = ""
var filteredEvents: [Event] {
switch settingsModel.settings.showCompletedInHome {
case true:
return viewModel.events
case false:
return viewModel.events.filter { !$0.complete }
}
}
var body: some View {
ScrollView {
ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: 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)
}
}

View File

@@ -1,48 +0,0 @@
//
// EditEventView.swift
// NearFuture
//
// Created by neon443 on 21/05/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,
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: .primaryAction) {
Button() {
dismiss()
} label: {
Text("Done")
.bold()
}
.disabled(event.name == "")
}
}
}
}
#Preview {
EditEventView(
viewModel: dummyEventViewModel(),
event: .constant(dummyEventViewModel().template)
)
}

View 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()
)
}

View File

@@ -14,10 +14,8 @@
A90D49442DDE1C7600781124 /* Tints.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A90D49432DDE1C1100781124 /* Tints.xcassets */; };
A90D49452DDE1C7600781124 /* Tints.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A90D49432DDE1C1100781124 /* Tints.xcassets */; };
A90D49462DDE1C7A00781124 /* Tints.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A90D49432DDE1C1100781124 /* Tints.xcassets */; };
A90D494B2DDE2C2900781124 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83C2DCAABE00064DCA0 /* AddEventView.swift */; };
A90D49522DDE2D0000781124 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D49512DDE2D0000781124 /* Extensions.swift */; };
A90D49532DDE2D0000781124 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D49512DDE2D0000781124 /* Extensions.swift */; };
A90D49562DDE2D5800781124 /* SFSymbolsPicker in Frameworks */ = {isa = PBXBuildFile; productRef = A90D49552DDE2D5800781124 /* SFSymbolsPicker */; };
A90D495B2DDE2EDB00781124 /* MacNearFutureApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D495A2DDE2EDB00781124 /* MacNearFutureApp.swift */; };
A90D495E2DDE3C7400781124 /* NFCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D495D2DDE3C7400781124 /* NFCommands.swift */; };
A90D495F2DDE3C7400781124 /* NFCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D495D2DDE3C7400781124 /* NFCommands.swift */; };
@@ -27,11 +25,26 @@
A914FA4B2DD26C6800856265 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A914FA4A2DD26C0F00856265 /* HomeView.swift */; };
A914FA4D2DD2768900856265 /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A914FA4C2DD2768900856265 /* WhatsNewView.swift */; };
A914FA4F2DD276D200856265 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A914FA4E2DD276D200856265 /* AboutView.swift */; };
A91EF8072DFC8B8B00B8463D /* ColorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */; };
A91EF8082DFC8B8B00B8463D /* ColorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */; };
A91EF8092DFC8B8B00B8463D /* ColorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */; };
A91EF80B2DFC910000B8463D /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF80A2DFC910000B8463D /* ViewModifiers.swift */; };
A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF80A2DFC910000B8463D /* ViewModifiers.swift */; };
A91EF80D2DFC910000B8463D /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF80A2DFC910000B8463D /* ViewModifiers.swift */; };
A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A914FA4C2DD2768900856265 /* WhatsNewView.swift */; };
A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF80F2DFCB66C00B8463D /* SettingsView.swift */; };
A91EF8132DFCC87D00B8463D /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83D2DCAABE00064DCA0 /* EditEventView.swift */; };
A91EF8142DFCC87D00B8463D /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83C2DCAABE00064DCA0 /* AddEventView.swift */; };
A91EF8182DFD77BF00B8463D /* SymbolsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */; };
A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */; };
A91EF81A2DFD77BF00B8463D /* SymbolsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */; };
A91EF81C2DFD796600B8463D /* SymbolsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */; };
A91EF81D2DFD796600B8463D /* SymbolsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */; };
A91EF81E2DFD796600B8463D /* SymbolsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */; };
A920C2882D24011400E4F9B1 /* NearFutureApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2872D24011400E4F9B1 /* NearFutureApp.swift */; };
A920C28C2D24011400E4F9B1 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Events.swift */; };
A920C28E2D24011A00E4F9B1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A920C28D2D24011A00E4F9B1 /* Assets.xcassets */; };
A920C2922D24011A00E4F9B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A920C2912D24011A00E4F9B1 /* Preview Assets.xcassets */; };
A920C2BE2D24021A00E4F9B1 /* SFSymbolsPicker in Frameworks */ = {isa = PBXBuildFile; productRef = A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */; };
A949F8322DCAAA8A0064DCA0 /* NearFutureIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = A949F8312DCAAA8A0064DCA0 /* NearFutureIcon.png */; };
A949F84B2DCAABE00064DCA0 /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83A2DCAABE00064DCA0 /* ArchiveView.swift */; };
A949F84C2DCAABE00064DCA0 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F83C2DCAABE00064DCA0 /* AddEventView.swift */; };
@@ -45,16 +58,24 @@
A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8462DCAABE00064DCA0 /* SettingsView.swift */; };
A949F8552DCAABE00064DCA0 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8482DCAABE00064DCA0 /* StatsView.swift */; };
A949F85F2DCABB420064DCA0 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F85D2DCABB420064DCA0 /* Buttons.swift */; };
A95E9ED32DFC703200ED655F /* iCloudSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8442DCAABE00064DCA0 /* iCloudSettingsView.swift */; };
A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95E9ED72DFC742B00ED655F /* AccentIcon.swift */; };
A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95E9ED72DFC742B00ED655F /* AccentIcon.swift */; };
A95E9EDA2DFC742B00ED655F /* AccentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95E9ED72DFC742B00ED655F /* AccentIcon.swift */; };
A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8452DCAABE00064DCA0 /* ImportView.swift */; };
A95E9EE52DFC77E200ED655F /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8432DCAABE00064DCA0 /* ExportView.swift */; };
A96609E72DFD800000DBFA78 /* HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8412DCAABE00064DCA0 /* HelpView.swift */; };
A979F60A2D270AF00094C0B3 /* NearFutureWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F6092D270AF00094C0B3 /* NearFutureWidgetsBundle.swift */; };
A979F60C2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F60B2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift */; };
A979F6102D270AF90094C0B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A979F60F2D270AF80094C0B3 /* Assets.xcassets */; };
A979F6142D270AF90094C0B3 /* NearFutureWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A979F6022D270AF00094C0B3 /* NearFutureWidgetsExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
A979F6182D2714310094C0B3 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Events.swift */; };
A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CA2DE730740008D61C /* EventListViewMac.swift */; };
A98C20CC2DE730740008D61C /* EditEventViewMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20C92DE730740008D61C /* EditEventViewMac.swift */; };
A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CD2DE7308E0008D61C /* ArchiveView.swift */; };
A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CF2DE731BD0008D61C /* HomeView.swift */; };
A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20D32DE7339E0008D61C /* AboutView.swift */; };
A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; };
A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; };
A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8402DCAABE00064DCA0 /* EventListView.swift */; };
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; };
/* End PBXBuildFile section */
@@ -104,6 +125,11 @@
A914FA4A2DD26C0F00856265 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
A914FA4C2DD2768900856265 /* WhatsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WhatsNewView.swift; path = NearFuture/Views/Settings/WhatsNewView.swift; sourceTree = SOURCE_ROOT; };
A914FA4E2DD276D200856265 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AboutView.swift; path = NearFuture/Views/Misc/AboutView.swift; sourceTree = SOURCE_ROOT; };
A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorCodable.swift; sourceTree = "<group>"; };
A91EF80A2DFC910000B8463D /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
A91EF80F2DFCB66C00B8463D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolsLoader.swift; sourceTree = "<group>"; };
A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolsPicker.swift; sourceTree = "<group>"; };
A920C2842D24011400E4F9B1 /* NearFuture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NearFuture.app; sourceTree = BUILT_PRODUCTS_DIR; };
A920C2872D24011400E4F9B1 /* NearFutureApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureApp.swift; sourceTree = "<group>"; };
A920C28B2D24011400E4F9B1 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
@@ -125,15 +151,15 @@
A949F8462DCAABE00064DCA0 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
A949F8482DCAABE00064DCA0 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
A949F85D2DCABB420064DCA0 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
A95E9ED72DFC742B00ED655F /* AccentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentIcon.swift; sourceTree = "<group>"; };
A979F6022D270AF00094C0B3 /* NearFutureWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NearFutureWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
A979F6092D270AF00094C0B3 /* NearFutureWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsBundle.swift; sourceTree = "<group>"; };
A979F60B2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsLiveActivity.swift; sourceTree = "<group>"; };
A979F60F2D270AF80094C0B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A98C20C92DE730740008D61C /* EditEventViewMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventViewMac.swift; sourceTree = "<group>"; };
A98C20CA2DE730740008D61C /* EventListViewMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListViewMac.swift; sourceTree = "<group>"; };
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; };
A98C20CF2DE731BD0008D61C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
A98C20D32DE7339E0008D61C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteEventButton.swift; sourceTree = "<group>"; };
A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -142,7 +168,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A90D49562DDE2D5800781124 /* SFSymbolsPicker in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -150,7 +175,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A920C2BE2D24021A00E4F9B1 /* SFSymbolsPicker in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -169,6 +193,9 @@
children = (
A920C28B2D24011400E4F9B1 /* Events.swift */,
A90D49602DDE626300781124 /* Settings.swift */,
A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */,
A95E9ED72DFC742B00ED655F /* AccentIcon.swift */,
A91EF8162DFD77A500B8463D /* SymbolsPicker */,
);
path = Model;
sourceTree = "<group>";
@@ -190,9 +217,8 @@
children = (
A90D49332DDE0FAF00781124 /* ContentViewMac.swift */,
A98C20CF2DE731BD0008D61C /* HomeView.swift */,
A98C20CA2DE730740008D61C /* EventListViewMac.swift */,
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */,
A98C20C82DE730420008D61C /* Modification */,
A91EF80F2DFCB66C00B8463D /* SettingsView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -203,6 +229,8 @@
A920C2872D24011400E4F9B1 /* NearFutureApp.swift */,
A90D49512DDE2D0000781124 /* Extensions.swift */,
A90D49202DDE0A3B00781124 /* Model */,
A91EF80A2DFC910000B8463D /* ViewModifiers.swift */,
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */,
);
path = Shared;
sourceTree = "<group>";
@@ -214,6 +242,15 @@
name = Frameworks;
sourceTree = "<group>";
};
A91EF8162DFD77A500B8463D /* SymbolsPicker */ = {
isa = PBXGroup;
children = (
A91EF8172DFD77BF00B8463D /* SymbolsLoader.swift */,
A91EF81B2DFD796600B8463D /* SymbolsPicker.swift */,
);
path = SymbolsPicker;
sourceTree = "<group>";
};
A920C27B2D24011300E4F9B1 = {
isa = PBXGroup;
children = (
@@ -351,14 +388,6 @@
path = NearFutureWidgets;
sourceTree = "<group>";
};
A98C20C82DE730420008D61C /* Modification */ = {
isa = PBXGroup;
children = (
A98C20C92DE730740008D61C /* EditEventViewMac.swift */,
);
path = Modification;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -374,11 +403,9 @@
);
dependencies = (
A98C20D22DE732B10008D61C /* PBXTargetDependency */,
A90D494D2DDE2C6000781124 /* PBXTargetDependency */,
);
name = MacNearFuture;
packageProductDependencies = (
A90D49552DDE2D5800781124 /* SFSymbolsPicker */,
);
productName = MacNearFuture;
productReference = A90D49262DDE0FA400781124 /* Near Future.app */;
@@ -400,7 +427,6 @@
);
name = NearFuture;
packageProductDependencies = (
A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */,
);
productName = NearFuture;
productReference = A920C2842D24011400E4F9B1 /* NearFuture.app */;
@@ -454,7 +480,6 @@
);
mainGroup = A920C27B2D24011300E4F9B1;
packageReferences = (
A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */,
);
productRefGroup = A920C2852D24011400E4F9B1 /* Products */;
projectDirPath = "";
@@ -505,18 +530,30 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */,
A98C20CC2DE730740008D61C /* EditEventViewMac.swift in Sources */,
A90D494B2DDE2C2900781124 /* AddEventView.swift in Sources */,
A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */,
A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */,
A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */,
A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */,
A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */,
A90D495E2DDE3C7400781124 /* NFCommands.swift in Sources */,
A95E9EE52DFC77E200ED655F /* ExportView.swift in Sources */,
A91EF8132DFCC87D00B8463D /* EditEventView.swift in Sources */,
A91EF8142DFCC87D00B8463D /* AddEventView.swift in Sources */,
A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */,
A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */,
A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */,
A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */,
A90D495B2DDE2EDB00781124 /* MacNearFutureApp.swift in Sources */,
A91EF8082DFC8B8B00B8463D /* ColorCodable.swift in Sources */,
A90D49522DDE2D0000781124 /* Extensions.swift in Sources */,
A91EF81D2DFD796600B8463D /* SymbolsPicker.swift in Sources */,
A90D49422DDE114100781124 /* Events.swift in Sources */,
A90D49382DDE0FAF00781124 /* ContentViewMac.swift in Sources */,
A90D49622DDE626300781124 /* Settings.swift in Sources */,
A95E9ED32DFC703200ED655F /* iCloudSettingsView.swift in Sources */,
A96609E72DFD800000DBFA78 /* HelpView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -534,15 +571,21 @@
A90D49612DDE626300781124 /* Settings.swift in Sources */,
A90D495F2DDE3C7400781124 /* NFCommands.swift in Sources */,
A949F84F2DCAABE00064DCA0 /* EventListView.swift in Sources */,
A91EF8092DFC8B8B00B8463D /* ColorCodable.swift in Sources */,
A949F8502DCAABE00064DCA0 /* HelpView.swift in Sources */,
A949F85F2DCABB420064DCA0 /* Buttons.swift in Sources */,
A914FA4D2DD2768900856265 /* WhatsNewView.swift in Sources */,
A91EF80B2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A949F8512DCAABE00064DCA0 /* ExportView.swift in Sources */,
A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */,
A90D49532DDE2D0000781124 /* Extensions.swift in Sources */,
A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */,
A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */,
A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */,
A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */,
A91EF81E2DFD796600B8463D /* SymbolsPicker.swift in Sources */,
A949F8552DCAABE00064DCA0 /* StatsView.swift in Sources */,
A91EF81A2DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A920C2882D24011400E4F9B1 /* NearFutureApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -552,7 +595,12 @@
buildActionMask = 2147483647;
files = (
A979F6182D2714310094C0B3 /* Events.swift in Sources */,
A91EF81C2DFD796600B8463D /* SymbolsPicker.swift in Sources */,
A979F60A2D270AF00094C0B3 /* NearFutureWidgetsBundle.swift in Sources */,
A95E9EDA2DFC742B00ED655F /* AccentIcon.swift in Sources */,
A91EF80D2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A91EF8182DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A91EF8072DFC8B8B00B8463D /* ColorCodable.swift in Sources */,
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */,
A979F60C2D270AF00094C0B3 /* NearFutureWidgetsLiveActivity.swift in Sources */,
A90D49632DDE626300781124 /* Settings.swift in Sources */,
@@ -562,10 +610,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
A90D494D2DDE2C6000781124 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = A90D494C2DDE2C6000781124 /* SFSymbolsPicker */;
};
A979F6132D270AF90094C0B3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
@@ -994,35 +1038,6 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/alessiorubicini/SFSymbolsPicker";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.6;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A90D494C2DDE2C6000781124 /* SFSymbolsPicker */ = {
isa = XCSwiftPackageProductDependency;
package = A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */;
productName = SFSymbolsPicker;
};
A90D49552DDE2D5800781124 /* SFSymbolsPicker */ = {
isa = XCSwiftPackageProductDependency;
package = A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */;
productName = SFSymbolsPicker;
};
A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */ = {
isa = XCSwiftPackageProductDependency;
package = A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */;
productName = SFSymbolsPicker;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A920C27C2D24011300E4F9B1 /* Project object */;
}

View File

@@ -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
}

View File

@@ -11,7 +11,8 @@ struct ArchiveView: View {
@ObservedObject var viewModel: EventViewModel
@State var showAddEvent: Bool = false
var filteredEvents: [Event] {
return viewModel.events.filter() {$0.complete}
let filteredEvents = viewModel.events.filter({$0.complete})
return filteredEvents.reversed()
}
var body: some View {
NavigationStack {
@@ -22,42 +23,40 @@ struct ArchiveView: View {
} else {
ScrollView {
ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: event)
.transition(.moveAndFadeReversed)
.id(event.complete)
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: Binding(
get: { event },
set: { newValue in
viewModel.editEvent(newValue)
}
)
)
} label: {
EventListView(viewModel: viewModel, event: event)
.id(event.complete)
}
.transition(.moveAndFadeReversed)
}
.padding(.horizontal)
}
.animation(.default, value: filteredEvents)
}
}
.transition(.opacity)
.scrollContentBackground(.hidden)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
AddEventButton(showingAddEventView: $showAddEvent)
}
}
.navigationTitle("Archive")
.apply {
if #available(iOS 17, *) {
$0.toolbarTitleDisplayMode(.inlineLarge)
} else {
$0.navigationBarTitleDisplayMode(.inline)
}
}
.modifier(navigationInlineLarge())
}
.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
viewModel: viewModel
)
}
}

View File

@@ -9,12 +9,10 @@ import SwiftUI
import UserNotifications
import SwiftData
enum Field {
case Search
}
enum Tab {
case home
case archive
case symbols
case stats
case settings
}
@@ -22,10 +20,10 @@ enum Tab {
struct ContentView: View {
@StateObject var viewModel: EventViewModel
@StateObject var settingsModel: SettingsViewModel
@State var selection: Tab = .home
@State var tabSelection: Tab = .home
var body: some View {
TabView(selection: $selection) {
TabView(selection: $tabSelection) {
HomeView(viewModel: viewModel, settingsModel: settingsModel)
.tabItem {
Label("Home", systemImage: "house")
@@ -36,7 +34,16 @@ struct ContentView: View {
Label("Archive", systemImage: "tray.full")
}
.tag(Tab.archive)
SymbolsPicker(
selection: .constant(""),
browsing: true
)
.tabItem {
Label("Symbols", systemImage: "star.circle")
}
.tag(Tab.symbols)
StatsView(viewModel: viewModel)
// SymbolsPickerStoryboardUIViewRepresentable()
.tabItem {
Label("Statistics", systemImage: "chart.pie")
}
@@ -47,11 +54,7 @@ struct ContentView: View {
}
.tag(Tab.settings)
}
.apply {
if #available(iOS 17, *) {
$0.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: selection)
}
}
.modifier(hapticHeavy(trigger: tabSelection))
.sheet(isPresented: $settingsModel.settings.showWhatsNew) {
WhatsNewView(settingsModel: settingsModel)
}

View File

@@ -6,21 +6,13 @@
//
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 event: Event = dummyEventViewModel().template
@State var adding: Bool
@State var adding: Bool = true
@State var showNeedsNameAlert: Bool = false
@State var isSymbolPickerPresented: Bool = false
@@ -33,13 +25,21 @@ struct AddEventView: View {
@Environment(\.dismiss) var dismiss
var isMac: Bool {
if #available(iOS 1, *) {
return false
} else {
return true
}
}
var body: some View {
ZStack {
if !adding {
backgroundGradient
}
NavigationStack {
Form {
List {
Section(
header:
Text("Event Details")
@@ -51,66 +51,52 @@ struct AddEventView: View {
Button() {
isSymbolPickerPresented.toggle()
} label: {
Image(systemName: eventSymbol)
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(eventColor)
.foregroundStyle(event.color.color)
}
.frame(width: 20)
.buttonStyle(.borderless)
.sheet(isPresented: $isSymbolPickerPresented) {
SymbolsPicker(
selection: $eventSymbol,
title: "Choose a Symbol",
searchLabel: "Search...",
autoDismiss: true)
selection: $event.symbol
)
.presentationDetents([.medium])
.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)
.modifier(presentationSizeForm())
}
TextField("Event Name", text: $event.name)
.textFieldStyle(.roundedBorder)
}
// dscription
ZStack {
TextField("Event Notes", text: $eventNotes)
TextField("Event Notes", text: $event.notes)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.trailing, eventNotes.isEmpty ? 0 : 30)
.animation(.spring, value: eventNotes)
.padding(.trailing, event.notes.isEmpty ? 0 : 30)
.animation(.spring, value: event.notes)
.focused($focusedField, equals: Field.Notes)
.submitLabel(.done)
.onSubmit {
focusedField = nil
}
// MagicClearButton(text: $eventNotes)
}
ColorPicker("Event Color", selection: $event.color.colorBind)
// date picker
HStack {
Spacer()
DatePicker("", selection: $eventDate, displayedComponents: .date)
// .datePickerStyle(datepickersty)
DatePicker("", selection: $event.date, displayedComponents: .date)
#if os(iOS)
.datePickerStyle(.wheel)
#else
.datePickerStyle(.graphical)
#endif
Spacer()
Button() {
eventDate = Date()
event.date = Date()
} label: {
Image(systemName: "arrow.uturn.left")
.resizable()
@@ -122,12 +108,15 @@ struct AddEventView: View {
DatePicker(
"",
selection: $eventDate,
selection: $event.date,
displayedComponents: .hourAndMinute
)
#if os(macOS)
.datePickerStyle(.stepperField)
#endif
// re-ocurrence Picker
Picker("Recurrence", selection: $eventRecurrence) {
Picker("Recurrence", selection: $event.recurrence) {
ForEach(Event.RecurrenceType.allCases, id: \.self) { recurrence in
Text(recurrence.rawValue.capitalized)
}
@@ -135,61 +124,44 @@ struct AddEventView: View {
.pickerStyle(SegmentedPickerStyle())
Text(
describeOccurrence(
date: eventDate,
recurrence: eventRecurrence
date: event.date,
recurrence: event.recurrence
)
)
}
}
.scrollContentBackground(.hidden)
.navigationTitle("\(adding ? "Add Event" : "")")
// .navigationBarTitleDisplayMode(.inline)
.modifier(navigationInlineLarge())
.toolbar {
ToolbarItem(/*placement: .topBarLeading*/) {
ToolbarItem(placement: .cancellationAction) {
if adding {
Button() {
resetAddEventView()
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.symbolRenderingMode(.hierarchical)
Image(systemName: "xmark")
.resizable()
.scaledToFit()
.frame(width: 30)
.tint(.one)
}
}
}
ToolbarItem/*(placement: .topBarTrailing)*/ {
ToolbarItem() {
if adding {
Button {
viewModel.addEvent(
newEvent: Event(
name: eventName,
complete: eventComplete,
completeDesc: eventCompleteDesc,
symbol: eventSymbol,
color: ColorCodable(eventColor),
notes: eventNotes,
date: eventDate,
recurrence: eventRecurrence
)
newEvent: event
)
bye.toggle()
resetAddEventView()
} label: {
Text("Save")
.font(.headline)
.cornerRadius(10)
.buttonStyle(BorderedProminentButtonStyle())
Label("Save", systemImage: "checkmark")
}
.apply {
if #available(iOS 17, *) {
$0.sensoryFeedback(.success, trigger: bye)
}
}
.disabled(eventName.isEmpty)
.tint(.accent)
.modifier(hapticSuccess(trigger: bye))
.disabled(event.name.isEmpty)
.onTapGesture {
if eventName.isEmpty {
if event.name.isEmpty {
showNeedsNameAlert.toggle()
}
}
@@ -201,36 +173,29 @@ struct AddEventView: View {
} message: {
Text("Give your Event a name before saving.")
}
if eventName.isEmpty {
HStack {
Image(systemName: "exclamationmark")
.foregroundStyle(.red)
Text("Give your event a name.")
}
}
}
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)
.scrollContentBackground(.hidden)
}
.apply {
if #available(iOS 16.4, *) {
$0.presentationBackground(.ultraThinMaterial)
}
}
}
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
event = viewModel.template
dismiss()
}
@@ -243,14 +208,6 @@ struct AddEventView: View {
.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
)
}

View File

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

View File

@@ -12,142 +12,185 @@ struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
@Environment(\.openWindow) var openWindow
@State var hovering: Bool = false
#if canImport(AppKit)
var body: some View {
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: $event
)
} label: {
ZStack {
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 7)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
ZStack {
Color.black.opacity(hovering ? 0.5 : 0.0)
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 7)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.shadow(radius: 5)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.shadow(radius: 5)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
Text("\(event.name)")
.font(.headline)
.foregroundStyle(.one)
.strikethrough(event.complete)
.multilineTextAlignment(.leading)
}
if !event.notes.isEmpty {
Text(event.notes)
.font(.subheadline)
.foregroundStyle(.one.opacity(0.8))
.multilineTextAlignment(.leading)
}
Text(
event.date.formatted(
date: .long,
time: .shortened
)
Text("\(event.name)")
.bold()
.foregroundStyle(.one)
.strikethrough(event.complete)
.multilineTextAlignment(.leading)
}
if !event.notes.isEmpty {
Text(event.notes)
.foregroundStyle(.one.opacity(0.8))
.multilineTextAlignment(.leading)
}
Text(
event.date.formatted(
date: .long,
time: .shortened
)
.font(.subheadline)
)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
if event.recurrence != .none {
Text("Occurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.one.opacity(event.complete ? 0.5 : 1))
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date).long)")
.multilineTextAlignment(.trailing)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
}
CompleteEventButton(
viewModel: viewModel,
event: $event
)
}
.fixedSize(horizontal: false, vertical: true)
}
.onHover { isHovering in
withAnimation {
hovering.toggle()
}
}
.onTapGesture {
openWindow(value: event.id)
}
.contextMenu() {
Button(role: .destructive) {
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events.remove(at: eventToModify)
viewModel.saveEvents()
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
#else
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 7)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.shadow(radius: 5)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
if event.recurrence != .none {
Text("Occurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.one.opacity(event.complete ? 0.5 : 1))
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date).long)")
.font(.subheadline)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
.multilineTextAlignment(.trailing)
}
Button() {
withAnimation {
event.complete.toggle()
}
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events[eventToModify] = event
viewModel.saveEvents()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.bold()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.buttonStyle(.borderless)
.frame(maxWidth: 25, maxHeight: 25)
.shadow(radius: 5)
.padding(.trailing, 5)
.apply {
if #available(iOS 17, *) {
$0.sensoryFeedback(.success, trigger: event.complete)
}
}
Text("\(event.name)")
.font(.headline)
.foregroundStyle(.one)
.strikethrough(event.complete)
.multilineTextAlignment(.leading)
}
.transition(.opacity)
.padding(.vertical, 5)
.background(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(
.one.opacity(0.5),
lineWidth: 1
)
if !event.notes.isEmpty {
Text(event.notes)
.font(.subheadline)
.foregroundStyle(.one.opacity(0.8))
.multilineTextAlignment(.leading)
}
Text(
event.date.formatted(
date: .long,
time: .shortened
)
)
.clipShape(
RoundedRectangle(cornerRadius: 10)
.font(.subheadline)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
.fixedSize(horizontal: false, vertical: true)
if event.recurrence != .none {
Text("Occurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.one.opacity(event.complete ? 0.5 : 1))
}
}
.contextMenu() {
Button(role: .destructive) {
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events.remove(at: eventToModify)
viewModel.saveEvents()
}
} label: {
Label("Delete", systemImage: "trash")
Spacer()
VStack {
Text("\(daysUntilEvent(event.date).long)")
.font(.subheadline)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
.multilineTextAlignment(.trailing)
}
CompleteEventButton(
viewModel: viewModel,
event: $event
)
}
.padding(.vertical, 5)
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(.one.opacity(0.5), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 15))
.fixedSize(horizontal: false, vertical: true)
.contextMenu() {
Button(role: .destructive) {
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events.remove(at: eventToModify)
viewModel.saveEvents()
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
#endif
}
#Preview("EventListView") {

View File

@@ -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)

View File

@@ -5,19 +5,13 @@
// Created by neon443 on 12/05/2025.
//
import SwiftUI;import AppIntents
import SwiftUI
import AppIntents
struct HomeView: View {
@ObservedObject var viewModel: EventViewModel
@ObservedObject var settingsModel: SettingsViewModel
@State private var eventName: String = ""
@State private var eventComplete: Bool = false
@State private var eventCompleteDesc: String = ""
@State private var eventSymbol: String = "star"
@State private var eventColor: Color = randomColor()
@State private var eventNotes: String = ""
@State private var eventDate = Date()
@State private var eventRecurrence: Event.RecurrenceType = .none
@State private var showingAddEventView: Bool = false
@State private var searchInput: String = ""
@Environment(\.colorScheme) var appearance
@@ -47,36 +41,30 @@ struct HomeView: View {
ZStack {
backgroundGradient
VStack {
ZStack {
TextField(
"\(Image(systemName: "magnifyingglass")) Search",
text: $searchInput
)
.padding(.trailing, searchInput.isEmpty ? 0 : 30)
.animation(.spring, value: searchInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.submitLabel(.done)
.focused($focusedField, equals: Field.Search)
.onSubmit {
focusedField = nil
}
MagicClearButton(text: $searchInput)
.onTapGesture {
focusedField = nil
}
}
.padding(.horizontal)
if filteredEvents.isEmpty && !searchInput.isEmpty {
HelpView(searchInput: $searchInput, focusedField: focusedField)
} else {
ScrollView {
ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: event)
// 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)
.id(event.complete)
}
.padding(.horizontal)
}
.padding(.horizontal)
// }
if filteredEvents.isEmpty {
HelpView(
searchInput: $searchInput,
@@ -88,30 +76,16 @@ struct HomeView: View {
.animation(.default, value: filteredEvents)
}
}
.searchable(text: $searchInput)
.navigationTitle("Near Future")
.apply {
if #available(iOS 17, *) {
$0.toolbarTitleDisplayMode(.inlineLarge)
} else {
$0.navigationBarTitleDisplayMode(.inline)
}
}
.modifier(navigationInlineLarge())
.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
viewModel: viewModel
)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
AddEventButton(showingAddEventView: $showingAddEventView)
}
}

View File

@@ -34,17 +34,12 @@ struct AddEventButton: View {
Button() {
showingAddEventView.toggle()
} label: {
ZStack {
Circle()
.frame(width: 33)
.foregroundStyle(.one)
Image(systemName: "plus")
.resizable()
.scaledToFit()
.frame(width: 15)
.bold()
.foregroundStyle(.two)
}
Image(systemName: "plus")
.resizable()
.scaledToFit()
.frame(width: 15)
.bold()
.foregroundStyle(.one)
}
}
}

View File

@@ -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")
}

View File

@@ -75,7 +75,6 @@ struct ImportView: View {
.ignoresSafeArea()
ZStack {
Rectangle()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 25))
VStack(alignment: .center) {
Text("Are you sure?")

View File

@@ -10,48 +10,21 @@ 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
}
}
func changeIcon(to: String) {
func changeIcon(to toIcon: String) {
guard UIApplication.shared.supportsAlternateIcons else {
print("doesnt tsupport alternate icons")
return
}
guard to != "orange" else {
guard toIcon != "orange" else {
UIApplication.shared.setAlternateIconName(nil) { error in
print(error as Any)
}
return
}
UIApplication.shared.setAlternateIconName(to) { error in
UIApplication.shared.setAlternateIconName(toIcon) { error in
print(error as Any)
}
}
@@ -74,6 +47,7 @@ struct SettingsView: View {
.foregroundStyle(color)
.frame(width: 30)
}
.buttonStyle(.plain)
if ColorCodable(color) == settingsModel.settings.tint {
let needContrast: Bool = ColorCodable(color) == settingsModel.settings.tint
Circle()
@@ -94,6 +68,8 @@ struct SettingsView: View {
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()
@@ -102,8 +78,6 @@ struct SettingsView: View {
}
}
}
Text("\(Image(systemName: "xmark")) Notifications disabled for Near Future")
.foregroundStyle(.red)
} else {
Text("\(Image(systemName: "checkmark")) Notifications enabled for Near Future")
.foregroundStyle(.green)
@@ -116,13 +90,7 @@ struct SettingsView: View {
NavigationLink() {
iCloudSettingsView(
viewModel: viewModel,
settingsModel: settingsModel,
hasUbiquitous: $hasUbiquitous,
lastSyncWasSuccessful: $lastSyncWasSuccessful,
lastSyncWasNormalAgo: $lastSyncWasNormalAgo,
localCountEqualToiCloud: $localCountEqualToiCloud,
icloudCountEqualToLocal: $icloudCountEqualToLocal,
updateStatus: updateStatus
settingsModel: settingsModel
)
} label: {
HStack {
@@ -131,12 +99,12 @@ struct SettingsView: View {
Spacer()
Circle()
.frame(width: 20, height: 20)
.foregroundStyle(iCloudStatusColor)
.foregroundStyle(viewModel.iCloudStatusColor)
}
}
.onAppear {
viewModel.sync()
updateStatus()
viewModel.updateiCStatus()
}
NavigationLink() {
ImportView(viewModel: viewModel, importStr: $importStr)
@@ -180,15 +148,9 @@ struct SettingsView: View {
}
}
}
.scrollContentBackground(.hidden)
.navigationTitle("Settings")
.apply {
if #available(iOS 17, *) {
$0.toolbarTitleDisplayMode(.inlineLarge)
} else {
$0.navigationBarTitleDisplayMode(.inline)
}
}
.modifier(navigationInlineLarge())
.scrollContentBackground(.hidden)
}
}
}

View File

@@ -19,6 +19,11 @@ struct WhatsNewView: View {
@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",
@@ -53,23 +58,22 @@ struct WhatsNewView: View {
}
var body: some View {
NavigationStack {
List {
VStack {
Text("What's New")
.font(.largeTitle)
.bold()
AboutView()
Divider()
VStack(alignment: .leading) {
ForEach(whatsNewChunks) { new in
WhatsNewChunkView(
symbol: new.symbol,
title: new.title,
subtitle: new.subtitle
)
}
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()
@@ -79,24 +83,14 @@ struct WhatsNewView: View {
.font(.headline)
.frame(height: 40)
.bold()
.frame(maxWidth: .infinity)
}
.buttonStyle(BorderedProminentButtonStyle())
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding().padding()
.apply {
if #available(iOS 17, *) {
$0.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: bye)
}
// .frame(maxWidth: .infinity)
}
.foregroundStyle(.orange)
.modifier(glassButton())
.modifier(hapticHeavy(trigger: bye))
}
.scrollContentBackground(.hidden)
.presentationDragIndicator(.visible)
.apply {
if #available(iOS 16.4, *) {
$0.presentationBackground(.ultraThinMaterial)
}
}
.onDisappear {
settingsModel.settings.prevAppVersion = getVersion()+getBuildID()
settingsModel.saveSettings()
@@ -122,7 +116,7 @@ struct WhatsNewChunkView: View {
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
.foregroundStyle(Color.accentColor)
.foregroundStyle(Color.orange)
.padding(.trailing, 15)
VStack(alignment: .leading) {
Text(title)

View File

@@ -13,13 +13,13 @@ struct iCloudSettingsView: View {
@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
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 {
@@ -55,7 +55,7 @@ struct iCloudSettingsView: View {
Button("OK", role: .destructive) {
viewModel.replaceiCloudWithLocalData()
viewModel.sync()
updateStatus()
viewModel.updateiCStatus()
}
Button("Cancel", role: .cancel) {}
} message: {
@@ -64,7 +64,7 @@ struct iCloudSettingsView: View {
Button() {
viewModel.sync()
updateStatus()
viewModel.updateiCStatus()
} label: {
Image(systemName: "arrow.triangle.2.circlepath")
.resizable()
@@ -87,7 +87,7 @@ struct iCloudSettingsView: View {
Button("OK", role: .destructive) {
viewModel.replaceLocalWithiCloudData()
viewModel.sync()
updateStatus()
viewModel.updateiCStatus()
}
Button("Cancel", role: .cancel) {}
} message: {
@@ -112,23 +112,23 @@ struct iCloudSettingsView: View {
.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)")
@@ -138,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")")
@@ -148,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)")
@@ -158,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)")
@@ -172,11 +172,10 @@ struct iCloudSettingsView: View {
}
.refreshable {
viewModel.sync()
updateStatus()
viewModel.updateiCStatus()
}
.scrollContentBackground(.hidden)
.navigationTitle("iCloud")
.navigationBarTitleDisplayMode(.inline)
}
}
}
@@ -184,12 +183,6 @@ struct iCloudSettingsView: View {
#Preview("iCloudSettingsView") {
iCloudSettingsView(
viewModel: dummyEventViewModel(),
settingsModel: dummySettingsViewModel(),
hasUbiquitous: .constant(true),
lastSyncWasSuccessful: .constant(true),
lastSyncWasNormalAgo: .constant(true),
localCountEqualToiCloud: .constant(true),
icloudCountEqualToLocal: .constant(true),
updateStatus: {}
settingsModel: dummySettingsViewModel()
)
}

View File

@@ -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())
}
}
}

View File

@@ -11,17 +11,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 +54,4 @@ Contributions are welcome! Just follow these steps:
## Used Tools/Frameworks
- Swift & SwiftUI by Apple
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -0,0 +1,50 @@
{
"fill" : {
"linear-gradient" : [
"display-p3:0.12307,0.73184,0.43944,1.00000",
"display-p3:0.12142,0.28022,0.58006,1.00000"
]
},
"groups" : [
{
"layers" : [
{
"fill" : {
"automatic-gradient" : "display-p3:1.00000,0.30347,0.00000,1.00000"
},
"image-name" : "Rounded Rectangle.png",
"name" : "Rounded Rectangle",
"position" : {
"scale" : 1,
"translation-in-points" : [
-466.2265625,
37.9921875
]
}
},
{
"fill" : {
"automatic-gradient" : "display-p3:0.96611,0.36599,0.00000,1.00000"
},
"hidden" : false,
"image-name" : "Oval copy.png",
"name" : "Oval copy"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -0,0 +1,129 @@
//
// CompleteEventButton.swift
// NearFuture
//
// Created by neon443 on 15/06/2025.
//
import SwiftUI
struct CompleteEventButton: View {
@ObservedObject var viewModel: EventViewModel
@Binding var event: Event
@State var timer: Timer?
@State var largeTick: Bool = false
@State var completeInProgress: Bool = false
@State var completeStartTime: Date = .now
@State var progress: Double = 0
private let completeDuration: TimeInterval = 3.0
var isMac: Bool {
#if canImport(AppKit)
return true
#else
return false
#endif
}
func startCompleting() {
#if canImport(UIKit)
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
#endif
withAnimation { completeInProgress = true }
completeStartTime = .now
progress = 0
timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard completeInProgress else { return }
guard timer.isValid else { return }
let elapsed = Date().timeIntervalSince(completeStartTime)
progress = min(1, elapsed)
if progress >= 1 {
withAnimation { completeInProgress = false }
viewModel.completeEvent(&event)
#if canImport(UIKit)
UINotificationFeedbackGenerator().notificationOccurred(.success)
#endif
timer.invalidate()
progress = 0
}
}
RunLoop.main.add(timer!, forMode: .common)
}
var body: some View {
Group {
if completeInProgress {
ZStack {
CircularProgressView(progress: $progress)
Image(systemName: "xmark")
.bold()
}
.onTapGesture {
withAnimation { completeInProgress = false }
}
} else {
Image(systemName: event.complete ? "checkmark.circle.fill" : "circle")
.resizable().scaledToFit()
.foregroundStyle(event.complete ? .green : event.color.color)
.bold()
.onTapGesture {
startCompleting()
}
.onHover() { hovering in
withAnimation {
largeTick.toggle()
}
}
.scaleEffect(largeTick ? 1.5 : 1)
}
}
.frame(maxWidth: isMac ? 20 : 30)
.shadow(color: .one.opacity(0.2), radius: 2.5)
.padding(.trailing, isMac ? 15 : 5)
.transition(.scale)
.animation(.spring, value: completeInProgress)
.animation(
.spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2),
value: largeTick
)
}
}
struct CircularProgressView: View {
@Binding var progress: Double
var body: some View {
ZStack {
Circle()
.stroke(
.two,
lineWidth: 5
)
Circle()
.trim(from: 0, to: progress)
.stroke(
.one,
lineWidth: 5
// style: StrokeStyle(
// lineWidth: 5,
// lineCap: .round
// )
)
.rotationEffect(.degrees(-90))
}
}
}
#Preview {
CompleteEventButton(
viewModel: dummyEventViewModel(),
event: .constant(dummyEventViewModel().example)
)
.scaleEffect(5)
}
#Preview {
CircularProgressView(progress: .constant(0.5))
}

View File

@@ -17,8 +17,6 @@ extension View {
)
.ignoresSafeArea(.all)
}
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}
extension AnyTransition {

View File

@@ -0,0 +1,47 @@
//
// AccentIcon.swift
// NearFuture
//
// Created by neon443 on 13/06/2025.
//
import Foundation
import SwiftUI
#if canImport(AppKit)
import AppKit
#endif
class AccentIcon {
#if canImport(UIKit)
var icon: UIImage
#elseif canImport(AppKit)
var icon: NSImage
#endif
var color: Color
var name: String
init(_ colorName: String) {
#if canImport(UIKit)
self.icon = UIImage(named: "AppIcon")!
self.color = Color(uiColor: UIColor(named: "uiColors/\(colorName)")!)
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: "AppIcon")
self.color = Color(nsColor: NSColor(named: "uiColors/\(colorName)")!)
#endif
self.name = colorName
if colorName != "orange" {
setSelfIcon(to: colorName)
}
}
func setSelfIcon(to name: String) {
#if canImport(UIKit)
self.icon = UIImage(named: name)!
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: name)
#endif
}
}

View File

@@ -0,0 +1,83 @@
//
// ColorCodable.swift
// NearFuture
//
// Created by neon443 on 13/06/2025.
//
import Foundation
import SwiftUI
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
struct ColorCodable: Codable, Equatable, Hashable {
init(_ color: Color) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1
#if canImport(UIKit)
let uiColor = UIColor(color)
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
#elseif canImport(AppKit)
let nscolor = NSColor(color).usingColorSpace(.deviceRGB)
nscolor!.getRed(&r, green: &g, blue: &b, alpha: &a)
#endif
self = ColorCodable(
red: r,
green: g,
blue: b
)
}
#if canImport(UIKit)
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 = ColorCodable(
red: r,
green: g,
blue: b
)
}
#elseif canImport(AppKit)
init(nsColor: NSColor) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0
let nsColor = nsColor.usingColorSpace(.deviceRGB)
nsColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
self = ColorCodable(
red: r,
green: g,
blue: b
)
}
#endif
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
}
}
}

View File

@@ -11,10 +11,8 @@ import SwiftUI
import WidgetKit
import UserNotifications
import AppIntents
import AudioToolbox
#if canImport(AppKit)
import AppKit
import IOKit
#endif
//@Model
@@ -26,7 +24,7 @@ import IOKit
// }
//}
struct Event: Identifiable, Codable, Equatable, Animatable {
struct Event: Identifiable, Codable, Equatable, Animatable, Hashable {
var id = UUID()
var name: String
var complete: Bool
@@ -42,75 +40,6 @@ struct Event: Identifiable, Codable, Equatable, Animatable {
}
}
struct ColorCodable: Codable, Equatable {
init(_ color: Color) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1
#if canImport(UIKit)
let uiColor = UIColor(color)
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
#elseif canImport(AppKit)
let nscolor = NSColor(color).usingColorSpace(.deviceRGB)
nscolor!.getRed(&r, green: &g, blue: &b, alpha: &a)
#endif
self = ColorCodable(
red: r,
green: g,
blue: b
)
}
#if canImport(UIKit)
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 = ColorCodable(
red: r,
green: g,
blue: b
)
}
#elseif canImport(AppKit)
init(nsColor: NSColor) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0
let nsColor = nsColor.usingColorSpace(.deviceRGB)
nsColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
self = ColorCodable(
red: r,
green: g,
blue: b
)
}
#endif
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())
@@ -164,6 +93,25 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
@Published var localEventCount: Int = 0
@Published var syncStatus: String = "Not Synced"
@Published var hasUbiquitous: Bool = false
@Published var lastSyncWasSuccessful: Bool = false
@Published var lastSyncWasNormalAgo: Bool = false
@Published var localCountEqualToiCloud: Bool = false
@Published var icloudCountEqualToLocal: Bool = false
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
}
}
init(load: Bool = true) {
self.editableTemplate = template
if load {
@@ -274,6 +222,24 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
saveEvents() //sync with icloud
}
func editEvent(_ editedEvent: Event) {
if let index = events.firstIndex(where: { editedEvent.id == $0.id }) {
self.events[index] = editedEvent
saveEvents()
}
}
func completeEvent(_ event: inout Event) {
withAnimation { event.complete.toggle() }
let eventToModify = self.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
self.events[eventToModify] = event
self.saveEvents()
}
}
func removeEvent(at index: IndexSet) {
events.remove(atOffsets: index)
saveEvents() //sync local and icl
@@ -343,6 +309,14 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
}
}
func updateiCStatus() {
hasUbiquitous = hasUbiquitousKeyValueStore()
lastSyncWasSuccessful = syncStatus.contains("Success")
lastSyncWasNormalAgo = lastSync?.timeIntervalSinceNow.isNormal ?? false
localCountEqualToiCloud = localEventCount == icloudEventCount
icloudCountEqualToLocal = icloudEventCount == localEventCount
}
//MARK: Danger Zone
func dangerClearLocalData() {
UserDefaults.standard.removeObject(forKey: "events")

View File

@@ -18,40 +18,6 @@ struct NFSettings: Codable, Equatable {
var prevAppVersion: String = getVersion()+getBuildID()
}
class AccentIcon {
#if canImport(UIKit)
var icon: UIImage
#elseif canImport(AppKit)
var icon: NSImage
#endif
var color: Color
var name: String
init(_ colorName: String) {
#if canImport(UIKit)
self.icon = UIImage(named: "AppIcon")!
self.color = Color(uiColor: UIColor(named: "uiColors/\(colorName)")!)
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: "AppIcon")
self.color = Color(nsColor: NSColor(named: "uiColors/\(colorName)")!)
#endif
self.name = colorName
if colorName != "orange" {
setSelfIcon(to: colorName)
}
}
func setSelfIcon(to name: String) {
#if canImport(UIKit)
self.icon = UIImage(named: name)!
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: name)
#endif
}
}
class SettingsViewModel: ObservableObject {
@Published var settings: NFSettings = NFSettings()

View File

@@ -0,0 +1,36 @@
//
// SymbolsLoader.swift
// NearFuture
//
// Created by neon443 on 14/06/2025.
//
import Foundation
class SymbolsLoader: ObservableObject {
@Published var allSymbols: [String] = []
init() {
self.allSymbols = getAllSymbols()
}
func getSymbols(_ searched: String) -> [String] {
if searched.isEmpty {
return allSymbols
} else {
return allSymbols.filter() { $0.localizedCaseInsensitiveContains(searched) }
}
}
func getAllSymbols() -> [String] {
var allSymbols = [String]()
if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"),
let resPath = bundle.path(forResource: "name_availability", ofType: "plist"),
let plist = try? NSDictionary(contentsOf: URL(fileURLWithPath: resPath), error: ()),
let plistSymbols = plist["symbols"] as? [String: String]
{
allSymbols = Array(plistSymbols.keys)
}
return allSymbols
}
}

View File

@@ -0,0 +1,89 @@
//
// SymbolsPicker.swift
// NearFuture
//
// Created by neon443 on 14/06/2025.
//
import SwiftUI
struct SymbolsPicker: View {
@StateObject private var symbolsLoader = SymbolsLoader()
@Binding var selection: String
@FocusState var searchfocuesd: Bool
@State var searchInput: String = ""
@State var browsing: Bool = false
@Environment(\.dismiss) var dismiss
var symbols: [String] {
return symbolsLoader.getSymbols(searchInput)
}
private func gridLayout(forWidth geoSizeWidth: CGFloat) -> [GridItem] {
let gridItem = GridItem(.fixed(80), spacing: 20, alignment: .center)
let columns = Int(geoSizeWidth/100.rounded(.up))
return Array(repeating: gridItem, count: columns)
}
var body: some View {
NavigationStack {
GeometryReader { geo in
ScrollView {
if symbols.isEmpty {
HStack {
Image(systemName: "magnifyingglass")
.resizable().scaledToFit()
.frame(width: 30)
Text("You look lost")
.font(.title)
.bold()
}
.padding()
Text("The symbol picker search only works with exact matches, try a different search term.")
}
LazyVGrid(columns: gridLayout(forWidth: geo.size.width)) {
ForEach(symbols, id: \.self) { symbol in
Button() {
selection = symbol
searchInput = ""
dismiss()
} label: {
VStack {
Image(systemName: symbol)
.resizable()
.scaledToFit()
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .gray, .black)
Text(symbol)
.truncationMode(.middle)
.font(.footnote)
}
}
.frame(maxWidth: 80, maxHeight: 80)
.buttonStyle(.plain)
}
}
}
}
.searchable(text: $searchInput)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
if !browsing {
Button() {
searchInput = ""
dismiss()
} label: {
Label("Cancel", systemImage: "xmark")
}
}
}
}
}
}
}
#Preview {
SymbolsPicker(selection: .constant(""))
}

View File

@@ -0,0 +1,76 @@
//
// ViewModifiers.swift
// NearFuture
//
// Created by neon443 on 13/06/2025.
//
import Foundation
import SwiftUI
struct hapticHeavy: ViewModifier {
var trigger: any Equatable
init(trigger: any Equatable) {
self.trigger = trigger
}
func body(content: Content) -> some View {
if #available(iOS 17, *) {
content
.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: trigger)
} else {
content
}
}
}
struct glassButton: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 19, macOS 16, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.borderedProminent)
.clipShape(RoundedRectangle(cornerRadius: 15))
.tint(.two)
}
}
}
struct hapticSuccess: ViewModifier {
var trigger: any Equatable
init(trigger: any Equatable) {
self.trigger = trigger
}
func body(content: Content) -> some View {
if #available(iOS 17, *) {
content.sensoryFeedback(.success, trigger: trigger)
} else {
content
}
}
}
struct navigationInlineLarge: ViewModifier {
func body(content: Content) -> some View {
#if os(macOS)
content
.toolbarTitleDisplayMode(.inlineLarge)
#else
content
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
struct presentationSizeForm: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 18, macOS 15, *) {
content.presentationSizing(.form)
} else {
content
}
}
}