mirror of
https://github.com/neon443/NearFuture.git
synced 2026-03-11 14:56:15 +00:00
Compare commits
42 Commits
ee2e05c523
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3501a9379 | ||
|
|
6f870ffa4f | ||
|
|
9841574d37 | ||
|
|
899304833c | ||
|
|
4629d4f75f | ||
|
|
be68c44ffe | ||
|
|
3fe9077e69 | ||
|
|
121dd79d54 | ||
|
|
01116c7fcb | ||
|
|
3690a9e4d2 | ||
|
|
c393404fec | ||
|
|
3ee22da036 | ||
|
|
2b25ddf9b3 | ||
|
|
01ff82181a | ||
|
|
b7ef7b4e19 | ||
|
|
7727f14ad4 | ||
|
|
e4107a2faa | ||
|
|
5ec16dd67a | ||
|
|
0cffe243eb | ||
|
|
6533fb85ed | ||
|
|
5c667679d5 | ||
|
|
5dd25f1ede | ||
|
|
d80011ea27 | ||
|
|
a40d4f4300 | ||
|
|
22af9fc060 | ||
|
|
2dd2c51059 | ||
|
|
4c9e72fad2 | ||
|
|
cb26c69492 | ||
|
|
13ef94ea3e | ||
|
|
6e5b7adbc4 | ||
|
|
659f14d5a3 | ||
|
|
0663ba9e59 | ||
|
|
6e7bc6c2d1 | ||
|
|
3f21074091 | ||
|
|
d5580e52f5 | ||
|
|
84a7091e05 | ||
|
|
44b40894e4 | ||
|
|
e4842bd29a | ||
|
|
b378a831be | ||
|
|
2ff96a7093 | ||
|
|
4f5e31a6f3 | ||
|
|
266b27d817 |
@@ -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.0.1
|
||||
NAME = Near Future
|
||||
BUILD_NUMBER = 1
|
||||
BUILD_NUMBER = 52
|
||||
|
||||
@@ -24,31 +24,34 @@ 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}) ?? viewModel.template
|
||||
},
|
||||
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") {
|
||||
@@ -60,7 +63,10 @@ struct NearFutureApp: App {
|
||||
.defaultPosition(UnitPoint.center)
|
||||
|
||||
Settings {
|
||||
Text("wip")
|
||||
SettingsView(
|
||||
viewModel: viewModel,
|
||||
settingsModel: settingsModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
struct NearFutureCommands: Commands {
|
||||
var body: some Commands {
|
||||
CommandGroup(after: CommandGroupPlacement.appInfo) {
|
||||
Text("hi")
|
||||
// Text("hi")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ struct ArchiveView: View {
|
||||
ScrollView {
|
||||
ForEach(filteredEvents) { event in
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeEvent(event)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.tint(.red )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
//
|
||||
// AddEventView.swift
|
||||
// MacNearFuture
|
||||
//
|
||||
// Created by neon443 on 11/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSymbolsPicker
|
||||
|
||||
struct AddEventView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
|
||||
@Binding var event: Event
|
||||
|
||||
@State var adding: Bool
|
||||
@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 body: some View {
|
||||
ZStack {
|
||||
if !adding {
|
||||
backgroundGradient
|
||||
}
|
||||
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,
|
||||
title: "Choose a Symbol",
|
||||
searchLabel: "Search...",
|
||||
autoDismiss: true)
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// MagicClearButton(text: $eventNotes)
|
||||
}
|
||||
|
||||
|
||||
// date picker
|
||||
HStack {
|
||||
Spacer()
|
||||
DatePicker("", selection: $event.date, displayedComponents: .date)
|
||||
Spacer()
|
||||
Button() {
|
||||
event.date = Date()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.left")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.frame(width: 20)
|
||||
}
|
||||
|
||||
DatePicker(
|
||||
"",
|
||||
selection: $event.date,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("\(adding ? "Add Event" : "")")
|
||||
.toolbar {
|
||||
ToolbarItem() {
|
||||
if adding {
|
||||
Button() {
|
||||
resetAddEventView()
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem/*(placement: .topBarTrailing)*/ {
|
||||
if adding {
|
||||
Button {
|
||||
viewModel.addEvent(
|
||||
newEvent: event
|
||||
)
|
||||
bye.toggle()
|
||||
resetAddEventView()
|
||||
} label: {
|
||||
Text("Save")
|
||||
.font(.headline)
|
||||
.cornerRadius(10)
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
}
|
||||
.tint(.accent)
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.sensoryFeedback(.success, 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.")
|
||||
}
|
||||
if event.name.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark")
|
||||
.foregroundStyle(.red)
|
||||
Text("Give your event a name.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.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,
|
||||
event: .constant(vm.template),
|
||||
adding: true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +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,
|
||||
event: $event,
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -11,20 +11,41 @@ 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)
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeEvent(event)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchInput)
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
155
MacNearFuture/Views/SettingsView.swift
Normal file
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()
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
A90D49462DDE1C7A00781124 /* Tints.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A90D49432DDE1C1100781124 /* Tints.xcassets */; };
|
||||
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 */; };
|
||||
@@ -26,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 */; };
|
||||
@@ -44,17 +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 /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20C92DE730740008D61C /* EditEventView.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 */; };
|
||||
A9B78B942DF9F3CF00647399 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B78B932DF9F3CF00647399 /* AddEventView.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,16 +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 /* EditEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventView.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>"; };
|
||||
A9B78B932DF9F3CF00647399 /* AddEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEventView.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 */
|
||||
|
||||
@@ -143,7 +168,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A90D49562DDE2D5800781124 /* SFSymbolsPicker in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -151,7 +175,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A920C2BE2D24021A00E4F9B1 /* SFSymbolsPicker in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -170,6 +193,9 @@
|
||||
children = (
|
||||
A920C28B2D24011400E4F9B1 /* Events.swift */,
|
||||
A90D49602DDE626300781124 /* Settings.swift */,
|
||||
A91EF8062DFC8B8B00B8463D /* ColorCodable.swift */,
|
||||
A95E9ED72DFC742B00ED655F /* AccentIcon.swift */,
|
||||
A91EF8162DFD77A500B8463D /* SymbolsPicker */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@@ -191,9 +217,8 @@
|
||||
children = (
|
||||
A90D49332DDE0FAF00781124 /* ContentViewMac.swift */,
|
||||
A98C20CF2DE731BD0008D61C /* HomeView.swift */,
|
||||
A98C20CA2DE730740008D61C /* EventListViewMac.swift */,
|
||||
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */,
|
||||
A98C20C82DE730420008D61C /* Events */,
|
||||
A91EF80F2DFCB66C00B8463D /* SettingsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -204,6 +229,8 @@
|
||||
A920C2872D24011400E4F9B1 /* NearFutureApp.swift */,
|
||||
A90D49512DDE2D0000781124 /* Extensions.swift */,
|
||||
A90D49202DDE0A3B00781124 /* Model */,
|
||||
A91EF80A2DFC910000B8463D /* ViewModifiers.swift */,
|
||||
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -215,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 = (
|
||||
@@ -352,15 +388,6 @@
|
||||
path = NearFutureWidgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A98C20C82DE730420008D61C /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A9B78B932DF9F3CF00647399 /* AddEventView.swift */,
|
||||
A98C20C92DE730740008D61C /* EditEventView.swift */,
|
||||
);
|
||||
path = Events;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -376,11 +403,9 @@
|
||||
);
|
||||
dependencies = (
|
||||
A98C20D22DE732B10008D61C /* PBXTargetDependency */,
|
||||
A90D494D2DDE2C6000781124 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MacNearFuture;
|
||||
packageProductDependencies = (
|
||||
A90D49552DDE2D5800781124 /* SFSymbolsPicker */,
|
||||
);
|
||||
productName = MacNearFuture;
|
||||
productReference = A90D49262DDE0FA400781124 /* Near Future.app */;
|
||||
@@ -402,7 +427,6 @@
|
||||
);
|
||||
name = NearFuture;
|
||||
packageProductDependencies = (
|
||||
A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */,
|
||||
);
|
||||
productName = NearFuture;
|
||||
productReference = A920C2842D24011400E4F9B1 /* NearFuture.app */;
|
||||
@@ -456,7 +480,6 @@
|
||||
);
|
||||
mainGroup = A920C27B2D24011300E4F9B1;
|
||||
packageReferences = (
|
||||
A920C2BC2D24021900E4F9B1 /* XCRemoteSwiftPackageReference "SFSymbolsPicker" */,
|
||||
);
|
||||
productRefGroup = A920C2852D24011400E4F9B1 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -507,18 +530,30 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A98C20CB2DE730740008D61C /* EventListViewMac.swift in Sources */,
|
||||
A98C20CC2DE730740008D61C /* EditEventView.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 */,
|
||||
A9B78B942DF9F3CF00647399 /* AddEventView.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;
|
||||
};
|
||||
@@ -536,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;
|
||||
@@ -554,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 */,
|
||||
@@ -564,10 +610,6 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
A90D494D2DDE2C6000781124 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = A90D494C2DDE2C6000781124 /* SFSymbolsPicker */;
|
||||
};
|
||||
A979F6132D270AF90094C0B3 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
platformFilter = ios;
|
||||
@@ -597,6 +639,7 @@
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -609,7 +652,7 @@
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -628,6 +671,7 @@
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -640,7 +684,7 @@
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -793,6 +837,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
@@ -821,7 +866,8 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -845,6 +891,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Near Future";
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
@@ -870,7 +917,8 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -996,35 +1044,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 */;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,35 +23,48 @@ 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)
|
||||
}
|
||||
.transition(.moveAndFadeReversed)
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeEvent(event)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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,
|
||||
event: $viewModel.editableTemplate,
|
||||
adding: true
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -6,19 +6,16 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSymbolsPicker
|
||||
|
||||
struct AddEventView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
|
||||
@Binding var event: Event
|
||||
@State var event: Event = dummyEventViewModel().template
|
||||
|
||||
@State var adding: Bool
|
||||
@State var adding: Bool = true
|
||||
@State var showNeedsNameAlert: Bool = false
|
||||
@State var isSymbolPickerPresented: Bool = false
|
||||
|
||||
@State private var bye: Bool = false
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
private enum Field {
|
||||
case Name, Notes
|
||||
@@ -26,99 +23,115 @@ 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
|
||||
}
|
||||
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,
|
||||
title: "Choose a Symbol",
|
||||
searchLabel: "Search...",
|
||||
autoDismiss: true)
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
// MagicClearButton(text: $eventNotes)
|
||||
}
|
||||
|
||||
|
||||
// date picker
|
||||
HStack {
|
||||
Spacer()
|
||||
DatePicker("", selection: $event.date, displayedComponents: .date)
|
||||
.datePickerStyle(.wheel)
|
||||
Spacer()
|
||||
Button() {
|
||||
event.date = Date()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.left")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20)
|
||||
.buttonStyle(.borderless)
|
||||
.sheet(isPresented: $isSymbolPickerPresented) {
|
||||
SymbolsPicker(
|
||||
selection: $event.symbol
|
||||
)
|
||||
.presentationDetents([.medium])
|
||||
.modifier(presentationSizeForm())
|
||||
}
|
||||
TextField("Event Name", text: $event.name)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.frame(width: 20)
|
||||
}
|
||||
|
||||
DatePicker(
|
||||
"",
|
||||
selection: $event.date,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
.datePickerStyle(.wheel)
|
||||
|
||||
// re-ocurrence Picker
|
||||
Picker("Recurrence", selection: $event.recurrence) {
|
||||
ForEach(Event.RecurrenceType.allCases, id: \.self) { recurrence in
|
||||
Text(recurrence.rawValue.capitalized)
|
||||
|
||||
// dscription
|
||||
ZStack {
|
||||
if event.notes.isEmpty {
|
||||
HStack {
|
||||
Text("Event Notes")
|
||||
.opacity(0.5)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
TextEditor(text: $event.notes)
|
||||
.lineLimit(10)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
Text(
|
||||
describeOccurrence(
|
||||
date: event.date,
|
||||
recurrence: event.recurrence
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("\(adding ? "Add Event" : "")")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.modifier(navigationInlineLarge())
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if adding {
|
||||
Button() {
|
||||
resetAddEventView()
|
||||
@@ -127,30 +140,24 @@ struct AddEventView: View {
|
||||
Image(systemName: "xmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30)
|
||||
.tint(.one)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
ToolbarItem() {
|
||||
if adding {
|
||||
Button {
|
||||
viewModel.addEvent(
|
||||
newEvent: event
|
||||
)
|
||||
bye.toggle()
|
||||
resetAddEventView()
|
||||
#if canImport(UIKit)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
#endif
|
||||
} label: {
|
||||
Text("Save")
|
||||
.font(.headline)
|
||||
.cornerRadius(10)
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
Label("Save", systemImage: "checkmark")
|
||||
}
|
||||
.tint(.accent)
|
||||
.apply {
|
||||
if #available(iOS 17, *) {
|
||||
$0.sensoryFeedback(.success, trigger: bye)
|
||||
}
|
||||
}
|
||||
.disabled(event.name.isEmpty)
|
||||
.onTapGesture {
|
||||
if event.name.isEmpty {
|
||||
@@ -165,18 +172,26 @@ struct AddEventView: View {
|
||||
} message: {
|
||||
Text("Give your Event a name before saving.")
|
||||
}
|
||||
if event.name.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark")
|
||||
.foregroundStyle(.red)
|
||||
Text("Give your event a name.")
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
if !adding {
|
||||
Button() {
|
||||
viewModel.editEvent(event)
|
||||
dismiss()
|
||||
#if canImport(UIKit)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Done", systemImage: "checkmark")
|
||||
}
|
||||
.disabled(event.name == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Editing \(event.name) - Ne")
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollContentBackground(isMac ? .automatic : .hidden)
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
@@ -195,7 +210,6 @@ struct AddEventView: View {
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddEventView(
|
||||
viewModel: vm,
|
||||
event: .constant(vm.template),
|
||||
adding: true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,37 +12,13 @@ struct EditEventView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@Binding var event: Event
|
||||
|
||||
fileprivate func saveEdits() {
|
||||
//if there is an event in vM.events with the id of the event we r editing,
|
||||
//firstindex - loops through the arr and finds first element where that events id matches editing event's id
|
||||
if let index = viewModel.events.firstIndex(where: { xEvent in
|
||||
xEvent.id == event.id
|
||||
}) {
|
||||
viewModel.events[index] = event
|
||||
}
|
||||
viewModel.saveEvents()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
AddEventView(
|
||||
viewModel: viewModel,
|
||||
event: $event,
|
||||
event: event,
|
||||
adding: false //bc we editing existing event
|
||||
)
|
||||
.navigationTitle("Edit Event")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button() {
|
||||
saveEdits()
|
||||
} label: {
|
||||
Text("Done")
|
||||
.bold()
|
||||
}
|
||||
.disabled(event.name == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,156 +12,171 @@ 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)
|
||||
}
|
||||
}
|
||||
#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)
|
||||
.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)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeEvent(event)
|
||||
} 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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,7 +12,6 @@ struct HomeView: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@ObservedObject var settingsModel: SettingsViewModel
|
||||
|
||||
@State private var event: Event = dummyEventViewModel().template
|
||||
@State private var showingAddEventView: Bool = false
|
||||
@State private var searchInput: String = ""
|
||||
@Environment(\.colorScheme) var appearance
|
||||
@@ -48,9 +47,29 @@ struct HomeView: View {
|
||||
ScrollView {
|
||||
// LazyVStack {
|
||||
ForEach(filteredEvents) { event in
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.transition(.moveAndFade)
|
||||
.id(event.complete)
|
||||
NavigationLink() {
|
||||
EditEventView(
|
||||
viewModel: viewModel,
|
||||
event: Binding(
|
||||
get: { event },
|
||||
set: { newValue in
|
||||
viewModel.editEvent(newValue)
|
||||
}
|
||||
)
|
||||
)
|
||||
} label: {
|
||||
EventListView(viewModel: viewModel, event: event)
|
||||
.id(event)
|
||||
}
|
||||
.transition(.moveAndFade)
|
||||
.contextMenu() {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeEvent(event)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
// }
|
||||
@@ -67,22 +86,14 @@ struct HomeView: View {
|
||||
}
|
||||
.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,
|
||||
event: $event,
|
||||
adding: true //adding event
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AddEventButton(showingAddEventView: $showingAddEventView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -66,65 +66,19 @@ struct ImportView: View {
|
||||
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()
|
||||
.alert("Are you sure?", isPresented: $showAlert) {
|
||||
Button(role: .destructive) {
|
||||
importEvents()
|
||||
} label: {
|
||||
Text("Replace Events")
|
||||
}
|
||||
Button() {
|
||||
importEvents()
|
||||
} label: {
|
||||
Text("Add to Events")
|
||||
.foregroundStyle(.one)
|
||||
}
|
||||
.frame(maxWidth: 250, maxHeight: 250)
|
||||
}
|
||||
.opacity(showAlert ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +83,11 @@ 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)
|
||||
@@ -117,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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,25 +154,25 @@ struct EventWidgetView: View {
|
||||
.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)
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
|
||||
39
README.md
39
README.md
@@ -1,6 +1,32 @@
|
||||
# NearFuture
|
||||
<div align="center">
|
||||
<br/>
|
||||
<p>
|
||||
<img src="https://github.com/neon443/NearFuture/blob/main/Resources/Assets.xcassets/AppIcon.appiconset/NearFutureIcon.png?raw=true" title="dockphobia" alt="dockphobia icon" width="100" />
|
||||
</p>
|
||||
<h3>Near Future</h3>
|
||||
<p>
|
||||
<a href="https://apps.apple.com/us/app/near-future-event-tracker/id6744963429">
|
||||
download
|
||||
<img alt="GitHub Release" src="https://img.shields.io/itunes/v/6744963429">
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
make your Dock scared of the mouse
|
||||
<br/>
|
||||
<a href="https://neon443.github.io">
|
||||
made by neon443
|
||||
</a>
|
||||
</p>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
[App Store](https://apps.apple.com/us/app/near-future-event-tracker/id6744963429)
|
||||
<div align="center">
|
||||
<a href="https://shipwrecked.hackclub.com/?t=ghrm" target="_blank">
|
||||
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/739361f1d440b17fc9e2f74e49fc185d86cbec14_badge.png"
|
||||
alt="This project is part of Shipwrecked, the world's first hackathon on an island!"
|
||||
style="width: 25%;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**Near Future** is a SwiftUI App to help people track upcoming events - Holidays, Trips, Birthdays, Weddings, Anniversaries.
|
||||
|
||||
@@ -11,17 +37,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 +80,4 @@ Contributions are welcome! Just follow these steps:
|
||||
|
||||
## Used Tools/Frameworks
|
||||
- Swift & SwiftUI by Apple
|
||||
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].
|
||||
- **SFSymbolsPicker** by [alessiorubicini/SFSymbolsPickerForSwiftUI].
|
||||
|
||||
137
Shared/CompleteEventButton.swift
Normal file
137
Shared/CompleteEventButton.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// CompleteEventButton.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 15/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompleteEventButton: View {
|
||||
@ObservedObject var viewModel: EventViewModel
|
||||
@Binding var event: Event
|
||||
|
||||
@MainActor @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.02, repeats: true) { timer in
|
||||
DispatchQueue.main.async {
|
||||
guard completeInProgress else { return }
|
||||
guard let timer = self.timer else { return }
|
||||
guard timer.isValid else { return }
|
||||
let elapsed = Date().timeIntervalSince(completeStartTime)
|
||||
progress = min(1, elapsed)
|
||||
#if canImport(UIKit)
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
#endif
|
||||
|
||||
if progress >= 1 {
|
||||
withAnimation { completeInProgress = false }
|
||||
viewModel.completeEvent(&event)
|
||||
#if canImport(UIKit)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()+0.02) {
|
||||
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))
|
||||
}
|
||||
@@ -17,21 +17,19 @@ extension View {
|
||||
)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
|
||||
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
|
||||
}
|
||||
|
||||
extension AnyTransition {
|
||||
static var moveAndFade: AnyTransition {
|
||||
.asymmetric(
|
||||
insertion: .move(edge: .leading),
|
||||
insertion: .opacity,
|
||||
removal: .move(edge: .trailing)
|
||||
)
|
||||
.combined(with: .opacity)
|
||||
}
|
||||
static var moveAndFadeReversed: AnyTransition {
|
||||
.asymmetric(
|
||||
insertion: .move(edge: .trailing),
|
||||
insertion: .opacity,
|
||||
removal: .move(edge: .leading)
|
||||
)
|
||||
.combined(with: .opacity)
|
||||
|
||||
47
Shared/Model/AccentIcon.swift
Normal file
47
Shared/Model/AccentIcon.swift
Normal 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
|
||||
}
|
||||
}
|
||||
83
Shared/Model/ColorCodable.swift
Normal file
83
Shared/Model/ColorCodable.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -212,20 +160,24 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
||||
eventUUIDs.remove(at: remove)
|
||||
}
|
||||
let components = getDateComponents(events[index].date)
|
||||
|
||||
//check the notif matches event details
|
||||
if req.content.title == events[index].name,
|
||||
req.content.subtitle == events[index].notes,
|
||||
req.trigger == UNCalendarNotificationTrigger(dateMatching: components, repeats: false) {
|
||||
//if it does, make sure the notif delets if u complete the veent
|
||||
if events[index].complete {
|
||||
//if it does, make sure the notif delets if u complete the veent or in the past
|
||||
if events[index].complete || events[index].date > .now {
|
||||
cancelNotif(req.identifier)
|
||||
} else {
|
||||
//dont cancel the notif
|
||||
}
|
||||
} else {
|
||||
//reschedult it because the event details have changed
|
||||
cancelNotif(req.identifier)
|
||||
scheduleEventNotif(events[index])
|
||||
}
|
||||
} else {
|
||||
//cancel if the event is deleted
|
||||
//cancel notif if the event is deleted (doesnt exist/cannot be matched)
|
||||
cancelNotif(req.identifier)
|
||||
}
|
||||
}
|
||||
@@ -234,6 +186,14 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
||||
scheduleEventNotif(event)
|
||||
}
|
||||
}
|
||||
Task {
|
||||
try? await UNUserNotificationCenter.current().setBadgeCount(await getNotifs().count)
|
||||
}
|
||||
print(eventUUIDs.count)
|
||||
print(events.count(where: {!$0.complete && $0.date < .now}))
|
||||
print(events.count(where: {!$0.complete && $0.date > .now}))
|
||||
print(events.count(where: {!$0.complete}))
|
||||
print(events.count(where: {$0.complete}))
|
||||
}
|
||||
|
||||
// save to local and icloud
|
||||
@@ -274,8 +234,31 @@ class EventViewModel: ObservableObject, @unchecked Sendable {
|
||||
saveEvents() //sync with icloud
|
||||
}
|
||||
|
||||
func removeEvent(at index: IndexSet) {
|
||||
events.remove(atOffsets: index)
|
||||
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(_ eventToRemove: Event) {
|
||||
let eventToModify = self.events.firstIndex() { currEvent in
|
||||
currEvent.id == eventToRemove.id
|
||||
}
|
||||
if let eventToModify = eventToModify {
|
||||
self.events.remove(at: eventToModify)
|
||||
}
|
||||
saveEvents() //sync local and icl
|
||||
}
|
||||
|
||||
@@ -343,6 +326,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")
|
||||
@@ -516,6 +507,7 @@ func getBuildID() -> String {
|
||||
return "\(build)"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func getDevice() -> (sf: String, label: String) {
|
||||
#if canImport(UIKit)
|
||||
let asi = ProcessInfo().isiOSAppOnMac
|
||||
@@ -533,57 +525,3 @@ func getDevice() -> (sf: String, label: String) {
|
||||
return (sf: "desktopcomputer", label: "Mac")
|
||||
#endif
|
||||
}
|
||||
|
||||
extension Event: AppEntity {
|
||||
static let defaultQuery = EventQuery()
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
||||
TypeDisplayRepresentation("skdfj")
|
||||
}
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation("eventsss")
|
||||
}
|
||||
}
|
||||
|
||||
struct EventQuery: EntityQuery, DynamicOptionsProvider {
|
||||
typealias Entity = Event
|
||||
@Dependency var vm: EventViewModel
|
||||
func results() async throws -> some ResultsCollection {
|
||||
return vm.events
|
||||
}
|
||||
// func defaultResult() async -> DefaultValue? {
|
||||
// return vm.events[0]
|
||||
// }
|
||||
func entities(for identifiers: [Entity.ID]) async throws -> [Entity] {
|
||||
return vm.events
|
||||
}
|
||||
func suggestedEntities() async throws -> some ResultsCollection {
|
||||
return vm.events //lol cba
|
||||
}
|
||||
}
|
||||
|
||||
struct CompleteEvent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Complete An Event"
|
||||
static var description = IntentDescription("Mark an Event as complete.")
|
||||
|
||||
@Parameter(title: "Event ID")
|
||||
var eventID: String
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
print("s")
|
||||
let viewModel = EventViewModel()
|
||||
print("hip")
|
||||
guard let eventUUID = UUID(uuidString: eventID) else {
|
||||
print(":sdklfajk")
|
||||
return .result()
|
||||
}
|
||||
print("hii")
|
||||
if let eventToModify = viewModel.events.firstIndex(where: { $0.id == eventUUID }) {
|
||||
print("hiii")
|
||||
viewModel.events[eventToModify].complete = true
|
||||
viewModel.saveEvents()
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,40 +18,7 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class SettingsViewModel: ObservableObject {
|
||||
@Published var settings: NFSettings = NFSettings()
|
||||
|
||||
@@ -70,7 +37,7 @@ class SettingsViewModel: ObservableObject {
|
||||
"pink"
|
||||
]
|
||||
|
||||
@Published var device: (sf: String, label: String)
|
||||
@Published var device: (sf: String, label: String) = ("", "")
|
||||
|
||||
init(load: Bool = true) {
|
||||
self.device = getDevice()
|
||||
|
||||
36
Shared/Model/SymbolsPicker/SymbolsLoader.swift
Normal file
36
Shared/Model/SymbolsPicker/SymbolsLoader.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// SymbolsLoader.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 14/06/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class SymbolsLoader: ObservableObject {
|
||||
private var allSymbols: [String] = []
|
||||
|
||||
init() {
|
||||
self.allSymbols = getAllSymbols()
|
||||
}
|
||||
|
||||
func getSymbols(_ searched: String) -> [String] {
|
||||
if searched.isEmpty {
|
||||
return []
|
||||
} 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
|
||||
}
|
||||
}
|
||||
99
Shared/Model/SymbolsPicker/SymbolsPicker.swift
Normal file
99
Shared/Model/SymbolsPicker/SymbolsPicker.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// 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 searchInput.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable().scaledToFit()
|
||||
.frame(width: 30)
|
||||
Text("Start a Search")
|
||||
.font(.title)
|
||||
.bold()
|
||||
}
|
||||
.padding()
|
||||
} else 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(""))
|
||||
}
|
||||
77
Shared/ViewModifiers.swift
Normal file
77
Shared/ViewModifiers.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// ViewModifiers.swift
|
||||
// NearFuture
|
||||
//
|
||||
// Created by neon443 on 13/06/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct hapticHeavy<T: Equatable>: ViewModifier {
|
||||
var trigger: T
|
||||
|
||||
init(trigger: T) {
|
||||
self.trigger = trigger
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onChange(of: trigger) { _ in
|
||||
#if canImport(UIKit)
|
||||
UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct hapticSuccess<T: Equatable>: ViewModifier {
|
||||
var trigger: T
|
||||
|
||||
init(trigger: T) {
|
||||
self.trigger = trigger
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onChange(of: trigger) { _ in
|
||||
#if canImport(UIKit)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct glassButton: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
#if swift(>=6.2)
|
||||
content.buttonStyle(.glass)
|
||||
#else
|
||||
content.buttonStyle(.borderedProminent)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||
.tint(.two)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user