This commit is contained in:
dimitris-c
2024-05-15 13:23:45 +03:00
parent 81bf90f5c4
commit 70a4dfd698
11 changed files with 512 additions and 30 deletions
@@ -23,6 +23,9 @@
9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; };
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; };
98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; };
98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; };
98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */; };
98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */; };
98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E6119B2BC72C0E0036BC47 /* DetailView.swift */; };
/* End PBXBuildFile section */
@@ -58,6 +61,9 @@
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = "<group>"; };
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = "<group>"; };
98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewAudioURLView.swift; sourceTree = "<group>"; };
98E6119B2BC72C0E0036BC47 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -94,6 +100,7 @@
isa = PBXGroup;
children = (
98BFB41B2BCAAD8A00E812C0 /* Info.plist */,
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */,
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */,
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
9816A8A82BC7F4DE00AD1299 /* Common */,
@@ -130,6 +137,8 @@
children = (
9806E8302BC6927D00757370 /* AudioPlayerModel.swift */,
9806E8292BC68F8700757370 /* AudioPlayerView.swift */,
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */,
98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */,
);
path = Content;
sourceTree = "<group>";
@@ -235,6 +244,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */,
98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */,
98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */,
9806E81A2BC5D12500757370 /* ContentView.swift in Sources */,
98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */,
@@ -242,6 +253,7 @@
9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */,
9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */,
9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */,
98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */,
9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */,
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */,
9806E8182BC5D12500757370 /* App.swift in Sources */,
+23
View File
@@ -9,13 +9,36 @@ import SwiftUI
@main
struct AudioPlayerApp: App {
@State var model = AppModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(model)
}
}
}
@Observable
class AppModel {
@ObservationIgnored
let audioPlayerService: AudioPlayerService
@ObservationIgnored
let equalizerService: EqualizerService
init(
audioPlayerService: AudioPlayerService = provideAudioPlayerService(),
equalizerService: (AudioPlayerService) -> EqualizerService = provideEqualizerService
) {
self.audioPlayerService = audioPlayerService
self.equalizerService = equalizerService(audioPlayerService)
}
}
func provideEqualizerService(playerService: AudioPlayerService) -> EqualizerService {
EqualizerService(playerService: playerService)
}
func provideAudioPlayerService() -> AudioPlayerService {
AudioPlayerService(
audioPlayer: provideDefaultAudioPlayer()
@@ -7,11 +7,12 @@
import Foundation
enum AudioContent: Int, CaseIterable {
enum AudioContent {
case offradio
case enlefko
case pepper966
case kosmos
case kosmosJazz
case radiox
case khruangbin
case piano
@@ -20,6 +21,7 @@ enum AudioContent: Int, CaseIterable {
case remoteWave
case local
case localWave
case custom(String)
var title: String {
switch self {
@@ -31,6 +33,8 @@ enum AudioContent: Int, CaseIterable {
return "Pepper 96.6"
case .kosmos:
return "Kosmos 93.6"
case .kosmosJazz:
return "Kosmos Jazz"
case .radiox:
return "Radio X"
case .khruangbin:
@@ -47,21 +51,25 @@ enum AudioContent: Int, CaseIterable {
return "Jazzy Frenchy"
case .nonOptimized:
return "Jazzy Frenchy"
case .custom(let url):
return url
}
}
var subtitle: String? {
switch self {
case .offradio:
return "Stream"
return "Stream • offradio.gr"
case .enlefko:
return "Stream"
return "Stream • enlefko.fm"
case .pepper966:
return "Stream"
return "Stream • pepper966.gr"
case .kosmos:
return "Stream"
return "Stream • ertecho.gr"
case .kosmosJazz:
return "Stream • ertecho.gr"
case .radiox:
return "Stream"
return "Stream • globalplayer.com"
case .khruangbin:
return "Remote mp3"
case .piano:
@@ -76,6 +84,8 @@ enum AudioContent: Int, CaseIterable {
return "Music by: bensound.com - m4a optimized"
case .nonOptimized:
return "Music by: bensound.com - m4a non-optimized"
case .custom:
return ""
}
}
@@ -89,6 +99,8 @@ enum AudioContent: Int, CaseIterable {
return URL(string: "https://n04.radiojar.com/pepper.m4a?1662039818=&rj-tok=AAABgvlUaioALhdOXDt0mgajoA&rj-ttl=5")!
case .kosmos:
return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")!
case .kosmosJazz:
return URL(string: "https://radiostreaming.ert.gr/ert-webjazz")!
case .radiox:
return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")!
case .khruangbin:
@@ -107,6 +119,8 @@ enum AudioContent: Int, CaseIterable {
return URL(fileURLWithPath: path)
case .remoteWave:
return URL(string: "https://file-examples.com/wp-content/storage/2017/11/file_example_WAV_5MG.wav")!
case .custom(let url):
return URL(string: url)!
}
}
}
@@ -64,6 +64,7 @@ struct AudioTrackView: View {
Text(track.title)
.font(.headline)
.fontWeight(.medium)
.foregroundStyle(.black)
.padding(.top, 8)
.padding(.bottom, track.subtitle == nil ? 8 : 0)
if let subtitle = track.subtitle {
@@ -0,0 +1,78 @@
//
// AddNewAudioURLView.swift
// AudioPlayer
//
// Created by Dimitris Chatzieleftheriou on 16/04/2024.
//
import SwiftUI
struct AddNewAudioURLView: View {
@Environment(\.dismiss) var dismiss
private let urlStyle = URL.FormatStyle(path: .omitWhen(.path, matches: ["/"]), query: .omitWhen(.query, matches: [""]))
@State private var audioUrl: URL?
var onAddNewUrl: (URL) -> Void
init(onAddNewUrl: @escaping (URL) -> Void) {
self.onAddNewUrl = onAddNewUrl
}
var body: some View {
NavigationStack {
VStack {
VStack(alignment: .leading) {
TextField(value: $audioUrl, format: urlStyle, prompt: nil, label: {
Text("Insert URL")
})
.keyboardType(.URL)
.autocorrectionDisabled()
.textFieldStyle(RoundedBorderTextFieldStyle())
.onSubmit {
if let url = audioUrl {
onAddNewUrl(url)
dismiss()
}
}
}
.padding(.horizontal, 16)
Button {
if let url = audioUrl {
onAddNewUrl(url)
dismiss()
}
} label: {
HStack {
Image(systemName: "plus")
Text("Add")
}
.foregroundStyle(Color.white)
}
.disabled(audioUrl == nil)
.opacity(audioUrl == nil ? 0.5 : 1.0)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.mint)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.navigationTitle("Add Audio URL")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(Color.gray)
}
}
}
}
}
}
#Preview {
AddNewAudioURLView(onAddNewUrl: { _ in })
}
@@ -7,10 +7,10 @@ import UIKit
import Foundation
import AudioStreaming
struct AudioPlaylist: Identifiable {
struct AudioPlaylist: Equatable, Identifiable {
var id: String { title }
let title: String
let tracks: [AudioTrack]
var tracks: [AudioTrack]
}
enum ScrubState: Equatable {
@@ -22,7 +22,7 @@ enum ScrubState: Equatable {
@Observable
public class AudioPlayerModel {
@ObservationIgnored
var audioPlayerService: AudioPlayerService
private(set) var audioPlayerService: AudioPlayerService
@ObservationIgnored
private var displayLink: DisplayLink?
@@ -49,8 +49,8 @@ public class AudioPlayerModel {
var currentTrack: AudioTrack?
init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: () -> AudioPlayerService = provideAudioPlayerService) {
self.audioPlayerService = audioPlayerService()
init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: AudioPlayerService) {
self.audioPlayerService = audioPlayerService
self.audioTracks = audioTracksProvider()
self.audioPlayerService.delegate = self
@@ -62,6 +62,20 @@ public class AudioPlayerModel {
displayLink = nil
}
func addNewAudioTrack(url: URL) {
let customIndex = audioTracks.firstIndex(where: { $0.id == "Custom" })
let audioTrack = AudioTrack(from: .custom(url.absoluteString), status: .idle)
let playlist = AudioPlaylist(title: "Custom", tracks: [audioTrack])
if let customIndex {
let tracks = audioTracks[customIndex].tracks
if !tracks.contains(audioTrack) {
audioTracks[customIndex].tracks.append(audioTrack)
}
} else {
audioTracks.append(playlist)
}
}
func mute() {
isMuted.toggle()
audioPlayerService.toggleMute()
@@ -187,7 +201,7 @@ extension AudioPlayerModel: AudioPlayerServiceDelegate {
func metadataReceived(metadata: [String : String]) {
guard !metadata.isEmpty else { return }
if let title = metadata["StreamTitle"] {
liveAudioMetadata = title
liveAudioMetadata = title.isEmpty ? "-" : title
} else {
liveAudioMetadata = nil
}
@@ -195,7 +209,7 @@ extension AudioPlayerModel: AudioPlayerServiceDelegate {
}
func audioTracksProvider() -> [AudioPlaylist] {
let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .radiox]
let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox]
let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave]
return [
@@ -6,29 +6,78 @@
import SwiftUI
struct AudioPlayerView: View {
@Environment(AppModel.self) var appModel
@State var model = AudioPlayerModel()
@State var model: AudioPlayerModel
@State var eqSheetIsShown: Bool = false
@State var addNewAudioIsShown: Bool = false
init(appModel: AppModel) {
self._model = State(wrappedValue: AudioPlayerModel(audioPlayerService: appModel.audioPlayerService))
}
var body: some View {
List {
ForEach(model.audioTracks) { section in
Section {
ForEach(section.tracks) { track in
AudioTrackView(track: track) {
model.play(track)
ScrollViewReader { proxy in
List {
ForEach(model.audioTracks) { section in
Section {
ForEach(section.tracks) { track in
AudioTrackView(track: track) {
model.play(track)
}
.id(track.id)
}
} header: {
Text(section.title)
}
}
}
.onChange(of: model.audioTracks) { _, newValue in
if let lastId = newValue.last?.tracks.last?.id {
withAnimation {
proxy.scrollTo(lastId, anchor: .bottom)
}
} header: {
Text(section.title)
}
}
}
.safeAreaInset(edge: .bottom) {
AudioPlayerControls(viewModel: model)
.background(Color.mint)
.background(
.ultraThinMaterial.shadow(
ShadowStyle.drop(color: .black.opacity(0.1), radius: 8, x: 0, y: -10)
)
)
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle("Audio Player")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
eqSheetIsShown.toggle()
} label: {
Image(systemName: "slider.horizontal.3")
}
Button {
addNewAudioIsShown.toggle()
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $eqSheetIsShown) {
EqualizerView(appModel: appModel)
.presentationDetents([.medium])
}
.sheet(isPresented: $addNewAudioIsShown) {
AddNewAudioURLView(
onAddNewUrl: { url in
model.addNewAudioTrack(url: url)
}
)
.presentationDetents([.height(150)])
}
}
}
@@ -59,12 +108,12 @@ struct AudioPlayerControls: View {
.frame(width: 20, height: 20)
.contentTransition(.symbolEffect(.replace))
}
.tint(.white)
.tint(.mint)
.padding(16)
if let audioMetadata = viewModel.liveAudioMetadata, viewModel.isLiveAudioStreaming {
Text("Now Playing: \(audioMetadata)")
.font(.caption)
.foregroundStyle(.white)
.foregroundStyle(.black)
.padding(.horizontal, 16)
}
Divider()
@@ -86,7 +135,7 @@ struct AudioPlayerControls: View {
Spacer()
Text(viewModel.formattedTotalTime ?? "")
}
.foregroundStyle(.white)
.foregroundStyle(.black)
.font(.caption)
.fontWeight(.medium)
}
@@ -97,7 +146,7 @@ struct AudioPlayerControls: View {
Text("Playback Rate: \(String(format: "%0.1f", viewModel.playbackRate))")
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(.white)
.foregroundStyle(.black)
Slider(value: $viewModel.playbackRate, in: 1.0...4.0, step: 0.2)
.onChange(of: viewModel.playbackRate) { _, new in
viewModel.update(rate: Float(new))
@@ -110,5 +159,5 @@ struct AudioPlayerControls: View {
}
#Preview {
AudioPlayerView(model: AudioPlayerModel())
AudioPlayerView(appModel: AppModel())
}
@@ -0,0 +1,239 @@
//
// Created by Dimitris Chatzieleftheriou on 15/04/2024.
//
import SwiftUI
@Observable
class EQBand: Identifiable {
var frequency: String
var min: Float
var max: Float
var value: Float
@ObservationIgnored
let index: Int
init(index: Int, frequency: String, min: Float, max: Float, value: Float) {
self.index = index
self.frequency = frequency
self.min = min
self.max = max
self.value = value
}
}
struct EqualizerView: View {
@Environment(\.dismiss) var dismiss
@Environment(AppModel.self) var appModel
@State var model: Model
init(appModel: AppModel) {
self._model = State(wrappedValue: Model(equalizerService: appModel.equalizerService))
}
var body: some View {
NavigationStack {
VStack {
Toggle(isOn: $model.isEnabled) {
Text("Enable")
}
.onChange(of: model.isEnabled) { _, _ in
model.enable()
}
.padding(.horizontal, 16)
VStack {
EQSliderView()
.frame(height: 180)
.padding(.horizontal, 16)
.environment(model)
Button {
withAnimation {
model.reset()
}
} label: {
HStack {
Text("Reset")
}
.foregroundStyle(Color.white)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.mint)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.task {
Task {
model.generateBands()
}
}
.navigationTitle("Equalizer")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(Color.gray)
}
}
}
}
}
}
struct EQSliderView: View {
@Environment(EqualizerView.Model.self) var eqModel
@State private var dragPointYLocations: [CGFloat] = Array(repeating: .zero, count: 6)
@State private var eqViewFrame: CGRect = .zero
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
// Draw labels for gain values
VStack {
Text("\(Int(eqModel.maxGain))db")
.font(.caption2)
.foregroundColor(.black)
Spacer()
Text("0dB")
.font(.caption2)
.foregroundColor(.black)
Spacer()
Text("\(Int(eqModel.minGain))db")
.font(.caption2)
.foregroundColor(.black)
}
GeometryReader { innerGeo in
ZStack {
Path { path in
path.move(to: CGPoint(x: innerGeo.size.width / 12, y: eqModel.dragPoints.first ?? 0))
for index in 1..<eqModel.dragPoints.count {
let x = positionForDragPoint(at: index, size: innerGeo.size)
let y = eqModel.dragPoints[index]
path.addLine(to: CGPoint(x: x, y: y))
}
}
.stroke(Color.mint, lineWidth: 2)
.animation(.default, value: eqModel.dragPoints)
Path { path in
for index in 0..<eqModel.dragPoints.count {
let x = positionForDragPoint(at: index, size: innerGeo.size)
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x, y: innerGeo.size.height))
}
path.move(to: CGPoint(x: 0, y: innerGeo.size.height / 2))
path.addLine(to: CGPoint(x: innerGeo.size.width, y: innerGeo.size.height / 2))
}
.stroke(Color.gray, lineWidth: 1)
ForEach(eqModel.bands) { band in
Circle()
.fill(Color.mint)
.frame(width: 20, height: 20)
.position(x: positionForDragPoint(at: band.index, size: innerGeo.size), y: eqModel.dragPoints[band.index])
.gesture(
DragGesture()
.onChanged { value in
let newY = min(max(value.location.y, 0), innerGeo.size.height)
eqModel.dragPoints[band.index] = newY
updateGainValue(at: band.index, in: innerGeo.size)
}
)
.onAppear {
eqModel.dragPoints[band.index] = gainToYPosition(at: band.value, in: innerGeo.size)
}
}
}
ForEach(eqModel.bands) { band in
Text(band.frequency)
.position(x: positionForDragPoint(at: band.index, size: innerGeo.size), y: innerGeo.size.height)
.font(.caption)
.foregroundColor(.black)
}
}
}
}
}
func positionForDragPoint(at index: Int, size: CGSize) -> CGFloat {
size.width / 12 * CGFloat(index * 2 + 1)
}
func updateGainValue(at index: Int, in size: CGSize) {
let percentage = dragPointYLocations[index] / size.height
let gain = (1 - Float(percentage)) * (eqModel.maxGain - eqModel.minGain) + eqModel.minGain
eqModel.update(gain: gain, index: index)
}
func gainToYPosition(at gain: Float, in size: CGSize) -> CGFloat {
let percentage = 1 - (gain - eqModel.minGain) / (eqModel.maxGain - eqModel.minGain)
return CGFloat(percentage) * size.height
}
}
extension EqualizerView {
@Observable
class Model {
@ObservationIgnored
private let equalizerService: EqualizerService
var isEnabled: Bool = false
var bands: [EQBand] = []
var dragPoints: [CGFloat] = []
let minGain: Float = -12
let maxGain: Float = 12
init(equalizerService: EqualizerService) {
self.equalizerService = equalizerService
dragPoints = [CGFloat].init(repeating: 0, count: equalizerService.bands.count)
}
func generateBands() {
bands = equalizerService.bands.enumerated().map { index, item in
var measurement = item.frequency
var frequency = String(Int(measurement))
if item.frequency >= 1000 {
measurement = item.frequency / 1000
frequency = "\(String(Int(measurement)))K"
}
return EQBand(index: index, frequency: frequency, min: minGain, max: maxGain, value: item.gain)
}
dragPoints = [CGFloat].init(repeating: 0, count: bands.count)
}
func enable() {
if isEnabled {
equalizerService.activate()
} else {
equalizerService.deactivate()
}
}
func update(gain: Float, index: Int) {
equalizerService.update(gain: gain, for: index)
}
func reset() {
equalizerService.reset()
dragPoints = [CGFloat].init(repeating: 0, count: bands.count)
}
}
}
#Preview {
EqualizerView(appModel: AppModel())
}
@@ -0,0 +1,48 @@
//
// Created by Dimitris Chatzieleftheriou on 15/04/2024.
//
import AVFoundation
final class EqualizerService {
private let playerService: AudioPlayerService
private let _freqs = [60, 150, 400, 1000, 2400, 15000]
private let eqUnit: AVAudioUnitEQ
var bands: [AVAudioUnitEQFilterParameters] {
eqUnit.bands
}
private(set) var isActivated: Bool = false
init(playerService: AudioPlayerService) {
self.playerService = playerService
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
for i in 0 ..< _freqs.count {
eqUnit.bands[i].bypass = false
eqUnit.bands[i].filterType = .parametric
eqUnit.bands[i].frequency = Float(_freqs[i])
eqUnit.bands[i].bandwidth = 0.5
eqUnit.bands[i].gain = 0
}
}
func update(gain: Float, for index: Int) {
eqUnit.bands[index].gain = gain
}
func reset() {
eqUnit.bands.forEach { $0.gain = 0 }
}
func activate() {
isActivated = true
playerService.add(eqUnit)
}
func deactivate() {
isActivated = false
playerService.remove(eqUnit)
}
}
@@ -6,7 +6,9 @@
import SwiftUI
struct ContentView: View {
@Environment(AppModel.self) var appModel
@State private var selection: NavigationContent?
var body: some View {
@@ -8,12 +8,14 @@
import SwiftUI
struct DetailView: View {
@Environment(AppModel.self) var appModel
var selection: NavigationContent
var body: some View {
switch selection {
case .audioPlayer:
AudioPlayerView()
AudioPlayerView(appModel: appModel)
case .audioQueue:
Text("Audio Queue")
}