import SwiftUI /// Editor for playlist grouping template. /// Lets the user type a custom template or pick a preset. struct GroupTemplateEditorSheet: View { let playlist: Playlist @EnvironmentObject private var theme: AppTheme @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss @State private var template: String = "" var body: some View { NavigationStack { List { Section { TextField("Template (empty = no grouping)", text: $template) .font(.body.monospaced()) .autocorrectionDisabled() .textInputAutocapitalization(.never) } header: { Text("Group Template") } footer: { 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.") } Section("Presets") { ForEach(GroupTemplateResolver.presets, id: \.template) { preset in Button { template = preset.template } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(preset.name) .foregroundStyle(theme.primaryText) if !preset.template.isEmpty { Text(preset.template) .font(.caption.monospaced()) .foregroundStyle(theme.tertiaryText) } } Spacer() if template == preset.template { Image(systemName: "checkmark") .foregroundStyle(theme.accent) } } } } } Section("Available Placeholders") { ForEach(GroupTemplateResolver.placeholders, id: \.token) { placeholder in Button { template += placeholder.token } label: { HStack { Text(placeholder.token) .font(.body.monospaced()) .foregroundStyle(theme.accent) Spacer() Text(placeholder.description) .font(.caption) .foregroundStyle(theme.tertiaryText) } } } } if !template.isEmpty { Section("Preview") { Text("Groups will look like:") .font(.caption) .foregroundStyle(theme.tertiaryText) Text(previewText) .font(.subheadline.weight(.semibold)) .foregroundStyle(theme.groupHeaderText) } } } .navigationTitle("Grouping") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { playlist.groupTemplate = template try? modelContext.save() dismiss() } } } .onAppear { template = playlist.groupTemplate } } } private var previewText: String { // Show a sample with dummy data let sample = template .replacingOccurrences(of: "{Artist}", with: "Raekwon") .replacingOccurrences(of: "{Album}", with: "Only Built 4 Cuban Linx") .replacingOccurrences(of: "{Genre}", with: "Hip Hop") .replacingOccurrences(of: "{Date}", with: "1995") .replacingOccurrences(of: "{Year}", with: "1995") .replacingOccurrences(of: "{Folder}", with: "Batch 1") .replacingOccurrences(of: "{Format}", with: "FLAC") .replacingOccurrences(of: "{BPM}", with: "90-100 BPM") .replacingOccurrences(of: "{Key}", with: "Am") return sample } }