Task.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. //
  2. // Task.swift
  3. // Todos
  4. //
  5. // Created by Sam Jaffe on 2/28/26.
  6. //
  7. import Foundation
  8. import SwiftData
  9. enum Status: String, CaseIterable, Identifiable, Codable {
  10. case Todo = " "
  11. case Complete = "V"
  12. case InProgress = "C"
  13. case Hiatus = "H"
  14. case Waiting = "R"
  15. var id: Self { self }
  16. var description: String { self.rawValue }
  17. var isStrong: Bool {
  18. self == .Complete || self == .Hiatus || self == .Waiting
  19. }
  20. var label: String {
  21. switch self {
  22. case .Todo: return "square.and.pencil"
  23. case .Complete: return "checkmark"
  24. case .InProgress: return "ellipsis.circle"
  25. case .Hiatus: return "clock.badge.questionmark"
  26. case .Waiting: return "airplane.circle"
  27. }
  28. }
  29. }
  30. @Model
  31. final class SubTask: Codable, Ordered {
  32. var sortOrder: Int = 0
  33. var name: String
  34. var task: Task?
  35. var notes: String = ""
  36. var status: Status = Status.Todo
  37. init(name: String, parent: Task? = nil) {
  38. self.name = name
  39. self.task = parent
  40. self.sortOrder = parent?.subtasks.count ?? 0
  41. }
  42. func yaml(_ indent: Int = 0) -> String {
  43. let hdr = String(repeating: " ", count: indent)
  44. let subhdr = hdr + " # "
  45. var rval = hdr + "- [\(status.rawValue)] \(name)\n"
  46. if !notes.isEmpty {
  47. rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
  48. }
  49. return rval
  50. }
  51. enum CodingKeys: CodingKey { case name, notes, status }
  52. required init(from decoder: any Decoder) throws {
  53. let container = try decoder.container(keyedBy: CodingKeys.self)
  54. name = try container.decode(String.self, forKey: .name)
  55. notes = try container.decode(String.self, forKey: .notes)
  56. status = try container.decode(Status.self, forKey: .status)
  57. }
  58. func encode(to encoder: any Encoder) throws {
  59. var container = encoder.container(keyedBy: CodingKeys.self)
  60. try container.encode(name, forKey: .name)
  61. try container.encode(notes, forKey: .notes)
  62. try container.encode(status, forKey: .status)
  63. }
  64. }
  65. @Model
  66. final class Tag: Codable {
  67. var id: String
  68. var task: Task?
  69. init(id: String, parent: Task? = nil) {
  70. self.id = id
  71. self.task = parent
  72. }
  73. func like(_ str: String) -> Bool {
  74. return id.caseInsensitiveCompare(str) == .orderedSame
  75. }
  76. required init(from decoder: any Decoder) throws {
  77. id = try decoder.singleValueContainer().decode(String.self)
  78. }
  79. func encode(to encoder: any Encoder) throws {
  80. var single = encoder.singleValueContainer()
  81. try single.encode(id)
  82. }
  83. }
  84. @Model
  85. final class Task: Codable, Ordered {
  86. var sortOrder: Int = 0
  87. var name: String
  88. var project: Project?
  89. var category: String = ""
  90. @Relationship(deleteRule: .cascade, inverse: \Tag.task)
  91. var tags: [Tag] = []
  92. @Relationship(deleteRule: .cascade, inverse: \SubTask.task)
  93. var subtasks: [SubTask] = []
  94. var notes: String = ""
  95. var status: Status = Status.Todo
  96. init(name: String, parent: Project? = nil) {
  97. self.name = name
  98. self.project = parent
  99. self.category = parent?.category ?? ""
  100. self.sortOrder = parent?.tasks.count ?? 0
  101. }
  102. func yaml(_ indent: Int = 0) -> String {
  103. let hdr = String(repeating: " ", count: indent)
  104. let subhdr = hdr + " # "
  105. var rval = hdr + "[\(status.rawValue)] \(name) "
  106. rval += "(\(tags.map(\.id).joined(separator: " ")))\n"
  107. if !notes.isEmpty {
  108. rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
  109. }
  110. rval += subtasks.map({ $0.yaml(indent + 1) }).joined()
  111. return rval
  112. }
  113. enum CodingKeys: CodingKey { case name, category, tags, subtasks, notes, status }
  114. required init(from decoder: any Decoder) throws {
  115. let container = try decoder.container(keyedBy: CodingKeys.self)
  116. name = try container.decode(String.self, forKey: .name)
  117. category = try container.decode(String.self, forKey: .category)
  118. tags = try container.decode([Tag].self, forKey: .tags)
  119. subtasks = try container.decode([SubTask].self, forKey: .subtasks)
  120. notes = try container.decode(String.self, forKey: .notes)
  121. status = try container.decode(Status.self, forKey: .status)
  122. tags.forEach({ $0.task = self })
  123. subtasks.forEach({ $0.task = self })
  124. }
  125. func encode(to encoder: any Encoder) throws {
  126. var container = encoder.container(keyedBy: CodingKeys.self)
  127. try container.encode(name, forKey: .name)
  128. try container.encode(category, forKey: .category)
  129. try container.encode(tags, forKey: .tags)
  130. try container.encode(subtasks, forKey: .subtasks)
  131. try container.encode(notes, forKey: .notes)
  132. try container.encode(status, forKey: .status)
  133. }
  134. }