diff --git a/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift index b0954ea..1f3641d 100644 --- a/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift +++ b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift @@ -420,7 +420,6 @@ extension View { enum AudioPanel: String, CaseIterable, Identifiable { case mixer = "Mixer" - case equalizer = "EQ" case enhance = "FX" var id: String { rawValue } @@ -428,7 +427,6 @@ enum AudioPanel: String, CaseIterable, Identifiable { var icon: String { switch self { case .mixer: "slider.vertical.3" - case .equalizer: "waveform.path.ecg" case .enhance: "sparkles" } } diff --git a/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift b/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift new file mode 100644 index 0000000..2725f74 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift @@ -0,0 +1,75 @@ +import Foundation + +/// Audio profile: EQ + enhancements (stored on device via PUT /api/audio/profile). +struct AudioProfileTemplate: Identifiable, Codable, Equatable, Hashable { + var id: String + var name: String + var subtitle: String + var systemImage: String + var isBuiltIn: Bool + var eq: [EQBandState] + var enhancements: EnhancementsState + + static let bandCenters: [Double] = [40, 120, 400, 1000, 3000, 10_000] + + static func eqBands(gains: [Double]) -> [EQBandState] { + zip(bandCenters, gains).enumerated().map { index, pair in + EQBandState(index: index, gainDb: pair.1, centerHz: pair.0, q: 1.414) + } + } + + func profileDTO(mixer: MixerState, master: MasterVolumeState) -> AudioProfileDTO { + AudioProfileDTO(mixer: mixer, master: master, eq: eq, enhancements: enhancements) + } + + func duplicatedAsUser(named name: String) -> AudioProfileTemplate { + AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Profilo personalizzato", + systemImage: "slider.horizontal.3", + isBuiltIn: false, + eq: eq, + enhancements: enhancements + ) + } +} + +enum AudioProfileLibrary { + static let builtIn: [AudioProfileTemplate] = [ + template(id: "builtin.flat", name: "Piatto", subtitle: "Risposta neutra", icon: "minus", gains: [0, 0, 0, 0, 0, 0], stereo: 0, bass: 0), + template(id: "builtin.vocal", name: "Voci", subtitle: "Parlato e podcast", icon: "person.wave.2", gains: [-2, -1, 3, 4, 2, 0], stereo: 15, bass: 5), + template(id: "builtin.bass", name: "Bassi", subtitle: "Più corpo e profondità", icon: "waveform.path", gains: [6, 4, 1, 0, -1, -2], stereo: 10, bass: 35), + template(id: "builtin.treble", name: "Alti", subtitle: "Dettaglio e aria", icon: "sparkles", gains: [-2, -1, 0, 2, 4, 5], stereo: 25, bass: 0), + template(id: "builtin.rock", name: "Rock", subtitle: "Energia V-shape", icon: "guitars", gains: [5, 3, -1, 1, 4, 5], stereo: 30, bass: 25), + template(id: "builtin.jazz", name: "Jazz", subtitle: "Caldo e naturale", icon: "music.quarternote.3", gains: [3, 2, 1, 2, 1, 0], stereo: 20, bass: 15), + template(id: "builtin.classical", name: "Classica", subtitle: "Equilibrio dinamico", icon: "hifispeaker.2", gains: [2, 1, 0, 0, 2, 3], stereo: 15, bass: 5), + template(id: "builtin.lounge", name: "Lounge", subtitle: "Ascolto rilassato", icon: "moon.stars", gains: [2, 1, 0, -1, -2, -3], stereo: 10, bass: 20), + template(id: "builtin.fm", name: "Radio FM", subtitle: "Voce in evidenza", icon: "radio", gains: [-1, 0, 2, 3, 2, 1], stereo: 20, bass: 10) + ] + + private static func template( + id: String, name: String, subtitle: String, icon: String, + gains: [Double], stereo: Int, bass: Int + ) -> AudioProfileTemplate { + AudioProfileTemplate( + id: id, + name: name, + subtitle: subtitle, + systemImage: icon, + isBuiltIn: true, + eq: AudioProfileTemplate.eqBands(gains: gains), + enhancements: EnhancementsState(stereoLevel: stereo, bassLevel: bass) + ) + } + + static func matchActive(_ profile: AudioProfileTemplate, eq: [EQBandState], enhancements: EnhancementsState) -> Bool { + guard eq.count == profile.eq.count else { return false } + let sortedEq = eq.sorted { $0.centerHz < $1.centerHz } + let sortedProfile = profile.eq.sorted { $0.centerHz < $1.centerHz } + let eqMatch = zip(sortedEq, sortedProfile).allSatisfy { abs($0.gainDb - $1.gainDb) < 0.6 } + return eqMatch + && profile.enhancements.stereoLevel == enhancements.stereoLevel + && profile.enhancements.bassLevel == enhancements.bassLevel + } +} diff --git a/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift index 06a9ea0..dbe2fe0 100644 --- a/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift +++ b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift @@ -116,7 +116,7 @@ struct MasterVolumeState: Equatable, Sendable, Codable { } } -struct EQBandState: Equatable, Sendable, Codable, Identifiable { +struct EQBandState: Equatable, Hashable, Sendable, Codable, Identifiable { var id: Int { index } var index: Int var gainDb: Double @@ -152,7 +152,7 @@ struct EQBandState: Equatable, Sendable, Codable, Identifiable { } } -struct EnhancementsState: Equatable, Sendable, Codable { +struct EnhancementsState: Equatable, Hashable, Sendable, Codable { var stereoLevel: Int = 0 var bassLevel: Int = 0 diff --git a/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift b/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift new file mode 100644 index 0000000..0d341d5 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift @@ -0,0 +1,20 @@ +import Foundation + +/// Persists user-created audio profiles on the iPhone (firmware stores one active profile only). +enum AudioProfileStore { + private static let key = "igiRadio.userAudioProfiles" + + static func loadUserProfiles() -> [AudioProfileTemplate] { + guard let data = UserDefaults.standard.data(forKey: key) else { return [] } + return (try? JSONDecoder().decode([AudioProfileTemplate].self, from: data)) ?? [] + } + + static func saveUserProfiles(_ profiles: [AudioProfileTemplate]) { + guard let data = try? JSONEncoder().encode(profiles) else { return } + UserDefaults.standard.set(data, forKey: key) + } + + static func allProfiles() -> [AudioProfileTemplate] { + AudioProfileLibrary.builtIn + loadUserProfiles() + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift new file mode 100644 index 0000000..b6ef50b --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift @@ -0,0 +1,51 @@ +import Foundation +import Observation + +@Observable +final class AudioProfileEditorViewModel { + var profile: AudioProfileTemplate + private let service: any DigiRadioService + private let onSave: (AudioProfileTemplate) -> Void + private var applyTask: Task? + + var isSaving = false + var isNewProfile: Bool + + init( + profile: AudioProfileTemplate, + service: any DigiRadioService, + isNewProfile: Bool, + onSave: @escaping (AudioProfileTemplate) -> Void + ) { + self.profile = profile + self.service = service + self.isNewProfile = isNewProfile + self.onSave = onSave + } + + func schedulePreviewApply() { + guard !profile.isBuiltIn else { return } + applyTask?.cancel() + applyTask = Task { + try? await Task.sleep(for: .milliseconds(400)) + guard !Task.isCancelled else { return } + await applyToDevice(persistUserCopy: false) + } + } + + func applyToDevice(persistUserCopy: Bool) async { + isSaving = true + defer { isSaving = false } + let audio = service.state.audio + let dto = profile.profileDTO(mixer: audio.mixer, master: audio.master) + try? await service.applyAudioProfile(dto) + if persistUserCopy, !profile.isBuiltIn { + onSave(profile) + } + } + + func saveUserProfile() { + guard !profile.isBuiltIn else { return } + onSave(profile) + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift new file mode 100644 index 0000000..44b2af8 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift @@ -0,0 +1,94 @@ +import Foundation +import Observation + +@Observable +final class AudioProfilesViewModel { + private let service: any DigiRadioService + + var builtIn: [AudioProfileTemplate] = AudioProfileLibrary.builtIn + var userProfiles: [AudioProfileTemplate] = [] + var activeProfileID: String? + var isLoading = false + var isApplying = false + var errorMessage: String? + + init(service: any DigiRadioService) { + self.service = service + reloadUserProfiles() + } + + func reloadUserProfiles() { + userProfiles = AudioProfileStore.loadUserProfiles() + } + + func load() async { + isLoading = true + defer { isLoading = false } + try? await service.refreshAudioProfile() + detectActiveProfile() + } + + func detectActiveProfile() { + let audio = service.state.audio + activeProfileID = AudioProfileStore.allProfiles().first { + AudioProfileLibrary.matchActive($0, eq: audio.eq, enhancements: audio.enhancements) + }?.id + } + + func apply(_ profile: AudioProfileTemplate) async { + isApplying = true + errorMessage = nil + defer { isApplying = false } + do { + let audio = service.state.audio + let dto = profile.profileDTO(mixer: audio.mixer, master: audio.master) + try await service.applyAudioProfile(dto) + activeProfileID = profile.id + } catch { + errorMessage = error.localizedDescription + } + } + + func deleteUserProfile(_ profile: AudioProfileTemplate) { + guard !profile.isBuiltIn else { return } + userProfiles.removeAll { $0.id == profile.id } + AudioProfileStore.saveUserProfiles(userProfiles) + if activeProfileID == profile.id { activeProfileID = nil } + } + + func saveUserProfile(_ profile: AudioProfileTemplate) { + var copy = profile + copy.isBuiltIn = false + if let index = userProfiles.firstIndex(where: { $0.id == copy.id }) { + userProfiles[index] = copy + } else { + userProfiles.append(copy) + } + AudioProfileStore.saveUserProfiles(userProfiles) + } + + func duplicateAsUser(from profile: AudioProfileTemplate, name: String) -> AudioProfileTemplate { + AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Profilo personalizzato", + systemImage: "slider.horizontal.3", + isBuiltIn: false, + eq: profile.eq, + enhancements: profile.enhancements + ) + } + + func profileFromDevice(name: String) -> AudioProfileTemplate { + let audio = service.state.audio + return AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Creato dal dispositivo", + systemImage: "waveform.path.ecg", + isBuiltIn: false, + eq: audio.eq.isEmpty ? AudioProfileLibrary.builtIn[0].eq : audio.eq, + enhancements: audio.enhancements + ) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift new file mode 100644 index 0000000..e99ca60 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +struct AudioProfileEditorView: View { + @Environment(AppEnvironment.self) private var environment + @Environment(\.dismiss) private var dismiss + + @State private var viewModel: AudioProfileEditorViewModel? + let profile: AudioProfileTemplate + let isNewProfile: Bool + let onSave: (AudioProfileTemplate) -> Void + + var body: some View { + let vm = viewModel ?? AudioProfileEditorViewModel( + profile: profile, + service: environment.digiRadio, + isNewProfile: isNewProfile, + onSave: onSave + ) + + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + if !vm.profile.isBuiltIn { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + Text("Nome profilo") + .font(.headline) + TextField("Nome", text: Binding( + get: { vm.profile.name }, + set: { vm.profile.name = $0 } + )) + .padding() + .background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 12)) + } + .igiPremiumCard() + } + + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Equalizzatore") + .font(.title3.weight(.bold)) + IGIGraphicEqualizer( + bands: Binding( + get: { vm.profile.eq }, + set: { vm.profile.eq = $0 } + ), + onCommit: { vm.schedulePreviewApply() } + ) + } + + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Enhancements") + .font(.headline) + HStack(spacing: IGITheme.spacingL) { + IGIEnhancementDial( + title: "Stereo", + systemImage: "circle.lefthalf.filled", + level: Binding( + get: { Double(vm.profile.enhancements.stereoLevel) }, + set: { vm.profile.enhancements.stereoLevel = Int($0) } + ), + onCommit: { vm.schedulePreviewApply() } + ) + IGIEnhancementDial( + title: "Bass", + systemImage: "waveform.path", + level: Binding( + get: { Double(vm.profile.enhancements.bassLevel) }, + set: { vm.profile.enhancements.bassLevel = Int($0) } + ), + onCommit: { vm.schedulePreviewApply() } + ) + } + } + .igiPremiumCard() + + if !vm.profile.isBuiltIn { + Button { + vm.saveUserProfile() + Task { await vm.applyToDevice(persistUserCopy: true) } + dismiss() + } label: { + Label("Salva profilo", systemImage: "square.and.arrow.down") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + } + + Button { + Task { await vm.applyToDevice(persistUserCopy: false) } + } label: { + Label("Applica a DigiRadio", systemImage: "play.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + + if vm.profile.isBuiltIn { + Button { + let duplicate = vm.profile.duplicatedAsUser(named: "\(vm.profile.name) custom") + onSave(duplicate) + dismiss() + } label: { + Label("Salva come profilo personale", systemImage: "plus.square.on.square") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + } + } + .padding(IGITheme.spacingM) + } + .background(IGIHeroBackground()) + .navigationTitle(vm.profile.isBuiltIn ? vm.profile.name : "Modifica profilo") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + if viewModel == nil { viewModel = vm } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift new file mode 100644 index 0000000..40a7430 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift @@ -0,0 +1,190 @@ +import SwiftUI + +struct AudioProfilesView: View { + @Environment(AppEnvironment.self) private var environment + @State private var viewModel: AudioProfilesViewModel? + @State private var editorProfile: AudioProfileTemplate? + @State private var editorIsNew = false + @State private var showNewProfileSheet = false + @State private var newProfileName = "" + + var body: some View { + let vm = viewModel ?? AudioProfilesViewModel(service: environment.digiRadio) + + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + activeBanner(vm) + profileSection(title: "Predefiniti", profiles: vm.builtIn, vm: vm, allowDelete: false, onDelete: nil) + profileSection(title: "I tuoi profili", profiles: vm.userProfiles, vm: vm, allowDelete: true, onDelete: { vm.deleteUserProfile($0) }) + + if vm.userProfiles.isEmpty { + Text("Crea un profilo personalizzato partendo da un preset o dal suono attuale del device.") + .font(.caption) + .foregroundStyle(.secondary) + .igiPremiumCard() + } + + HStack(spacing: IGITheme.spacingM) { + Button { + newProfileName = "Il mio profilo" + showNewProfileSheet = true + } label: { + Label("Nuovo profilo", systemImage: "plus.circle.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + + NavigationLink { + AudioView() + } label: { + Label("Mixer", systemImage: "slider.vertical.3") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + } + } + .padding(IGITheme.spacingM) + } + .background(IGIHeroBackground()) + .navigationTitle("Profilo audio") + .navigationBarTitleDisplayMode(.large) + .navigationDestination(item: $editorProfile) { profile in + AudioProfileEditorView( + profile: profile, + isNewProfile: editorIsNew, + onSave: { saved in + vm.saveUserProfile(saved) + vm.reloadUserProfiles() + vm.activeProfileID = saved.id + } + ) + } + .alert("Nuovo profilo", isPresented: $showNewProfileSheet) { + TextField("Nome", text: $newProfileName) + Button("Annulla", role: .cancel) {} + Button("Crea") { + let profile = vm.profileFromDevice(name: newProfileName.isEmpty ? "Il mio profilo" : newProfileName) + editorIsNew = true + editorProfile = profile + } + } message: { + Text("Parte dal profilo attualmente sul DigiRadio.") + } + .onAppear { + if viewModel == nil { viewModel = vm } + Task { await vm.load() } + } + } + + @ViewBuilder + private func activeBanner(_ vm: AudioProfilesViewModel) -> some View { + HStack(spacing: IGITheme.spacingM) { + Image(systemName: "checkmark.seal.fill") + .font(.title2) + .foregroundStyle(IGITheme.accent) + VStack(alignment: .leading, spacing: 2) { + Text("Profilo attivo") + .font(.caption) + .foregroundStyle(.secondary) + Text(vm.builtIn.first(where: { $0.id == vm.activeProfileID })?.name + ?? vm.userProfiles.first(where: { $0.id == vm.activeProfileID })?.name + ?? "Personalizzato / device") + .font(.headline) + } + Spacer() + if vm.isApplying { ProgressView() } + } + .igiPremiumCard() + } + + @ViewBuilder + private func profileSection( + title: String, + profiles: [AudioProfileTemplate], + vm: AudioProfilesViewModel, + allowDelete: Bool, + onDelete: ((AudioProfileTemplate) -> Void)? + ) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text(title) + .font(.title3.weight(.bold)) + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: IGITheme.spacingS)], spacing: IGITheme.spacingS) { + ForEach(profiles) { profile in + AudioProfileCard( + profile: profile, + isActive: vm.activeProfileID == profile.id, + allowDelete: allowDelete + ) { + Task { await vm.apply(profile) } + } onEdit: { + editorIsNew = false + editorProfile = profile + } onDelete: { + onDelete?(profile) + } + } + } + } + .igiPremiumCard() + } +} + +private struct AudioProfileCard: View { + var profile: AudioProfileTemplate + var isActive: Bool + var allowDelete: Bool + var onApply: () -> Void + var onEdit: () -> Void + var onDelete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + HStack { + Image(systemName: profile.systemImage) + .foregroundStyle(isActive ? .white : IGITheme.accent) + Spacer() + if isActive { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.white) + } + } + Text(profile.name) + .font(.headline) + .foregroundStyle(isActive ? .white : .primary) + Text(profile.subtitle) + .font(.caption2) + .foregroundStyle(isActive ? .white.opacity(0.85) : .secondary) + .lineLimit(2) + + HStack(spacing: IGITheme.spacingS) { + Button("Applica", action: onApply) + .font(.caption.weight(.semibold)) + .buttonStyle(.borderedProminent) + .tint(isActive ? .white : IGITheme.accent) + + Button("Modifica", action: onEdit) + .font(.caption.weight(.semibold)) + .buttonStyle(.bordered) + .tint(isActive ? .white : .primary) + } + .padding(.top, 4) + } + .padding(IGITheme.spacingM) + .background( + isActive + ? AnyShapeStyle(IGITheme.accent.gradient) + : AnyShapeStyle(Color.primary.opacity(0.05)), + in: RoundedRectangle(cornerRadius: 16, style: .continuous) + ) + .contextMenu { + if allowDelete { + Button(role: .destructive, action: onDelete) { + Label("Elimina", systemImage: "trash") + } + } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift index 8518bd0..43019ac 100644 --- a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift @@ -33,8 +33,6 @@ struct AudioView: View { switch vm.panel { case .mixer: mixerPanel(vm) - case .equalizer: - equalizerPanel(vm) case .enhance: enhancePanel(vm) } @@ -161,30 +159,6 @@ struct AudioView: View { } } - @ViewBuilder - private func equalizerPanel(_ vm: AudioViewModel) -> some View { - VStack(alignment: .leading, spacing: IGITheme.spacingM) { - Text("Equalizzatore 6 bande") - .font(.headline) - Text("Trascina le barre per regolare il guadagno. Rilascia per applicare al DSP.") - .font(.caption) - .foregroundStyle(.secondary) - - if vm.eqBands.isEmpty { - ProgressView() - .frame(maxWidth: .infinity) - } else { - IGIGraphicEqualizer( - bands: Binding( - get: { vm.eqBands }, - set: { vm.eqBands = $0 } - ), - onCommit: { vm.scheduleApply() } - ) - } - } - } - @ViewBuilder private func enhancePanel(_ vm: AudioViewModel) -> some View { HStack(spacing: IGITheme.spacingL) { diff --git a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift index ff11ec1..d4cb1cd 100644 --- a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift @@ -196,9 +196,9 @@ struct HomeView: View { quickLinkLabel("DAB", icon: "antenna.radiowaves.left.and.right") } NavigationLink { - AudioView() + AudioProfilesView() } label: { - quickLinkLabel("Audio", icon: "slider.vertical.3") + quickLinkLabel("Profilo", icon: "waveform.path.ecg") } NavigationLink { StreamingView() diff --git a/Software/APP/igiRadio/igiRadio/Views/RootView.swift b/Software/APP/igiRadio/igiRadio/Views/RootView.swift index f1d3a08..84e43a9 100644 --- a/Software/APP/igiRadio/igiRadio/Views/RootView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/RootView.swift @@ -36,8 +36,8 @@ private struct IPhoneRootView: View { NavigationStack { PresetsView() } .tabItem { Label("Preset", systemImage: "star.fill") } - NavigationStack { AudioView() } - .tabItem { Label("Audio", systemImage: "waveform") } + NavigationStack { AudioProfilesView() } + .tabItem { Label("Profilo", systemImage: "waveform.path.ecg") } NavigationStack { SettingsRootView() } .tabItem { Label("Impostazioni", systemImage: "gearshape.fill") } @@ -63,7 +63,7 @@ private struct IPadRootView: View { case .dab: DABRadioView() case .stream: StreamingView() case .bluetooth: BluetoothView() - case .audio: AudioView() + case .audioProfiles: AudioProfilesView() case .presets: PresetsView() case .settings: SettingsRootView() } @@ -72,7 +72,7 @@ private struct IPadRootView: View { } enum SidebarItem: String, CaseIterable, Identifiable { - case home, fm, dab, stream, bluetooth, audio, presets, settings + case home, fm, dab, stream, bluetooth, audioProfiles, presets, settings var id: String { rawValue } @@ -83,7 +83,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .dab: "DAB" case .stream: "Stream" case .bluetooth: "Bluetooth" - case .audio: "Audio" + case .audioProfiles: "Profilo audio" case .presets: "Preset" case .settings: "Impostazioni" } @@ -96,7 +96,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .dab: "antenna.radiowaves.left.and.right" case .stream: "dot.radiowaves.forward" case .bluetooth: "dot.radiowaves.left.and.right" - case .audio: "waveform" + case .audioProfiles: "waveform.path.ecg" case .presets: "star.fill" case .settings: "gearshape.fill" } diff --git a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift index 69b853c..be7bdde 100644 --- a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift +++ b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift @@ -16,10 +16,15 @@ struct SettingsRootView: View { NavigationLink("Preset") { PresetsView() } } Section("Audio") { + NavigationLink { + AudioProfilesView() + } label: { + Label("Profili audio", systemImage: "waveform.path.ecg") + } NavigationLink { AudioView() } label: { - Label("Mixer & Equalizzatore", systemImage: "slider.vertical.3") + Label("Mixer & volume", systemImage: "slider.vertical.3") } NavigationLink("Bluetooth speaker") { BluetoothView() } }