// // PlaylistFormSheet.swift // Yattee // // Reusable form sheet for creating and editing playlists. // import SwiftUI struct PlaylistFormSheet: View { enum Mode: Equatable { case create case edit(LocalPlaylist) static func == (lhs: Mode, rhs: Mode) -> Bool { switch (lhs, rhs) { case (.create, .create): return true case (.edit(let lhsPlaylist), .edit(let rhsPlaylist)): return lhsPlaylist.id == rhsPlaylist.id default: return false } } } let mode: Mode let onSave: (String, String?) -> Void @Environment(\.dismiss) private var dismiss @State private var title: String = "" @State private var descriptionText: String = "" private let maxDescriptionLength = 1000 private var isEditing: Bool { if case .edit = mode { return true } return false } private var navigationTitle: String { isEditing ? String(localized: "playlist.edit") : String(localized: "playlist.new") } private var saveButtonTitle: String { isEditing ? String(localized: "common.save") : String(localized: "common.create") } private var canSave: Bool { !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } var body: some View { NavigationStack { #if os(tvOS) tvOSContent #else formContent .navigationTitle(navigationTitle) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "common.cancel")) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(saveButtonTitle) { save() } .disabled(!canSave) } } #endif } .onAppear { if case .edit(let playlist) = mode { title = playlist.title descriptionText = playlist.playlistDescription ?? "" } } #if os(iOS) .presentationDetents([.medium]) #endif } // MARK: - Form Content #if !os(tvOS) private var formContent: some View { Form { Section { TextField(String(localized: "playlist.name"), text: $title) #if os(iOS) .textInputAutocapitalization(.sentences) #endif } header: { Text(String(localized: "playlist.name")) } Section { TextEditor(text: $descriptionText) .frame(minHeight: 100) .onChange(of: descriptionText) { _, newValue in if newValue.count > maxDescriptionLength { descriptionText = String(newValue.prefix(maxDescriptionLength)) } } } header: { Text(String(localized: "playlist.description")) } footer: { HStack { Text(String(localized: "playlist.description.optional")) Spacer() Text("\(descriptionText.count)/\(maxDescriptionLength)") .monospacedDigit() } .foregroundStyle(.secondary) } } #if os(iOS) .scrollDismissesKeyboard(.interactively) #endif } #endif // MARK: - tvOS Content #if os(tvOS) private var tvOSContent: some View { VStack(spacing: 0) { // Header HStack { Button(String(localized: "common.cancel")) { dismiss() } .buttonStyle(TVToolbarButtonStyle()) Spacer() Text(navigationTitle) .font(.title2) .fontWeight(.semibold) Spacer() Button(saveButtonTitle) { save() } .buttonStyle(TVToolbarButtonStyle()) .disabled(!canSave) } .padding(.horizontal, 48) .padding(.vertical, 24) // Form Form { Section { TVSettingsTextField( title: String(localized: "playlist.name"), text: $title ) } header: { Text(String(localized: "playlist.name")) } Section { TVSettingsTextField( title: String(localized: "playlist.description.placeholder"), text: $descriptionText ) } header: { Text(String(localized: "playlist.description")) } footer: { HStack { Text(String(localized: "playlist.description.optional")) Spacer() Text("\(descriptionText.count)/\(maxDescriptionLength)") .monospacedDigit() } .foregroundStyle(.secondary) } } } } #endif // MARK: - Actions private func save() { let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedDescription = descriptionText.trimmingCharacters(in: .whitespacesAndNewlines) onSave(trimmedTitle, trimmedDescription.isEmpty ? nil : trimmedDescription) dismiss() } } // MARK: - Preview #Preview("Create") { PlaylistFormSheet(mode: .create) { _, _ in } }