Triton/Packages/Pics/Sources/Pics/Views/Upload/UploadView.swift
Otavio Cordeiro ee174ab836 Add SwiftLint
2026-01-14 12:30:53 -03:00

306 lines
8.6 KiB
Swift

import DesignSystem
import FoundationExtensions
import PhotosUI
import PicsPersistenceService
import SwiftData
import SwiftUI
struct UploadView: View {
// MARK: - Properties
@State private var selectedItem: PhotosPickerItem?
@State private var viewModel: UploadViewModel
@Environment(\.dismiss) private var dismiss
@Query(SomeTag.fetchDescriptor()) private var existingTags: [SomeTag]
// MARK: - Lifecycle
init(
viewModel: UploadViewModel
) {
self.viewModel = viewModel
}
// MARK: - Public
var body: some View {
makeContentView()
.toolbar {
makeToolbarContent()
}
.navigationTitle("")
.onChange(of: viewModel.shouldDismiss) { _, shouldDismiss in
if shouldDismiss {
dismiss()
}
}
.padding()
}
// MARK: - Private
private func handleDrop(providers: [NSItemProvider]) -> Bool {
Task {
await viewModel.handleImageDrop(providers: providers)
}
return true
}
@ViewBuilder
private func makeContentView() -> some View {
VStack {
HStack(alignment: .top) {
makeSidebarView()
.frame(width: 200)
makeEditorView()
}
makeVisibilityView()
}
}
@ViewBuilder
private func makeSidebarView() -> some View {
VStack {
if viewModel.imageData != nil {
makePictureView()
makeRemoveButtonView()
} else {
ZStack {
makeDropZoneBorder()
makeDropZoneContentView()
}
.onDrop(of: [.image], isTargeted: $viewModel.isDragging) { providers -> Bool in
handleDrop(providers: providers)
}
}
}
}
@ViewBuilder
private func makeDropZoneBorder() -> some View {
RoundedRectangle(cornerRadius: 8)
.fill(
AnyShapeStyle(
viewModel.isDragging ? Color.accentColor.opacity(0.3) : .secondary.opacity(0.05)
)
)
.stroke(Color.accentColor, lineWidth: 1.0)
.opacity(0.3)
.frame(minHeight: 200)
}
@ViewBuilder
private func makeDropZoneContentView() -> some View {
VStack(spacing: 12) {
Image(systemName: viewModel.dropZoneImageName)
.font(.system(size: 48))
.foregroundStyle(viewModel.isDragging ? Color.accentColor : .secondary)
if !viewModel.isDragging {
Text("Drag your picture here or click the button to select one from your Photo Library")
.multilineTextAlignment(.center)
.padding(.horizontal)
makePicturePickerView()
}
}
.padding(.vertical, 16)
}
@ViewBuilder
private func makePictureView() -> some View {
if let cgImage = viewModel.imageData?.cgImage {
Image(cgImage, scale: 1.0, label: Text("Selected Image"))
.resizable()
.scaledToFit()
.clipped()
.clipShape(.rect(cornerRadius: 8))
}
}
@ViewBuilder
private func makePicturePickerView() -> some View {
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()
) {
Text("Select from Library")
}
.help("Choose an image from your photo library")
.onChange(of: selectedItem) {
Task {
viewModel.imageData = try? await selectedItem?.loadTransferable(type: Data.self)
}
}
.buttonStyle(.borderedProminent)
}
@ViewBuilder
private func makeRemoveButtonView() -> some View {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.imageData = nil
selectedItem = nil
}
} label: {
Label("Remove", systemImage: "trash")
}
.help("Remove selected image")
.buttonStyle(.bordered)
}
@ViewBuilder
private func makeEditorView() -> some View {
VStack {
PlaceholderTextEditor(
placeholder: "Caption",
text: $viewModel.caption,
help: "Add a caption for your image"
)
PlaceholderTextEditor(
placeholder: "Alt text",
text: $viewModel.altText,
help: "Add descriptive alt text for accessibility"
)
makeTagsView()
}
}
@ViewBuilder
private func makeTagsView() -> some View {
VStack(alignment: .leading) {
makeTagInputView()
makeTagSuggestionsView()
makeSelectedTagsView()
}
}
@ViewBuilder
private func makeTagInputView() -> some View {
TextField("Add tag", text: $viewModel.tagInput)
.autocorrectionDisabled(true)
.font(.body.monospaced())
.textFieldCard()
.help("Enter a tag and press the return key to add it, or press tab to select the first suggestion")
.onSubmit {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.addTag(viewModel.tagInput)
}
}
.onChange(of: viewModel.tagInput) {
viewModel.updateTagSuggestions(from: existingTags.map(\.title))
}
.onKeyPress(.tab) {
do {
try viewModel.selectFistTagSuggestion()
return .handled
} catch {
return .ignored
}
}
}
@ViewBuilder
private func makeTagSuggestionsView() -> some View {
if !viewModel.suggestedTags.isEmpty {
TagListView(
tags: viewModel.suggestedTags,
helpText: { "Add existing tag '\($0)'" },
action: { tag in
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.addTag(tag)
}
}
)
}
}
@ViewBuilder
private func makeSelectedTagsView() -> some View {
if !viewModel.tags.isEmpty {
TagListView(
tags: viewModel.tags,
style: .remove,
helpText: { "Remove tag '\($0)'" },
action: { tag in
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.removeTag(tag)
}
}
)
}
}
@ViewBuilder
private func makeVisibilityView() -> some View {
HStack {
Spacer()
Toggle("Hidden from Public?", isOn: $viewModel.isHidden)
.help("Hide image from public gallery")
}
}
@ToolbarContentBuilder
private func makeToolbarContent() -> some ToolbarContent {
ToolbarItemGroup {
if viewModel.shouldShowProgress {
ProgressToolbarItem()
}
if viewModel.showAddressesPicker {
AddressPickerToolbarItem(
addresses: viewModel.addresses,
selection: $viewModel.selectedAddress,
helpText: "Select address for picture upload"
)
}
makeUploadPictureToolbarItem()
}
}
@ViewBuilder
private func makeUploadPictureToolbarItem() -> some View {
Button {
viewModel.uploadPicture()
} label: {
Image(systemName: viewModel.isSubmitDisabled ? "paperplane" : "paperplane.fill")
}
.help("Upload picture")
.disabled(viewModel.isSubmitDisabled)
}
}
// MARK: - Preview
#if DEBUG
#Preview("Without Data") {
UploadView(
viewModel: UploadViewModelMother.makeUploadViewModel()
)
}
#Preview("With Data") {
UploadView(
viewModel: UploadViewModelMother.makeUploadViewModel(
caption: "This is the photo caption",
altText: "This is a very long alt text for the image, describing the image.",
isHidden: false,
tags: ["foo", "bar"],
imageData: DataMother.makeSquareImageData()
)
)
}
#Preview("Drag-And-Drop") {
UploadView(
viewModel: UploadViewModelMother.makeUploadViewModel(
isDragging: true
)
)
}
#endif