| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- 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
- }
- }
|