ContentView.swift 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. //
  2. // ContentView.swift
  3. // Todos
  4. //
  5. // Created by Sam Jaffe on 2/28/26.
  6. //
  7. import SwiftUI
  8. import SwiftData
  9. struct ContentView: View {
  10. @Environment(\.modelContext) private var modelContext
  11. @AppStorage(UserDefaultsKeys.WeekStart) private var weekStart = Date()
  12. let inPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
  13. @Query(sort: \Project.sortOrder) private var items: [Project]
  14. @State private var selection: Project?
  15. var body: some View {
  16. NavigationSplitView {
  17. List(selection: $selection) {
  18. ForEach(items, id: \.self) { item in
  19. NavigationLink(value: item) {
  20. Text(item.name)
  21. } .contextMenu {
  22. Button {
  23. deleteItem(item: item)
  24. } label: {
  25. Label("Delete", systemImage: "trash")
  26. }
  27. }
  28. }
  29. .onMove(perform: reOrder)
  30. .onDelete(perform: deleteItems)
  31. }
  32. .navigationSplitViewColumnWidth(min: 180, ideal: 200)
  33. .toolbar {
  34. ToolbarItem {
  35. Button(action: addItem) {
  36. Label("New Project", systemImage: "plus")
  37. }
  38. }
  39. }
  40. } detail: {
  41. if let selection = selection {
  42. ProjectPanelView(item: selection)
  43. } else {
  44. let header = items.isEmpty ? "Create a New Project" : "Select a project from the sidebar"
  45. ContentUnavailableView(header, systemImage: "doc.text.image.fill")
  46. }
  47. }
  48. .onAppear(perform: autosave)
  49. }
  50. private func addItem() {
  51. withAnimation {
  52. let newItem = Project(timestamp: Date())
  53. modelContext.insert(newItem)
  54. }
  55. }
  56. private func reOrder(fromOffsets: IndexSet, toOffset: Int) {
  57. var tmp = items.sorted(by: { $0.sortOrder < $1.sortOrder })
  58. tmp.move(fromOffsets: fromOffsets, toOffset: toOffset)
  59. for (index, item) in tmp.enumerated() {
  60. item.sortOrder = index
  61. }
  62. try? self.modelContext.save()
  63. }
  64. private func autosave() {
  65. if inPreview {
  66. // This isn't great, but we shouldn't be running this in a preview
  67. // environment in the first place, so w/e.
  68. return
  69. }
  70. let now = Date()
  71. let sunday = Calendar.current.nextDate(after: weekStart,
  72. matching: DateComponents(weekday: 1),
  73. matchingPolicy: .nextTime)
  74. if now <= sunday! {
  75. return
  76. }
  77. let ymd = weekStart.formatted(.iso8601.year().month().day())
  78. SaveController.save(items, toUrl: SaveController.filename(date: ymd))
  79. weekStart = now
  80. cleanup()
  81. }
  82. private func cleanup() {
  83. for item in items {
  84. item.tasks.removeAll(where: { $0.status == .Complete })
  85. for task in item.tasks {
  86. if task.status == .InProgress {
  87. task.status = .Todo
  88. }
  89. task.subtasks.removeAll(where: { $0.status == .Complete })
  90. for subtask in task.subtasks where subtask.status == .InProgress {
  91. subtask.status = .Todo
  92. }
  93. }
  94. }
  95. }
  96. private func deleteItem(item: Project) {
  97. if let selection = selection, selection == item {
  98. self.selection = nil
  99. }
  100. withAnimation {
  101. modelContext.delete(item)
  102. }
  103. }
  104. private func deleteItems(offsets: IndexSet) {
  105. if let selection = selection, offsets.contains(where: { items[$0] == selection }) {
  106. self.selection = nil
  107. }
  108. withAnimation {
  109. for index in offsets {
  110. modelContext.delete(items[index])
  111. }
  112. }
  113. }
  114. }
  115. #Preview {
  116. ContentView()
  117. .modelContainer(for: Project.self, inMemory: true)
  118. }