Implements add media page

This commit is contained in:
2025-06-11 20:13:37 -07:00
parent 601ffc4a75
commit 937a061cdd
8 changed files with 156 additions and 42 deletions

View File

@@ -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)
}

View File

@@ -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" : {

View File

@@ -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")
}

View File

@@ -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))
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -55,9 +55,9 @@ struct NowPlayingView: View
Spacer()
}
}
.tint(Color(uiColor: .label))
.imageScale(.large)
.frame(height: 34.0)
.tint(.label)
Spacer()