From 65e86d30ec3a6dfcb4b06e704244c8e38577ddc9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 23 Nov 2025 14:09:14 +0100 Subject: [PATCH] Fix iOS playback settings menu text disappearing and resizing issues When tapping menus in playback settings (playback mode, quality profile, stream, rate, captions, audio track), the selected value text would disappear and cause unwanted resizing animations. Implemented ZStack overlay technique for all iOS menu buttons: - Visible static label remains on screen - Invisible Menu overlay (.opacity(0)) handles tap interactions - Prevents text from disappearing when menu opens - Eliminates resizing animations on option selection --- Shared/Player/PlaybackSettings.swift | 131 +++++++++++++++++++-------- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index 104bf6ee..e9fcce93 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -209,16 +209,22 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 100, alignment: .center) #elseif os(iOS) - Menu { - ratePicker - } label: { + ZStack { Text(player.rateLabel(player.currentRate)) .foregroundColor(.primary) .frame(width: 70) + + Menu { + ratePicker + } label: { + Text(player.rateLabel(player.currentRate)) + .foregroundColor(.primary) + .frame(width: 70) + .opacity(0) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.primary) .frame(width: 70, height: 40) #else Text(player.rateLabel(player.currentRate)) @@ -331,12 +337,20 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 300, alignment: .trailing) #else - Menu { - playbackModePicker - } label: { + ZStack { Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage) + .foregroundColor(.accentColor) + + Menu { + playbackModePicker + } label: { + Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage) + .foregroundColor(.accentColor) + .opacity(0) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } #endif } @@ -356,15 +370,22 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 300, alignment: .trailing) #elseif os(iOS) - Menu { - qualityProfilePicker - } label: { + ZStack { Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) + .foregroundColor(.accentColor) .frame(maxWidth: 240, alignment: .trailing) + + Menu { + qualityProfilePicker + } label: { + Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) + .foregroundColor(.accentColor) + .frame(maxWidth: 240, alignment: .trailing) + .opacity(0) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.accentColor) .frame(maxWidth: 240, alignment: .trailing) .frame(height: 40) #else @@ -406,15 +427,22 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 300, alignment: .trailing) #elseif os(iOS) - Menu { - StreamControl() - } label: { + ZStack { Text(player.streamSelection?.resolutionAndFormat ?? "loading...") - .frame(width: 140, height: 40, alignment: .trailing) .foregroundColor(player.streamSelection == nil ? .secondary : .accentColor) + .frame(width: 140, height: 40, alignment: .trailing) + + Menu { + StreamControl() + } label: { + Text(player.streamSelection?.resolutionAndFormat ?? "loading...") + .foregroundColor(player.streamSelection == nil ? .secondary : .accentColor) + .frame(width: 140, height: 40, alignment: .trailing) + .opacity(0) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) .frame(width: 140, height: 40, alignment: .trailing) #else StreamControl(focusedField: $focusedField) @@ -429,18 +457,13 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 300, alignment: .trailing) #elseif os(iOS) - Menu { - if videoCaptions?.isEmpty == false { - captionsPicker - } - } label: { + ZStack { HStack(spacing: 4) { Image(systemName: "text.bubble") if let captions = player.captions, let language = LanguageCodes(rawValue: captions.code) { Text("\(language.description.capitalized) (\(language.rawValue))") - .foregroundColor(.accentColor) } else { if videoCaptions?.isEmpty == true { Text("Not available") @@ -449,13 +472,38 @@ struct PlaybackSettings: View { } } } + .foregroundColor(.accentColor) .frame(alignment: .trailing) .frame(height: 40) - .disabled(videoCaptions?.isEmpty == true) + + Menu { + if videoCaptions?.isEmpty == false { + captionsPicker + } + } label: { + HStack(spacing: 4) { + Image(systemName: "text.bubble") + if let captions = player.captions, + let language = LanguageCodes(rawValue: captions.code) + { + Text("\(language.description.capitalized) (\(language.rawValue))") + } else { + if videoCaptions?.isEmpty == true { + Text("Not available") + } else { + Text("Disabled") + } + } + } + .foregroundColor(.accentColor) + .frame(alignment: .trailing) + .frame(height: 40) + .opacity(0) + .disabled(videoCaptions?.isEmpty == true) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.accentColor) #else ControlsOverlayButton(focusedField: $focusedField, field: .captions) { HStack(spacing: 8) { @@ -500,15 +548,22 @@ struct PlaybackSettings: View { .controlSize(.large) .frame(width: 300, alignment: .trailing) #elseif os(iOS) - Menu { - audioTrackPicker - } label: { + ZStack { Text(player.selectedAudioTrack?.displayLanguage ?? "Original") + .foregroundColor(.accentColor) .frame(maxWidth: 240, alignment: .trailing) + + Menu { + audioTrackPicker + } label: { + Text(player.selectedAudioTrack?.displayLanguage ?? "Original") + .foregroundColor(.accentColor) + .frame(maxWidth: 240, alignment: .trailing) + .opacity(0) + } + .buttonStyle(.plain) + .transaction { t in t.animation = .none } } - .transaction { t in t.animation = .none } - .buttonStyle(.plain) - .foregroundColor(.accentColor) .frame(maxWidth: 240, alignment: .trailing) .frame(height: 40) #else