Task.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. }
  41. func yaml(_ indent: Int = 0) -> String {
  42. let hdr = String(repeating: " ", count: indent)
  43. let subhdr = hdr + " # "
  44. var rval = hdr + "- [\(status.rawValue)] \(name)\n"
  45. if !notes.isEmpty {
  46. rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
  47. }
  48. return rval
  49. }
  50. enum CodingKeys: CodingKey { case name, notes, status }
  51. required init(from decoder: any Decoder) throws {
  52. let container = try decoder.container(keyedBy: CodingKeys.self)
  53. name = try container.decode(String.self, forKey: .name)
  54. notes = try container.decode(String.self, forKey: .notes)
  55. status = try container.decode(Status.self, forKey: .status)
  56. }
  57. func encode(to encoder: any Encoder) throws {
  58. var container = encoder.container(keyedBy: CodingKeys.self)
  59. try container.encode(name, forKey: .name)
  60. try container.encode(notes, forKey: .notes)
  61. try container.encode(status, forKey: .status)
  62. }
  63. }
  64. @Model
  65. final class Tag: Codable {
  66. var id: String
  67. var task: Task?
  68. init(id: String, parent: Task? = nil) {
  69. self.id = id
  70. self.task = parent
  71. }
  72. func like(_ str: String) -> Bool {
  73. return id.caseInsensitiveCompare(str) == .orderedSame
  74. }
  75. required init(from decoder: any Decoder) throws {
  76. id = try decoder.singleValueContainer().decode(String.self)
  77. }
  78. func encode(to encoder: any Encoder) throws {
  79. var single = encoder.singleValueContainer()
  80. try single.encode(id)
  81. }
  82. }
  83. @Model
  84. final class Task: Codable, Ordered {
  85. var sortOrder: Int = 0
  86. var name: String
  87. var project: Project?
  88. var category: String = ""
  89. @Relationship(deleteRule: .cascade, inverse: \Tag.task)
  90. var tags: [Tag] = []
  91. @Relationship(deleteRule: .cascade, inverse: \SubTask.task)
  92. var subtasks: [SubTask] = []
  93. var notes: String = ""
  94. var status: Status = Status.Todo
  95. init(name: String, parent: Project? = nil) {
  96. self.name = name
  97. self.project = parent
  98. self.category = parent?.category ?? ""
  99. }
  100. func yaml(_ indent: Int = 0) -> String {
  101. let hdr = String(repeating: " ", count: indent)
  102. let subhdr = hdr + " # "
  103. var rval = hdr + "[\(status.rawValue)] \(name) "
  104. rval += "(\(tags.map(\.id).joined(separator: " ")))\n"
  105. if !notes.isEmpty {
  106. rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
  107. }
  108. rval += subtasks.map({ $0.yaml(indent + 1) }).joined()
  109. return rval
  110. }
  111. enum CodingKeys: CodingKey { case name, category, tags, subtasks, notes, status }
  112. required init(from decoder: any Decoder) throws {
  113. let container = try decoder.container(keyedBy: CodingKeys.self)
  114. name = try container.decode(String.self, forKey: .name)
  115. category = try container.decode(String.self, forKey: .category)
  116. tags = try container.decode([Tag].self, forKey: .tags)
  117. subtasks = try container.decode([SubTask].self, forKey: .subtasks)
  118. notes = try container.decode(String.self, forKey: .notes)
  119. status = try container.decode(Status.self, forKey: .status)
  120. tags.forEach({ $0.task = self })
  121. subtasks.forEach({ $0.task = self })
  122. }
  123. func encode(to encoder: any Encoder) throws {
  124. var container = encoder.container(keyedBy: CodingKeys.self)
  125. try container.encode(name, forKey: .name)
  126. try container.encode(category, forKey: .category)
  127. try container.encode(tags, forKey: .tags)
  128. try container.encode(subtasks, forKey: .subtasks)
  129. try container.encode(notes, forKey: .notes)
  130. try container.encode(status, forKey: .status)
  131. }
  132. }