Implements add media page
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension Optional
|
extension Optional
|
||||||
{
|
{
|
||||||
@@ -91,3 +92,8 @@ struct RequestBuilder
|
|||||||
case delete = "DELETE"
|
case delete = "DELETE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Color
|
||||||
|
{
|
||||||
|
static let label = Color(uiColor: .label)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ADD_MEDIA" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Add Media"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ADD_SERVER" : {
|
"ADD_SERVER" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -178,6 +188,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SEARCH_FOR_MEDIA" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Search YouTube for Media…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SERVER_IS_ONLINE" : {
|
"SERVER_IS_ONLINE" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -33,4 +33,6 @@ extension LocalizedStringKey
|
|||||||
static let noServersConfigured = LocalizedStringKey("NO_SERVERS_CONFIGURED")
|
static let noServersConfigured = LocalizedStringKey("NO_SERVERS_CONFIGURED")
|
||||||
static let playlistEmpty = LocalizedStringKey("PLAYLIST_IS_EMPTY")
|
static let playlistEmpty = LocalizedStringKey("PLAYLIST_IS_EMPTY")
|
||||||
static let favoritesEmpty = LocalizedStringKey("FAVORITES_IS_EMPTY")
|
static let favoritesEmpty = LocalizedStringKey("FAVORITES_IS_EMPTY")
|
||||||
|
static let addMedia = LocalizedStringKey("ADD_MEDIA")
|
||||||
|
static let searchForMedia = LocalizedStringKey("SEARCH_FOR_MEDIA")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
QueueCube/Views/AddMediaView.swift
Normal file
110
QueueCube/Views/AddMediaView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,14 @@ struct ContentView: View
|
|||||||
.presentationBackground(.regularMaterial)
|
.presentationBackground(.regularMaterial)
|
||||||
.presentationDetents([ .height(320.0) ])
|
.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
|
// MARK: - Types
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ class MainViewModel
|
|||||||
var selectedTab: Tab = .playlist
|
var selectedTab: Tab = .playlist
|
||||||
|
|
||||||
var isNowPlayingSheetPresented: Bool = false
|
var isNowPlayingSheetPresented: Bool = false
|
||||||
|
var isAddMediaSheetPresented: Bool = false
|
||||||
|
|
||||||
var playlistModel = MediaListViewModel(mode: .playlist)
|
var playlistModel = MediaListViewModel(mode: .playlist)
|
||||||
var favoritesModel = MediaListViewModel(mode: .favorites)
|
var favoritesModel = MediaListViewModel(mode: .favorites)
|
||||||
var nowPlayingViewModel = NowPlayingViewModel()
|
var nowPlayingViewModel = NowPlayingViewModel()
|
||||||
var addMediaViewModel = AddMediaBarViewModel()
|
var addMediaViewModel = AddMediaView.ViewModel()
|
||||||
var serverSelectionViewModel = ServerSelectionToolbarModifier.ViewModel()
|
var serverSelectionViewModel = ServerSelectionToolbarModifier.ViewModel()
|
||||||
|
|
||||||
private var refreshingFromAPIDepth: UInt8 = 0
|
private var refreshingFromAPIDepth: UInt8 = 0
|
||||||
@@ -39,7 +40,7 @@ class MainViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onAddButtonTapped() {
|
func onAddButtonTapped() {
|
||||||
|
isAddMediaSheetPresented = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func onNowPlayingMiniTapped() {
|
func onNowPlayingMiniTapped() {
|
||||||
@@ -101,6 +102,7 @@ class MainViewModel
|
|||||||
let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !strippedURL.isEmpty {
|
if !strippedURL.isEmpty {
|
||||||
addMediaViewModel.fieldContents = ""
|
addMediaViewModel.fieldContents = ""
|
||||||
|
isAddMediaSheetPresented = false
|
||||||
|
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .playlist:
|
case .playlist:
|
||||||
@@ -112,6 +114,10 @@ class MainViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMediaViewModel.onCancel = { [weak self] in
|
||||||
|
self?.isAddMediaSheetPresented = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withModificationsViaAPI(_ modificationBlock: (API) async throws -> Void) async {
|
func withModificationsViaAPI(_ modificationBlock: (API) async throws -> Void) async {
|
||||||
@@ -341,7 +347,7 @@ struct ErrorDisplayModifier: ViewModifier
|
|||||||
.fill(.background)
|
.fill(.background)
|
||||||
|
|
||||||
contentPlaceholderView(title: .connectionError, systemImage: "exclamationmark.triangle.fill")
|
contentPlaceholderView(title: .connectionError, systemImage: "exclamationmark.triangle.fill")
|
||||||
.tint(Color(uiColor: .label))
|
.tint(.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ struct NowPlayingView: View
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(Color(uiColor: .label))
|
|
||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
.frame(height: 34.0)
|
.frame(height: 34.0)
|
||||||
|
.tint(.label)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user