GroupTemplateEditorSheet.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import SwiftUI
  2. /// Editor for playlist grouping template.
  3. /// Lets the user type a custom template or pick a preset.
  4. struct GroupTemplateEditorSheet: View {
  5. let playlist: Playlist
  6. @EnvironmentObject private var theme: AppTheme
  7. @Environment(\.modelContext) private var modelContext
  8. @Environment(\.dismiss) private var dismiss
  9. @State private var template: String = ""
  10. var body: some View {
  11. NavigationStack {
  12. List {
  13. Section {
  14. TextField("Template (empty = no grouping)", text: $template)
  15. .font(.body.monospaced())
  16. .autocorrectionDisabled()
  17. .textInputAutocapitalization(.never)
  18. } header: {
  19. Text("Group Template")
  20. } footer: {
  21. Text("Use placeholders like {Album}, {Artist}, {Date}, etc. You can also use {Year} as an alias for {Date}. Tracks with the same resolved value will be grouped together.")
  22. }
  23. Section("Presets") {
  24. ForEach(GroupTemplateResolver.presets, id: \.template) { preset in
  25. Button {
  26. template = preset.template
  27. } label: {
  28. HStack {
  29. VStack(alignment: .leading, spacing: 2) {
  30. Text(preset.name)
  31. .foregroundStyle(theme.primaryText)
  32. if !preset.template.isEmpty {
  33. Text(preset.template)
  34. .font(.caption.monospaced())
  35. .foregroundStyle(theme.tertiaryText)
  36. }
  37. }
  38. Spacer()
  39. if template == preset.template {
  40. Image(systemName: "checkmark")
  41. .foregroundStyle(theme.accent)
  42. }
  43. }
  44. }
  45. }
  46. }
  47. Section("Available Placeholders") {
  48. ForEach(GroupTemplateResolver.placeholders, id: \.token) { placeholder in
  49. Button {
  50. template += placeholder.token
  51. } label: {
  52. HStack {
  53. Text(placeholder.token)
  54. .font(.body.monospaced())
  55. .foregroundStyle(theme.accent)
  56. Spacer()
  57. Text(placeholder.description)
  58. .font(.caption)
  59. .foregroundStyle(theme.tertiaryText)
  60. }
  61. }
  62. }
  63. }
  64. if !template.isEmpty {
  65. Section("Preview") {
  66. Text("Groups will look like:")
  67. .font(.caption)
  68. .foregroundStyle(theme.tertiaryText)
  69. Text(previewText)
  70. .font(.subheadline.weight(.semibold))
  71. .foregroundStyle(theme.groupHeaderText)
  72. }
  73. }
  74. }
  75. .navigationTitle("Grouping")
  76. .navigationBarTitleDisplayMode(.inline)
  77. .toolbar {
  78. ToolbarItem(placement: .cancellationAction) {
  79. Button("Cancel") { dismiss() }
  80. }
  81. ToolbarItem(placement: .confirmationAction) {
  82. Button("Save") {
  83. playlist.groupTemplate = template
  84. try? modelContext.save()
  85. dismiss()
  86. }
  87. }
  88. }
  89. .onAppear {
  90. template = playlist.groupTemplate
  91. }
  92. }
  93. }
  94. private var previewText: String {
  95. // Show a sample with dummy data
  96. let sample = template
  97. .replacingOccurrences(of: "{Artist}", with: "Raekwon")
  98. .replacingOccurrences(of: "{Album}", with: "Only Built 4 Cuban Linx")
  99. .replacingOccurrences(of: "{Genre}", with: "Hip Hop")
  100. .replacingOccurrences(of: "{Date}", with: "1995")
  101. .replacingOccurrences(of: "{Year}", with: "1995")
  102. .replacingOccurrences(of: "{Folder}", with: "Batch 1")
  103. .replacingOccurrences(of: "{Format}", with: "FLAC")
  104. .replacingOccurrences(of: "{BPM}", with: "90-100 BPM")
  105. .replacingOccurrences(of: "{Key}", with: "Am")
  106. return sample
  107. }
  108. }