From ba241fc4355b328be996f1fd1cc1d7d148610135 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 3 Sep 2024 16:33:08 +0100 Subject: [PATCH] UI Ship review feedback for Duck Player onboarding (#3186) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208188855592767/f **Description**: * Change font style * Update copy --- .../SystemGray90.colorset/Contents.json | 38 ++ DuckDuckGo/Common/Localizables/UserText.swift | 6 +- DuckDuckGo/Localizable.xcstrings | 532 +++++++++++++++++- .../DuckPlayerOnboardingModalView.swift | 52 +- .../YoutubePlayer/TabModal/TabModal.swift | 20 +- 5 files changed, 626 insertions(+), 22 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/SystemGray90.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/SystemGray90.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/SystemGray90.colorset/Contents.json new file mode 100644 index 0000000000..e035ee6e40 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/SystemGray90.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.133", + "red" : "0.133" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c397dcfa68..dd6c175855 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -362,8 +362,10 @@ struct UserText { static let duckPlayerContingencyMessageBody = NSLocalizedString("duck-player.video-contingency-message", value: "Duck Player's functionality has been affected by recent changes to YouTube. We’re working to fix these issues and appreciate your understanding.", comment: "Message explaining to the user that Duck Player is not available") static let duckPlayerContingencyMessageCTA = NSLocalizedString("duck-player.video-contingency-cta", value: "Learn More", comment: "Button for the message explaining to the user that Duck Player is not available so the user can learn more") - static let duckPlayerOnboardingChoiceModalTitle = NSLocalizedString("duck-player.onboarding-choice-modal-title", value: "Drowning in ads on YouTube?", comment: "Title for a Duck Player onboarding modal screen") - static let duckPlayerOnboardingChoiceModalMessage = NSLocalizedString("duck-player.onboarding-choice-modal-message", value: "Duck Player lets you watch without targeted ads and comes free to use in DuckDuckGo.", comment: "Message for a Duck Player onboarding modal screen") + static let duckPlayerOnboardingChoiceModalTitleTop = NSLocalizedString("duck-player.onboarding-choice-modal-title-top", value: "Drowning in ads on YouTube?", comment: "Top title for a Duck Player onboarding modal screen") + static let duckPlayerOnboardingChoiceModalTitleBottom = NSLocalizedString("duck-player.onboarding-choice-modal-title-bottom", value: "Try Duck Player!", comment: "Bottom title for a Duck Player onboarding modal screen") + + static let duckPlayerOnboardingChoiceModalMessage = NSLocalizedString("duck-player.onboarding-choice-modal-message-body", value: "Duck Player lets you watch YouTube without targeted ads in DuckDuckGo and what you watch won't influence your recommendations.", comment: "Message for a Duck Player onboarding modal screen") static let duckPlayerOnboardingChoiceModalCTAConfirm = NSLocalizedString("duck-player.onboarding-choice-modal-CTA-confirm", value: "Turn on Duck Player", comment: "Confirm Button to enable Duck Player. -Duck Player- should not be translated") static let duckPlayerOnboardingChoiceModalCTADeny = NSLocalizedString("duck-player.onboarding-choice-modal-CTA-deny", value: "Not Now", comment: "Deny Button to enable Duck Player") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 8cd2dcad72..8eda5f4a91 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -13541,7 +13541,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "È DuckDuckGo Search." + "value" : "Vantaggi di DuckDuckGo Search." } }, "nl" : { @@ -18701,6 +18701,486 @@ } } }, + "duck-player.onboarding-choice-modal-CTA-confirm" : { + "comment" : "Confirm Button to enable Duck Player. -Duck Player- should not be translated", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player aktivieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn on Duck Player" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar Duck Player" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activez Duck Player" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attiva Duck Player" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player aanzetten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Włącz Duck Player" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligar o Duck Player" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включить Duck Player" + } + } + } + }, + "duck-player.onboarding-choice-modal-CTA-deny" : { + "comment" : "Deny Button to enable Duck Player", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jetzt nicht" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Not Now" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahora no" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas maintenant" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non adesso" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niet nu" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie teraz" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agora não" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не сейчас" + } + } + } + }, + "duck-player.onboarding-choice-modal-message-body" : { + "comment" : "Message for a Duck Player onboarding modal screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit dem Duck Player kannst du YouTube ohne gezielte Werbung in DuckDuckGo ansehen und was du dir ansiehst, hat keinen Einfluss auf deine Empfehlungen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Duck Player lets you watch YouTube without targeted ads in DuckDuckGo and what you watch won't influence your recommendations." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player te permite ver YouTube sin anuncios segmentados en DuckDuckGo y lo que veas no influirá en tus recomendaciones." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player vous permet de regarder YouTube sans publicités ciblées dans DuckDuckGo. De plus, ce que vous regardez n'influence pas vos recommandations." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player ti consente di guardare YouTube senza annunci mirati in DuckDuckGo e ciò che guardi non inciderà sui consigli che ricevi." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Met Duck Player kun je YouTube bekijken in DuckDuckGo, zonder gerichte advertenties. Wat je bekijkt heeft geen invloed op je aanbevelingen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duck Player umożliwia oglądanie YouTube bez ukierunkowanych reklam w DuckDuckGo i wpływu oglądanych treści na rekomendacje." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Duck Player permite-te ver o YouTube sem anúncios segmentados no DuckDuckGo, e o que vês não vai influenciar as tuas recomendações." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проигрыватель Duck Player позволяет смотреть видео из YouTube в браузере DuckDuckGo без целевой рекламы. Просмотренные ролики не влияют на рекомендации." + } + } + } + }, + "duck-player.onboarding-choice-modal-title-bottom" : { + "comment" : "Bottom title for a Duck Player onboarding modal screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teste Duck Player!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try Duck Player!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Prueba Duck Player!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez Duck Player !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prova Duck Player!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer Duck Player!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wypróbuj Duck Player!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experimenta o Duck Player!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попробуйте Duck Player!" + } + } + } + }, + "duck-player.onboarding-choice-modal-title-top" : { + "comment" : "Top title for a Duck Player onboarding modal screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ertrinkst du in Werbung auf YouTube?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Drowning in ads on YouTube?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Te ahogas en anuncios en YouTube?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "YouTube vous inonde de publicités ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "YouTube ti inonda di annunci?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te veel advertenties op YouTube?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Za dużo reklam na YouTube?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Já não suportas ver anúncios no YouTube?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шквал рекламы на YouTube?" + } + } + } + }, + "duck-player.onboarding-confirmation-modal-CTA-confirm" : { + "comment" : "Button to confirm on Duck Player onboarding modal confirmation screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstanden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Got it" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "J'ai compris" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ho capito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik snap het" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rozumiem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Понятно" + } + } + } + }, + "duck-player.onboarding-confirmation-modal-message" : { + "comment" : "Message for a Duck Player onboarding modal confirmation screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wähl ein Video aus, um die Magie des Duck Players zu erleben." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pick a video to see Duck Player work its magic." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elige un vídeo para ver cómo Duck Player hace su magia." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choisissez une vidéo pour voir opérer la magie de Duck Player." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scegli un video per vedere Duck Player fare la sua magia." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies een video om te zien hoe Duck Player voor je werkt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz film, aby zobaczyć, jak działa Duck Player." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escolhe um vídeo para o Duck Player fazer a sua magia." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выберите видео, чтобы оценить потрясающие возможности Duck Player в деле." + } + } + } + }, + "duck-player.onboarding-confirmation-modal-title" : { + "comment" : "Title for a Duck Player onboarding modal confirmation screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alles bereit!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "All set!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Todo listo!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tout est prêt !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutto pronto!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helemaal klaar!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wszystko gotowe!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tudo pronto!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всё готово!" + } + } + } + }, "duck-player.show-buttons" : { "comment" : "Private YouTube Player option", "extractionState" : "extracted_with_value", @@ -52648,11 +53128,59 @@ "comment" : "Menu with feedback commands", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback von Privacy Pro senden" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Send Privacy Pro Feedback" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar comentarios sobre Privacy Pro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer des commentaires sur Privacy Pro" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invia feedback su Privacy Pro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback sturen naar Privacy Pro" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyślij opinię na temat Privacy Pro" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar feedback sobre o Privacy Pro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить отзыв о Privacy Pro" + } } } }, @@ -58493,4 +59021,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift index 8f7ecbae99..82d6cefffc 100644 --- a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift @@ -21,8 +21,8 @@ import SwiftUI struct DuckPlayerOnboardingModalView: View { private enum Constants { static let outerContainerWidth: CGFloat = 504 - static let smallContainerHeight: CGFloat = 182 - static let bigContainerHeight: CGFloat = 286 + static let smallContainerHeight: CGFloat = 166 + static let bigContainerHeight: CGFloat = 350 static let containerCornerRadius: CGFloat = 12 static let darkModeBorderColor: Color = .white.opacity(0.2) static let whiteModeBorderColor: Color = .black.opacity(0.1) @@ -64,28 +64,46 @@ struct DuckPlayerOnboardingModalView: View { case .onboardingOptions: DuckPlayerOnboardingChoiceView(turnOnButtonPressed: { - viewModel.currentView = .confirmation + withAnimation { + viewModel.currentView = .confirmation + } viewModel.handleTurnOnCTA() }, notNowPressed: viewModel.handleNotNowCTA) } } } +private enum Constants { + enum FontSize { + static let title: CGFloat = 17 + static let body: CGFloat = 13 + } + + enum Layout { + static let modalOuterVerticalSpacing: CGFloat = 20 + static let modalInnerVerticalSpacing: CGFloat = 8 + } +} + private struct DuckPlayerOnboardingChoiceView: View { let turnOnButtonPressed: () -> Void let notNowPressed: () -> Void var body: some View { - VStack(spacing: 20) { + VStack(spacing: Constants.Layout.modalOuterVerticalSpacing) { DaxSpeechBubble { - VStack (alignment: .leading, spacing: 8) { - Text(UserText.duckPlayerOnboardingChoiceModalTitle) - .font(.title) - .padding(.horizontal) + VStack (alignment: .leading, spacing: Constants.Layout.modalInnerVerticalSpacing) { + VStack (alignment: .leading, spacing: 0) { + Text(UserText.duckPlayerOnboardingChoiceModalTitleTop) + Text(UserText.duckPlayerOnboardingChoiceModalTitleBottom) + } + .font(.system(size: Constants.FontSize.title).weight(.bold)) + .padding(.horizontal) Text(UserText.duckPlayerOnboardingChoiceModalMessage) - .font(.body) + .font(.system(size: Constants.FontSize.body)) .multilineText() + .lineSpacing(4) .padding(.horizontal) HStack { @@ -113,6 +131,7 @@ private struct DuckPlayerOnboardingChoiceView: View { Text(UserText.duckPlayerOnboardingChoiceModalCTAConfirm) } .buttonStyle(PrimaryCTAStyle()) + } } } @@ -121,20 +140,21 @@ private struct DuckPlayerOnboardingChoiceView: View { private struct DuckPlayerOnboardingConfirmationView: View { let voidButtonPressed: () -> Void var body: some View { - VStack(spacing: 20) { + VStack(spacing: Constants.Layout.modalOuterVerticalSpacing) { DaxSpeechBubble { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: Constants.Layout.modalInnerVerticalSpacing) { Text(UserText.duckPlayerOnboardingConfirmationModalTitle) - .font(.title) + .foregroundColor(.systemGray90) + .font(.system(size: Constants.FontSize.title).weight(.bold)) .padding(.horizontal) Text(UserText.duckPlayerOnboardingConfirmationModalMessage) - .font(.body) + .foregroundColor(.systemGray90) + .font(.system(size: Constants.FontSize.body)) .padding(.horizontal) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) - } Button { @@ -279,7 +299,6 @@ private struct SecondaryCTAStyle: ButtonStyle { }, notNowPressed: { }) - .frame(width: 504, height: 286) Divider() .padding() @@ -287,7 +306,8 @@ private struct SecondaryCTAStyle: ButtonStyle { DuckPlayerOnboardingConfirmationView(voidButtonPressed: { }) - .frame(width: 504, height: 152) } + .frame(width: 504) + .fixedSize() .padding() } diff --git a/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift index 1c6422ba8e..ff8cbd0ddc 100644 --- a/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift +++ b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift @@ -19,7 +19,7 @@ import Cocoa import Combine private enum AnimationConsts { - static let yAnimationOffset: CGFloat = 65 + static let yAnimationOffset: CGFloat = 70 static let duration: CGFloat = 0.6 } @@ -36,6 +36,10 @@ public final class TabModal { window.isOpaque = false window.hasShadow = true window.level = .floating + + window.contentView?.wantsLayer = true + window.contentView?.layer?.cornerRadius = 12 + window.contentView?.layer?.masksToBounds = true } modalViewController.view.wantsLayer = true return windowController @@ -125,13 +129,25 @@ extension TabModal: TabModalPresentable { overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) overlayWindow.alphaValue = 0 + /// There's a bug in macOS 14.x where, if a window's alpha value is animated from X to Y, the final value will always be X. + /// This is a workaround to prevent that. + var titleWindowOffset: CGFloat = 0 + if #unavailable(macOS 15) { + overlayWindow.styleMask.insert(.titled) + titleWindowOffset = 28 + } + NSAnimationContext.runAnimationGroup { context in context.duration = AnimationConsts.duration - let newOrigin = NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset) + let newOrigin = NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset - titleWindowOffset) let size = overlayWindow.frame.size overlayWindow.animator().alphaValue = 1 overlayWindow.animator().setFrame(NSRect(origin: newOrigin, size: size), display: true) + } + /// Second part of the workaround mentioned above + if #unavailable(macOS 15) { + overlayWindow.styleMask.remove(.titled) } } else { overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset))