mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
210
Yattee/Views/Playlist/PlaylistFormSheet.swift
Normal file
210
Yattee/Views/Playlist/PlaylistFormSheet.swift
Normal file
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user