From 937a061cddb0599f8c97afa4a4f41be092210ed0 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 11 Jun 2025 20:13:37 -0700 Subject: [PATCH] Implements add media page --- QueueCube/Backend/Utilities.swift | 6 ++ QueueCube/Localizable/Localizable.xcstrings | 20 ++++ QueueCube/Localizable/Strings.swift | 2 + QueueCube/Views/AddMediaBarView.swift | 38 ------- QueueCube/Views/AddMediaView.swift | 110 ++++++++++++++++++++ QueueCube/Views/ContentView.swift | 8 ++ QueueCube/Views/MainView.swift | 12 ++- QueueCube/Views/NowPlayingView.swift | 2 +- 8 files changed, 156 insertions(+), 42 deletions(-) delete mode 100644 QueueCube/Views/AddMediaBarView.swift create mode 100644 QueueCube/Views/AddMediaView.swift diff --git a/QueueCube/Backend/Utilities.swift b/QueueCube/Backend/Utilities.swift index ae1ef10..975147f 100644 --- a/QueueCube/Backend/Utilities.swift +++ b/QueueCube/Backend/Utilities.swift @@ -6,6 +6,7 @@ // import Foundation +import SwiftUI extension Optional { @@ -91,3 +92,8 @@ struct RequestBuilder case delete = "DELETE" } } + +extension Color +{ + static let label = Color(uiColor: .label) +} diff --git a/QueueCube/Localizable/Localizable.xcstrings b/QueueCube/Localizable/Localizable.xcstrings index a2c0bde..df780dc 100644 --- a/QueueCube/Localizable/Localizable.xcstrings +++ b/QueueCube/Localizable/Localizable.xcstrings @@ -24,6 +24,16 @@ } } }, + "ADD_MEDIA" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Media" + } + } + } + }, "ADD_SERVER" : { "localizations" : { "en" : { @@ -178,6 +188,16 @@ } } }, + "SEARCH_FOR_MEDIA" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Search YouTube for Media…" + } + } + } + }, "SERVER_IS_ONLINE" : { "localizations" : { "en" : { diff --git a/QueueCube/Localizable/Strings.swift b/QueueCube/Localizable/Strings.swift index bb9a6b5..c7de132 100644 --- a/QueueCube/Localizable/Strings.swift +++ b/QueueCube/Localizable/Strings.swift @@ -33,4 +33,6 @@ extension LocalizedStringKey static let noServersConfigured = LocalizedStringKey("NO_SERVERS_CONFIGURED") static let playlistEmpty = LocalizedStringKey("PLAYLIST_IS_EMPTY") static let favoritesEmpty = LocalizedStringKey("FAVORITES_IS_EMPTY") + static let addMedia = LocalizedStringKey("ADD_MEDIA") + static let searchForMedia = LocalizedStringKey("SEARCH_FOR_MEDIA") } diff --git a/QueueCube/Views/AddMediaBarView.swift b/QueueCube/Views/AddMediaBarView.swift deleted file mode 100644 index 8ef535d..0000000 --- a/QueueCube/Views/AddMediaBarView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// AddMediaBarView.swift -// QueueCube -// -// Created by James Magahern on 3/3/25. -// - -import SwiftUI - -@Observable -class AddMediaBarViewModel -{ - var fieldContents: String = "" - - var onAdd: (String) -> Void = { _ in } - var onSearch: () -> Void = {} -} - -struct AddMediaBarView: View -{ - @State var model: AddMediaBarViewModel - - var body: some View { - VStack { - HStack { - Button(action: model.onSearch) { Image(systemName: "magnifyingglass") } - - TextField(.addAnyURL, text: $model.fieldContents) - .textFieldStyle(.roundedBorder) - - Button(action: { model.onAdd(model.fieldContents) }) { Text(.add) } - .keyboardShortcut(.defaultAction) - } - .padding() - } - .background(Color.black.opacity(0.4)) - } -} diff --git a/QueueCube/Views/AddMediaView.swift b/QueueCube/Views/AddMediaView.swift new file mode 100644 index 0000000..d73f3c1 --- /dev/null +++ b/QueueCube/Views/AddMediaView.swift @@ -0,0 +1,110 @@ +// +// AddMediaView.swift +// QueueCube +// +// Created by James Magahern on 6/11/25. +// + +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) + } + + if model.supportsSearch { + Section { + NavigationLink { + SearchMediaView(model: $model) + } label: { + Image(systemName: "magnifyingglass") + Button(.searchForMedia, action: model.onSearch) + } + .tint(.label) + } + } + } + .task { fieldFocused = true } + .onAppear { model.activeDetent = ViewModel.Detent.collapsed.value } + .navigationTitle(.addMedia) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button(.add, action: model.addButtonTapped) + .disabled(model.fieldContents.isEmpty) + .bold() + } + + ToolbarItemGroup(placement: .topBarLeading) { + Button(.cancel, action: model.onCancel) + } + } + } + } + + // MARK: - Types + + enum Page: String, Identifiable + { + case addURL + case searchMedia + + var id: String { rawValue } + } + + @Observable + class ViewModel + { + var fieldContents: String = "" + var onAdd: (String) -> Void = { _ in } + var onCancel: () -> Void = { } + 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) + } + } +} + +struct SearchMediaView: View +{ + @Binding var model: AddMediaView.ViewModel + + var body: some View { + HStack { + + } + .navigationTitle(.searchForMedia) + .presentationBackground(.regularMaterial) + .onAppear { + model.activeDetent = AddMediaView.ViewModel.Detent.expanded.value + } + } +} diff --git a/QueueCube/Views/ContentView.swift b/QueueCube/Views/ContentView.swift index 309c6f0..042ebf6 100644 --- a/QueueCube/Views/ContentView.swift +++ b/QueueCube/Views/ContentView.swift @@ -22,6 +22,14 @@ struct ContentView: View .presentationBackground(.regularMaterial) .presentationDetents([ .height(320.0) ]) } + .sheet(isPresented: $model.isAddMediaSheetPresented) { + AddMediaView(model: $model.addMediaViewModel) + .presentationBackground(.regularMaterial) + .presentationDetents( + Set(AddMediaView.ViewModel.Detent.allCases.map { $0.value }), + selection: $model.addMediaViewModel.activeDetent + ) + } } // MARK: - Types diff --git a/QueueCube/Views/MainView.swift b/QueueCube/Views/MainView.swift index 5b76ada..116a3fe 100644 --- a/QueueCube/Views/MainView.swift +++ b/QueueCube/Views/MainView.swift @@ -16,11 +16,12 @@ class MainViewModel var selectedTab: Tab = .playlist var isNowPlayingSheetPresented: Bool = false + var isAddMediaSheetPresented: Bool = false var playlistModel = MediaListViewModel(mode: .playlist) var favoritesModel = MediaListViewModel(mode: .favorites) var nowPlayingViewModel = NowPlayingViewModel() - var addMediaViewModel = AddMediaBarViewModel() + var addMediaViewModel = AddMediaView.ViewModel() var serverSelectionViewModel = ServerSelectionToolbarModifier.ViewModel() private var refreshingFromAPIDepth: UInt8 = 0 @@ -39,7 +40,7 @@ class MainViewModel } func onAddButtonTapped() { - + isAddMediaSheetPresented = true } func onNowPlayingMiniTapped() { @@ -101,6 +102,7 @@ class MainViewModel let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines) if !strippedURL.isEmpty { addMediaViewModel.fieldContents = "" + isAddMediaSheetPresented = false switch selectedTab { case .playlist: @@ -112,6 +114,10 @@ class MainViewModel } } } + + addMediaViewModel.onCancel = { [weak self] in + self?.isAddMediaSheetPresented = false + } } func withModificationsViaAPI(_ modificationBlock: (API) async throws -> Void) async { @@ -341,7 +347,7 @@ struct ErrorDisplayModifier: ViewModifier .fill(.background) contentPlaceholderView(title: .connectionError, systemImage: "exclamationmark.triangle.fill") - .tint(Color(uiColor: .label)) + .tint(.label) } } } diff --git a/QueueCube/Views/NowPlayingView.swift b/QueueCube/Views/NowPlayingView.swift index 764659a..677dc3b 100644 --- a/QueueCube/Views/NowPlayingView.swift +++ b/QueueCube/Views/NowPlayingView.swift @@ -55,9 +55,9 @@ struct NowPlayingView: View Spacer() } } - .tint(Color(uiColor: .label)) .imageScale(.large) .frame(height: 34.0) + .tint(.label) Spacer()