Files
NearFuture/Shared/Model/Event.swift
neon443 62318c5ddc mac ui
2025-05-21 18:10:22 +01:00

715 lines
17 KiB
Swift

//
// Item.swift
// NearFuture
//
// Created by neon443 on 24/12/2024.
//
import Foundation
import SwiftData
import SwiftUI
import WidgetKit
import UserNotifications
import AppIntents
#if canImport(AppKit)
import AppKit
import IOKit
#endif
//@Model
//final class Item {
// var timestamp: Date
//
// init(timestamp: Date) {
// self.timestamp = timestamp
// }
//}
struct Event: Identifiable, Codable, Equatable, Animatable {
var id = UUID()
var name: String
var complete: Bool
var completeDesc: String
var symbol: String
var color: ColorCodable
var notes: String
var date: Date
var recurrence: RecurrenceType
enum RecurrenceType: String, Codable, CaseIterable {
case none, daily, weekly, monthly, yearly
}
}
struct ColorCodable: Codable, Equatable {
init(_ color: Color) {
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())
let startOfDayEvent = calendar.startOfDay(for: eventDate)
let components = calendar.dateComponents([.day], from: startOfDayNow, to: startOfDayEvent)
guard let days = components.day else { return ("N/A", "N/A") }
guard days != 0 else { return ("Today", "Today") }
if days < 0 {
//past
return (
"\(-days) day\(plu(days)) ago",
"\(days)d"
)
} else {
//future
return (
"\(days) day\(plu(days))",
"\(days)d"
)
}
}
struct NFSettings: Codable, Equatable {
var showCompletedInHome: Bool
var tint: ColorCodable
var showWhatsNew: Bool
var prevAppVersion: String
}
class AccentIcon {
#if canImport(UIKit)
var icon: UIImage
#elseif canImport(AppKit)
var icon: NSImage
#endif
var color: Color
var name: String
init(_ colorName: String) {
#if canImport(UIKit)
self.icon = UIImage(named: "AppIcon")!
self.color = Color(uiColor: UIColor(named: "uiColors/\(colorName)")!)
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: "AppIcon")
self.color = Color(nsColor: NSColor(named: "uiColors/\(colorName)")!)
#endif
self.name = colorName
if colorName != "orange" {
setSelfIcon(to: colorName)
}
}
func setSelfIcon(to name: String) {
#if canImport(UIKit)
self.icon = UIImage(named: name)!
#elseif canImport(AppKit)
self.icon = NSImage(imageLiteralResourceName: name)
#endif
}
}
class SettingsViewModel: ObservableObject {
#if canImport(UIKit)
@Published var settings: Settings = Settings(
showCompletedInHome: false,
tint: ColorCodable(uiColor: UIColor(named: "AccentColor")!),
showWhatsNew: true,
prevAppVersion: getVersion()+getBuildID()
)
#elseif canImport(AppKit)
@Published var settings: NFSettings = NFSettings(
showCompletedInHome: false,
tint: ColorCodable(nsColor: NSColor(named: "AccentColor")!),
showWhatsNew: true,
prevAppVersion: getVersion()+getBuildID()
)
#endif
@Published var notifsGranted: Bool = false
@Published var colorChoices: [AccentIcon] = []
let accentChoices: [String] = [
"red",
"orange",
"yellow",
"green",
"blue",
"bloo",
"purple",
"pink"
]
@Published var device: (sf: String, label: String)
init(load: Bool = true) {
self.device = getDevice()
if load {
loadSettings()
Task {
let requestResult = await requestNotifs()
await MainActor.run {
self.notifsGranted = requestResult
}
}
}
}
func changeTint(to: String) {
#if canImport(UIKit)
if let uicolor = UIColor(named: "uiColors/\(to)") {
self.settings.tint = ColorCodable(uiColor: uicolor)
saveSettings()
}
#elseif canImport(AppKit)
if let nscolor = NSColor(named: "uiColors/\(to)") {
self.settings.tint = ColorCodable(nsColor: nscolor)
}
#endif
}
let appGroupSettingsStore = UserDefaults(suiteName: "group.NearFuture") ?? UserDefaults.standard
let icSettStore = NSUbiquitousKeyValueStore.default
func loadSettings() {
let decoder = JSONDecoder()
if let icSettings = icSettStore.data(forKey: "settings") {
if let decodedSetts = try? decoder.decode(NFSettings.self, from: icSettings) {
self.settings = decodedSetts
}
} else if let savedData = appGroupSettingsStore.data(forKey: "settings") {
if let decodedSetts = try? decoder.decode(NFSettings.self, from: savedData) {
self.settings = decodedSetts
}
}
if self.settings.prevAppVersion != getVersion()+getBuildID() {
self.settings.showWhatsNew = true
}
}
func saveSettings() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(settings) {
appGroupSettingsStore.set(encoded, forKey: "settings")
icSettStore.set(encoded, forKey: "settings")
icSettStore.synchronize()
loadSettings()
}
}
}
class EventViewModel: ObservableObject, @unchecked Sendable {
@Published var events: [Event] = []
@Published var icloudData: [Event] = []
public let template: Event = Event(
name: "",
complete: false,
completeDesc: "",
symbol: "star",
color: ColorCodable(randomColor()),
notes: "",
date: Date(),
recurrence: .none
)
@Published var editableTemplate: Event
@Published var example: Event = Event(
name: "event",
complete: false,
completeDesc: "dofajiof",
symbol: "star",
color: ColorCodable(.orange),
notes: "lksdjfakdflkasjlkjl",
date: Date(),
recurrence: .daily
)
@Published var lastSync: Date? = nil
@Published var icloudEventCount: Int = 0
@Published var localEventCount: Int = 0
@Published var syncStatus: String = "Not Synced"
init(load: Bool = true) {
self.editableTemplate = template
if load {
loadEvents()
}
}
//appgroup or regular userdefaults
let appGroupUserDefaults = UserDefaults(suiteName: "group.NearFuture") ?? UserDefaults.standard
//icloud store
let icloudStore = NSUbiquitousKeyValueStore.default
// load from icloud or local
func loadEvents() {
//load icloud 1st
if let icData = icloudStore.data(forKey: "events") {
let decoder = JSONDecoder()
if let decodedIcEvents = try? decoder.decode([Event].self, from: icData) {
self.icloudData = decodedIcEvents
self.events = decodedIcEvents
}
}
if events.isEmpty, let savedData = appGroupUserDefaults.data(forKey: "events") {
let decoder = JSONDecoder()
if let decodedEvents = try? decoder.decode([Event].self, from: savedData) {
self.events = decodedEvents
}
}
updateSyncStatus()
self.events.sort() {$0.date < $1.date}
}
func getNotifs() async -> [UNNotificationRequest] {
return await UNUserNotificationCenter.current().pendingNotificationRequests()
}
func checkPendingNotifs(_ pending: [UNNotificationRequest]) {
var eventUUIDs = events.map({$0.id.uuidString})
for req in pending {
//match the notif to an event
if let index = events.firstIndex(where: {$0.id.uuidString == req.identifier}) {
if let remove = eventUUIDs.firstIndex(where: {$0 == req.identifier}) {
eventUUIDs.remove(at: remove)
}
let components = getDateComponents(events[index].date)
//check the notif matches event details
if req.content.title == events[index].name,
req.content.subtitle == events[index].notes,
req.trigger == UNCalendarNotificationTrigger(dateMatching: components, repeats: false) {
//if it does, make sure the notif delets if u complete the veent
if events[index].complete {
cancelNotif(req.identifier)
}
} else {
cancelNotif(req.identifier)
scheduleEventNotif(events[index])
}
} else {
//cancel if the event is deleted
cancelNotif(req.identifier)
}
}
for uuid in eventUUIDs {
if let event = events.first(where: {$0.id.uuidString == uuid}) {
scheduleEventNotif(event)
}
}
}
// save to local and icloud
func saveEvents() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(events) {
appGroupUserDefaults.set(encoded, forKey: "events")
//sync
icloudStore.set(encoded, forKey: "events")
icloudStore.synchronize()
updateSyncStatus()
loadEvents()
Task {
await checkPendingNotifs(getNotifs())
}
WidgetCenter.shared.reloadAllTimelines()//reload all widgets when saving events
objectWillChange.send()
}
}
private func updateSyncStatus() {
lastSync = Date()
icloudEventCount = icloudData.count
localEventCount = events.count
if icloudEventCount == localEventCount {
syncStatus = "Successful"
} else {
syncStatus = "Pending"
}
}
func addEvent(newEvent: Event) {
events.append(newEvent)
scheduleEventNotif(newEvent)
saveEvents() //sync with icloud
}
func removeEvent(at index: IndexSet) {
events.remove(atOffsets: index)
saveEvents() //sync local and icl
}
func hasUbiquitousKeyValueStore() -> Bool {
let icloud = NSUbiquitousKeyValueStore.default
let key = "com.neon443.NearFuture.testkey"
let value = "testValue"
icloud.set(value, forKey: key)
icloud.synchronize()
if icloud.string(forKey: key) != nil {
icloud.removeObject(forKey: key)
icloud.synchronize()
return true
} else {
print("!has UbiquitousKeyValueStore")
icloud.removeObject(forKey: key)
icloud.synchronize()
return false
}
}
func sync() {
NSUbiquitousKeyValueStore.default.synchronize()
loadEvents()
}
func replaceLocalWithiCloudData() {
icloudStore.synchronize()
self.events = self.icloudData
saveEvents()
}
func replaceiCloudWithLocalData() {
icloudStore.synchronize()
self.icloudData = self.events
saveEvents()
}
func exportEvents() -> String {
let encoder = JSONEncoder()
if let json = try? encoder.encode(self.events) {
return "\(json.base64EncodedString())"
}
return ""
}
func importEvents(_ imported: String) throws {
guard let data = Data(base64Encoded: imported) else {
throw importError.invalidB64
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([Event].self, from: data)
self.events = decoded
saveEvents()
} catch {
throw error
}
}
//MARK: Danger Zone
func dangerClearLocalData() {
UserDefaults.standard.removeObject(forKey: "events")
appGroupUserDefaults.removeObject(forKey: "events")
events.removeAll()
cancelAllNotifs()
updateSyncStatus()
}
func dangerCleariCloudData() {
icloudStore.removeObject(forKey: "events")
icloudStore.synchronize()
icloudData.removeAll()
cancelAllNotifs()
updateSyncStatus()
}
func dangerResetLocalData() {
let userDFDict = UserDefaults.standard.dictionaryRepresentation()
for key in userDFDict.keys {
UserDefaults.standard.removeObject(forKey: key)
}
let appGUSDDict = appGroupUserDefaults.dictionaryRepresentation()
for key in appGUSDDict.keys {
appGroupUserDefaults.removeObject(forKey: key)
}
events.removeAll()
cancelAllNotifs()
updateSyncStatus()
}
func dangerResetiCloud() {
let icloudDict = icloudStore.dictionaryRepresentation
for key in icloudDict.keys {
icloudStore.removeObject(forKey: key)
}
icloudStore.synchronize()
icloudData.removeAll()
cancelAllNotifs()
updateSyncStatus()
}
}
class dummyEventViewModel: EventViewModel, @unchecked Sendable{
var template2: Event
override init(load: Bool = false) {
self.template2 = Event(
name: "template2",
complete: false,
completeDesc: "",
symbol: "hammer",
color: ColorCodable(randomColor()),
notes: "notes",
date: Date(),
recurrence: .none
)
super.init(load: false)
self.events = [self.example, self.template, self.template2]
self.events[0].complete.toggle()
}
}
class dummySettingsViewModel: SettingsViewModel {
override init(load: Bool = false) {
super.init(load: false)
}
}
func describeOccurrence(date: Date, recurrence: Event.RecurrenceType) -> String {
let dateString = date.formatted(date: .long, time: .omitted)
let recurrenceDescription: String
switch recurrence {
case .none:
recurrenceDescription = "Occurs once on"
case .daily:
recurrenceDescription = "Repeats every day from"
case .weekly:
recurrenceDescription = "Repeats every week from"
case .monthly:
recurrenceDescription = "Repeats every month from"
case .yearly:
recurrenceDescription = "Repeats every year from"
}
return "\(recurrenceDescription) \(dateString)"
}
func randomRainbowColor() -> Color {
return [
Color.red,
Color.orange,
Color.yellow,
Color.green,
Color.blue,
Color.indigo,
Color.purple
].randomElement()!
}
func randomColor() -> Color {
let r = Double.random(in: 0...1)
let g = Double.random(in: 0...1)
let b = Double.random(in: 0...1)
return Color(red: r, green: g, blue: b)
}
func plu(_ inp: Int) -> String {
var input = inp
if inp < 0 { input.negate() }
return "\(input == 1 ? "" : "s")"
}
public enum importError: Error {
case invalidB64
}
func requestNotifs() async -> Bool {
let result = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .carPlay, .sound])
return result ?? false
}
func scheduleNotif(title: String, sub: String, date: Date, id: String = UUID().uuidString) {
let content = UNMutableNotificationContent()
content.title = title
content.subtitle = sub
content.sound = .default
let identifier = id
let dateComponents = getDateComponents(date)
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
func scheduleEventNotif(_ event: Event) {
scheduleNotif(
title: event.name,
sub: event.notes,
date: event.date,
id: event.id.uuidString
)
}
func getDateComponents(_ date: Date) -> DateComponents {
return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date)
}
func cancelNotif(_ id: String) {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
}
func cancelAllNotifs() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
func getVersion() -> String {
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] else {
fatalError("no bundle id wtf lol")
}
return "\(version)"
}
func getBuildID() -> String {
guard let build = Bundle.main.infoDictionary?["CFBundleVersion"] else {
fatalError("wtf did u do w the build number")
}
return "\(build)"
}
func getDevice() -> (sf: String, label: String) {
#if canImport(UIKit)
let asi = ProcessInfo().isiOSAppOnMac
let model = UIDevice().model
if asi {
return (sf: "laptopcomputer", label: "Computer")
} else if model == "iPhone" {
return (sf: model.lowercased(), label: model)
} else if model == "iPad" {
return (sf: model.lowercased(), label: model)
}
return (sf: "iphone", label: "iPhone")
#elseif canImport(AppKit)
return (sf: "", label: "")
#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()
}
}