import SwiftUI /// Editor for per-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 { VStack(alignment: .leading, spacing: 16) { // Title HStack { Text("Track Grouping") .font(.title3.bold()) Spacer() Button("Cancel") { dismiss() } .keyboardShortcut(.cancelAction) Button("Save") { playlist.groupTemplate = template try? modelContext.save() dismiss() } .keyboardShortcut(.defaultAction) } // Template text field VStack(alignment: .leading, spacing: 4) { Text("Group Template") .font(.caption.bold()) .foregroundStyle(.secondary) TextField("e.g. {Album} ({Year})", text: $template) .font(.body.monospaced()) .textFieldStyle(.roundedBorder) Text("Use placeholders like {Album}, {Artist}, {Year}, etc. Tracks with the same resolved value will be grouped together. Empty = no grouping.") .font(.caption) .foregroundStyle(.secondary) } Divider() // Presets VStack(alignment: .leading, spacing: 6) { Text("Presets") .font(.caption.bold()) .foregroundStyle(.secondary) LazyVGrid(columns: [GridItem(.adaptive(minimum: 140))], spacing: 4) { ForEach(GroupTemplateResolver.presets, id: \.template) { preset in Button { template = preset.template } label: { HStack(spacing: 6) { VStack(alignment: .leading, spacing: 1) { Text(preset.name) .font(.system(size: 12, weight: .medium)) if !preset.template.isEmpty { Text(preset.template) .font(.system(size: 10, design: .monospaced)) .foregroundStyle(.secondary) } } Spacer(minLength: 0) if template == preset.template { Image(systemName: "checkmark") .foregroundStyle(.green) .font(.system(size: 11)) } } .padding(6) .background(template == preset.template ? Color.accentColor.opacity(0.1) : Color.primary.opacity(0.03)) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(template == preset.template ? Color.accentColor : Color.gray.opacity(0.2), lineWidth: 1) ) } .buttonStyle(.plain) } } } Divider() // Placeholders (tap to insert) VStack(alignment: .leading, spacing: 6) { Text("Placeholders (click to insert)") .font(.caption.bold()) .foregroundStyle(.secondary) HStack(spacing: 6) { ForEach(GroupTemplateResolver.placeholders, id: \.token) { placeholder in Button { template += placeholder.token } label: { Text(placeholder.token) .font(.system(size: 11, design: .monospaced)) .padding(.horizontal, 6) .padding(.vertical, 3) .background(Color.accentColor.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 4)) } .buttonStyle(.plain) .help(placeholder.description) } } } // Preview if !template.isEmpty { Divider() VStack(alignment: .leading, spacing: 4) { Text("Preview") .font(.caption.bold()) .foregroundStyle(.secondary) Text(previewText) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(theme.primaryText) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.primary.opacity(0.04)) .clipShape(RoundedRectangle(cornerRadius: 6)) } } Spacer() } .padding(20) .frame(width: 560, height: 440) .onAppear { template = playlist.groupTemplate } } private var previewText: String { let sample = template .replacingOccurrences(of: "{Artist}", with: "Raimundo Fagner") .replacingOccurrences(of: "{Album}", with: "Raimundo Fagner (1973)") .replacingOccurrences(of: "{Genre}", with: "MPB") .replacingOccurrences(of: "{Year}", with: "2026") .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 } }