From 3aa819ecccccd4748b3fc8deee18fbcfc7f51cf6 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 15 Nov 2025 17:42:00 -0800 Subject: [PATCH] ios: Fixes for add media view - Adds paste button - Fix autofocus behavior (using UIKit) - Remove pointless sheet detents --- ios/QueueCube.xcodeproj/project.pbxproj | 1 + ios/QueueCube/Backend/API.swift | 2 - ios/QueueCube/Views/AddMediaView.swift | 39 ++++------- .../Views/AutofocusingTextField.swift | 69 +++++++++++++++++++ ios/QueueCube/Views/ContentView.swift | 4 -- ios/QueueCube/Views/MainView.swift | 11 +-- 6 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 ios/QueueCube/Views/AutofocusingTextField.swift diff --git a/ios/QueueCube.xcodeproj/project.pbxproj b/ios/QueueCube.xcodeproj/project.pbxproj index 1fceea3..3baeffb 100644 --- a/ios/QueueCube.xcodeproj/project.pbxproj +++ b/ios/QueueCube.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ CD8ACBBF2DC5B8F2008BF856 /* Exceptions for "QueueCube" folder in "QueueCube" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + App/Entitlements.plist, App/Info.plist, ); target = CD4E9B962D7691C20066FC17 /* QueueCube */; diff --git a/ios/QueueCube/Backend/API.swift b/ios/QueueCube/Backend/API.swift index ccf95b4..c1ba4a8 100644 --- a/ios/QueueCube/Backend/API.swift +++ b/ios/QueueCube/Backend/API.swift @@ -243,8 +243,6 @@ actor API private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream.Continuation) { print("Websocket Error: \(error)") - let nsError = error as NSError - // Always notify observers of WebSocket errors so reconnection can happen // The UI layer can decide whether to show the error to the user continuation.yield(.error(.websocketError(error))) diff --git a/ios/QueueCube/Views/AddMediaView.swift b/ios/QueueCube/Views/AddMediaView.swift index 2bc1f6a..36c7eab 100644 --- a/ios/QueueCube/Views/AddMediaView.swift +++ b/ios/QueueCube/Views/AddMediaView.swift @@ -10,17 +10,23 @@ import SwiftUI struct AddMediaView: View { @Binding var model: ViewModel - @FocusState var fieldFocused: Bool var body: some View { NavigationStack { Form { // Add URL Section { - TextField(.addAnyURL, text: $model.fieldContents) - .autocapitalization(.none) - .autocorrectionDisabled() - .focused($fieldFocused) + HStack { + AutofocusingTextField(String(localized: "ADD_ANY_URL"), text: $model.fieldContents) + .autocapitalization(.none) + .autocorrectionDisabled() + + PasteButton(payloadType: String.self) { payload in + guard let contents = payload.first else { return } + model.fieldContents = contents + } + .labelStyle(.iconOnly) + } } if model.supportsSearch { @@ -35,8 +41,6 @@ struct AddMediaView: View } } } - .task { fieldFocused = true } - .onAppear { model.activeDetent = ViewModel.Detent.collapsed.value } .navigationTitle(.addMedia) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -72,21 +76,6 @@ struct AddMediaView: View var onSearch: () -> Void = { } var supportsSearch: Bool = true - var activeDetent: PresentationDetent = Detent.collapsed.value - - enum Detent: CaseIterable - { - case collapsed - case expanded - - var value: PresentationDetent { - switch self { - case .collapsed: .height(320.0) - case .expanded: .large - } - } - } - fileprivate func addButtonTapped() { onAdd(fieldContents) } @@ -107,11 +96,8 @@ struct SearchMediaView: View Image(systemName: "magnifyingglass") .foregroundColor(.secondary) - TextField(.searchForMedia, text: $searchText) + AutofocusingTextField(String(localized: "SEARCH_FOR_MEDIA"), text: $searchText, onSubmit: performSearch) .focused($searchFieldFocused) - .onSubmit { - performSearch() - } if !searchText.isEmpty { Button { @@ -153,7 +139,6 @@ struct SearchMediaView: View .navigationTitle(.searchForMedia) .presentationBackground(.regularMaterial) .onAppear { - model.activeDetent = AddMediaView.ViewModel.Detent.expanded.value searchFieldFocused = true } } diff --git a/ios/QueueCube/Views/AutofocusingTextField.swift b/ios/QueueCube/Views/AutofocusingTextField.swift new file mode 100644 index 0000000..5432c27 --- /dev/null +++ b/ios/QueueCube/Views/AutofocusingTextField.swift @@ -0,0 +1,69 @@ +// +// AutofocusingTextField.swift +// QueueCube +// +// Created by James Magahern on 11/15/25. +// + +import SwiftUI +import UIKit + +/// Stupid: it appears to be impossible to make it so SwiftUI's `.focused(_:)` modifier takes place during the +/// presentation of a sheet, so this needs to exist just to make sure it's made first responder as soon as it moves to +/// the view hierarchy. +struct AutofocusingTextField: UIViewRepresentable +{ + let placeholder: String + @Binding var text: String + var onSubmit: () -> Void = {} + + init(_ placeholder: String, text: Binding, onSubmit: @escaping () -> Void = {}) { + self.placeholder = placeholder + self._text = text + self.onSubmit = onSubmit + } + + func makeUIView(context: Context) -> UITextField { + let tf = FirstResponderTextField() + tf.placeholder = placeholder + tf.delegate = context.coordinator + tf.returnKeyType = .done + tf.setContentHuggingPriority(.defaultHigh, for: .vertical) + return tf + } + + func updateUIView(_ uiView: UITextField, context: Context) { + if uiView.text != text { + uiView.text = text + } + context.coordinator.parent = self // keep latest onSubmit/text binding + } + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + final class Coordinator: NSObject, UITextFieldDelegate { + var parent: AutofocusingTextField + + init(parent: AutofocusingTextField) { + self.parent = parent + } + + func textFieldDidChangeSelection(_ textField: UITextField) { + parent.text = textField.text ?? "" + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + parent.onSubmit() + return true + } + } +} + +final class FirstResponderTextField: UITextField { + override func didMoveToSuperview() { + super.didMoveToSuperview() + becomeFirstResponder() + } +} diff --git a/ios/QueueCube/Views/ContentView.swift b/ios/QueueCube/Views/ContentView.swift index 5e232c0..f5de372 100644 --- a/ios/QueueCube/Views/ContentView.swift +++ b/ios/QueueCube/Views/ContentView.swift @@ -29,10 +29,6 @@ struct ContentView: View .sheet(isPresented: $model.isAddMediaSheetPresented) { AddMediaView(model: $model.addMediaViewModel) .presentationBackground(.regularMaterial) - .presentationDetents( - Set(AddMediaView.ViewModel.Detent.allCases.map { $0.value }), - selection: $model.addMediaViewModel.activeDetent - ) } .sheet(isPresented: $model.isEditSheetPresented) { EditItemView(model: $model.editMediaViewModel) diff --git a/ios/QueueCube/Views/MainView.swift b/ios/QueueCube/Views/MainView.swift index 2116278..f69b807 100644 --- a/ios/QueueCube/Views/MainView.swift +++ b/ios/QueueCube/Views/MainView.swift @@ -325,18 +325,9 @@ struct ServerSelectionToolbarModifier: ViewModifier } } } - - #if false - // TODO - Section { - Button(.addServer) { - - } - } - #endif } label: { Label(model.selectedServer?.displayName ?? "Servers", systemImage: "chevron.down") - .labelStyle(.titleAndIcon) + .labelStyle(.titleOnly) } .buttonBorderShape(.capsule) .buttonStyle(.bordered)