diff --git a/apps/iOS/App/Info.plist b/apps/iOS/App/Info.plist index 08465ae..d488f6e 100644 --- a/apps/iOS/App/Info.plist +++ b/apps/iOS/App/Info.plist @@ -8,6 +8,17 @@ system bar no longer applies. --> UIDesignRequiresCompatibility + + + UIUserInterfaceStyle + Light + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/apps/iOS/Views/AuthorityRecommendationView.swift b/apps/iOS/Views/AuthorityRecommendationView.swift index 518a302..faa0728 100644 --- a/apps/iOS/Views/AuthorityRecommendationView.swift +++ b/apps/iOS/Views/AuthorityRecommendationView.swift @@ -151,41 +151,37 @@ struct AuthorityRecommendationsView: View { showingAddSheet = false } } - .confirmationDialog( + // Alerts rather than confirmationDialogs: on iPad a dialog is presented + // as a popover, and on iPadOS 26 (with the app's + // UIDesignRequiresCompatibility opt-out) that popover renders with no + // readable content. Alerts are unaffected. + .alert( "Delete Authority Reference", isPresented: $showingDeleteConfirmation, - titleVisibility: .visible - ) { + presenting: deleteTarget + ) { authority in Button("Delete", role: .destructive) { - if let authority = deleteTarget { - Task { await deleteAuthority(authority) } - } + Task { await deleteAuthority(authority) } } Button("Cancel", role: .cancel) { deleteTarget = nil } - } message: { - if let authority = deleteTarget { - Text("Remove this \(authority.sourceDisplayName) reference from the recording?") - } + } message: { authority in + Text("Remove this \(authority.sourceDisplayName) reference from the recording?") } - .confirmationDialog( + .alert( "Link to This Recording", isPresented: $showingLinkConfirmation, - titleVisibility: .visible - ) { + presenting: linkTarget + ) { authority in Button("Link to \(albumTitle)") { - if let authority = linkTarget { - Task { await linkAuthority(authority) } - } + Task { await linkAuthority(authority) } } Button("Cancel", role: .cancel) { linkTarget = nil } - } message: { - if let authority = linkTarget { - Text("Link \"\(authority.artistName ?? "Unknown") - \(authority.albumTitle ?? "Unknown")\" to this recording?") - } + } message: { authority in + Text("Link \"\(authority.artistName ?? "Unknown") - \(authority.albumTitle ?? "Unknown")\" to this recording?") } .overlay { if linkingInProgress { diff --git a/apps/iOS/Views/MusicBrainzSearchSheet.swift b/apps/iOS/Views/MusicBrainzSearchSheet.swift index 1bf153e..0ac599f 100644 --- a/apps/iOS/Views/MusicBrainzSearchSheet.swift +++ b/apps/iOS/Views/MusicBrainzSearchSheet.swift @@ -12,6 +12,7 @@ struct MusicBrainzSearchSheet: View { let onSongImported: () -> Void @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) private var openURL @EnvironmentObject var authManager: AuthenticationManager @StateObject private var musicBrainzService = MusicBrainzService() @@ -73,31 +74,39 @@ struct MusicBrainzSearchSheet: View { } message: { Text(resultMessage) } - .confirmationDialog( - "Request Song", - isPresented: .constant(selectedWork != nil && !isSubmitting), - titleVisibility: .visible - ) { - if let work = selectedWork { - Button("Request \"\(work.title)\"") { - Task { - await requestSong(work) - } - } - - if let url = URL(string: work.musicbrainzUrl) { - Link("View on MusicBrainz", destination: url) - } - - Button("Cancel", role: .cancel) { - selectedWork = nil - } + } + // An alert rather than a confirmationDialog: on iPad a dialog is presented + // as a popover, and on iPadOS 26 (with the app's + // UIDesignRequiresCompatibility opt-out) that popover renders with no + // readable content. Alerts are unaffected, but SwiftUI only honours one + // alert per view, so this one hangs off the NavigationStack while the + // result alert above stays on its content. Alerts also only take buttons, + // so the MusicBrainz link opens through the environment. + .alert( + "Request Song", + isPresented: Binding( + get: { selectedWork != nil && !isSubmitting }, + set: { if !$0 { selectedWork = nil } } + ), + presenting: selectedWork + ) { work in + Button("Request \"\(work.title)\"") { + Task { + await requestSong(work) } - } message: { - if let work = selectedWork { - Text("Request that \"\(work.title)\" by \(work.composerDisplay) be added? We'll review it and add the song if it's a good fit.") + } + + if let url = URL(string: work.musicbrainzUrl) { + Button("View on MusicBrainz") { + openURL(url) } } + + Button("Cancel", role: .cancel) { + selectedWork = nil + } + } message: { work in + Text("Request that \"\(work.title)\" by \(work.composerDisplay) be added? We'll review it and add the song if it's a good fit.") } .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) diff --git a/apps/iOS/Views/RecordingContributionEditView.swift b/apps/iOS/Views/RecordingContributionEditView.swift index c28254f..84fb1a9 100644 --- a/apps/iOS/Views/RecordingContributionEditView.swift +++ b/apps/iOS/Views/RecordingContributionEditView.swift @@ -149,11 +149,11 @@ struct RecordingContributionEditView: View { } message: { Text(errorMessage) } - .confirmationDialog( - "Delete Contribution", - isPresented: $showDeleteConfirmation, - titleVisibility: .visible - ) { + // An alert rather than a confirmationDialog: on iPad a dialog is + // presented as a popover, and on iPadOS 26 (with the app's + // UIDesignRequiresCompatibility opt-out) that popover renders with no + // readable content. Alerts are unaffected. + .alert("Delete Contribution", isPresented: $showDeleteConfirmation) { Button("Delete", role: .destructive) { deleteContribution() } diff --git a/apps/iOS/Views/SongDetailView.swift b/apps/iOS/Views/SongDetailView.swift index 0593d3f..a095eee 100644 --- a/apps/iOS/Views/SongDetailView.swift +++ b/apps/iOS/Views/SongDetailView.swift @@ -31,9 +31,6 @@ struct SongDetailView: View { @State private var alertMessage = "" @State private var isAddingToRepertoire = false - // Song refresh management - @State private var showRefreshConfirmation = false - // NEW: Toast notification @State private var toast: ToastItem? @@ -140,9 +137,29 @@ struct SongDetailView: View { .font(ApproachNoteTheme.largeTitle(weight: .regular)) .foregroundColor(ApproachNoteTheme.textSecondary) ) - .onLongPressGesture { - if canQueueForRefresh { - showRefreshConfirmation = true + // Long-press the title to re-research the song. This used to be a + // confirmationDialog, but on iPad that presents as a popover, and + // on iPadOS 26 (with the app's UIDesignRequiresCompatibility + // opt-out) the popover renders as an empty panel over the tab bar + // with no readable content. A context menu draws its own chrome, + // anchors to the title, and is the natural match for a long press. + .contextMenu { + Section("Refresh Song Data") { + if canQueueForRefresh { + Button { + refreshSongData(forceRefresh: false) + } label: { + Label("Quick Refresh", systemImage: "arrow.clockwise") + } + Button { + refreshSongData(forceRefresh: true) + } label: { + Label("Full Refresh", systemImage: "arrow.triangle.2.circlepath") + } + Text("Quick uses cached data. Full re-fetches everything.") + } else { + Text("This song is already queued for research.") + } } } @@ -346,21 +363,6 @@ struct SongDetailView: View { } message: { Text(alertMessage) } - .confirmationDialog( - "Refresh Song Data", - isPresented: $showRefreshConfirmation, - titleVisibility: .visible - ) { - Button("Quick Refresh") { - refreshSongData(forceRefresh: false) - } - Button("Full Refresh") { - refreshSongData(forceRefresh: true) - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Quick refresh uses cached data for faster results. Full refresh re-fetches everything from external sources.") - } .toast($toast) .onDisappear { viewModel.stopResearchStatusPolling() diff --git a/apps/iOS/Views/YouTubeImportView.swift b/apps/iOS/Views/YouTubeImportView.swift index 8978031..46423fb 100644 --- a/apps/iOS/Views/YouTubeImportView.swift +++ b/apps/iOS/Views/YouTubeImportView.swift @@ -94,11 +94,11 @@ struct YouTubeImportView: View { ) } } - .confirmationDialog( - "Link to Recording?", - isPresented: $showingRecordingChoice, - titleVisibility: .visible - ) { + // An alert rather than a confirmationDialog: on iPad a dialog is presented + // as a popover, and on iPadOS 26 (with the app's + // UIDesignRequiresCompatibility opt-out) that popover renders with no + // readable content. Alerts are unaffected. + .alert("Link to Recording?", isPresented: $showingRecordingChoice) { Button("Pick a Recording") { showingRecordingPicker = true }