From 9d59b0231b174ad7354ba6555060f5b59318179f Mon Sep 17 00:00:00 2001 From: Brandon T Date: Thu, 12 Sep 2024 18:12:23 -0400 Subject: [PATCH] Add language selection view --- .../BVC+TabManagerDelegate.swift | 5 +- .../BrowserViewController/BVC+Translate.swift | 7 +- .../Browser/Toolbars/UrlBar/File.swift | 2 +- .../UrlBar/TranslateURLBarButton.swift | 46 +-- .../Browser/Translate/BravePopup.swift | 122 +++++++ .../Browser/Translate/TranslateToast.swift | 309 +++++++++++++++++- .../BraveTranslateScriptHandler.swift | 17 +- 7 files changed, 452 insertions(+), 56 deletions(-) create mode 100644 ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/BravePopup.swift diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift index 6c0e72710089..b81e82b48f7c 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift @@ -156,10 +156,9 @@ extension BrowserViewController: TabManagerDelegate { topToolbar.updateReaderModeState(.unavailable) } - if ((selected?.getContentScript( + if (selected?.getContentScript( name: BraveTranslateScriptHandler.scriptName - ) as? BraveTranslateScriptHandler) != nil) - { + ) as? BraveTranslateScriptHandler) != nil { updateTranslateURLBar(tab: selected, state: selected?.translationState ?? .unavailable) updatePlaylistURLBar( tab: selected, diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Translate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Translate.swift index 62c810d4b4d4..ec61ecd94241 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Translate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Translate.swift @@ -8,8 +8,8 @@ import DesignSystem import Foundation import Onboarding import Preferences -import UIKit import SwiftUI +import UIKit extension BrowserViewController: BraveTranslateScriptHandlerDelegate { func updateTranslateURLBar(tab: Tab?, state: TranslateURLBarButton.TranslateState) { @@ -20,7 +20,7 @@ extension BrowserViewController: BraveTranslateScriptHandlerDelegate { // translateActivity(info: state == .existingItem ? item : nil) topToolbar.updateTranslateButtonState(state) - + showTranslateOnboarding(tab: tab) { [weak tab] translateEnabled in if let scriptHandler = tab?.getContentScript( name: BraveTranslateScriptHandler.scriptName @@ -63,6 +63,7 @@ extension BrowserViewController: BraveTranslateScriptHandlerDelegate { guard let tab = tab, let self = self else { return } self.topToolbar.locationView.translateButton.setOnboardingState(enabled: false) + Preferences.Translate.translateEnabled.value = true if let scriptHandler = tab.getContentScript( name: BraveTranslateScriptHandler.scriptName @@ -112,7 +113,7 @@ extension BrowserViewController: BraveTranslateScriptHandlerDelegate { func presentToast(_ languageInfo: BraveTranslateLanguageInfo) { let popover = PopoverController( content: TranslateToast(languageInfo: languageInfo), - autoLayoutConfiguration: .phoneWidth + autoLayoutConfiguration: nil ) popover.popoverDidDismiss = { [weak self] _ in diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/File.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/File.swift index c91f2da82be6..031d5a60ecc3 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/File.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/File.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Brandon T on 2024-08-23. // diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TranslateURLBarButton.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TranslateURLBarButton.swift index d78827c3e158..07167650f36a 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TranslateURLBarButton.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TranslateURLBarButton.swift @@ -35,6 +35,11 @@ class TranslateURLBarButton: UIButton { fatalError() } + override func layoutSubviews() { + super.layoutSubviews() + gradientView.frame = bounds + } + override var isSelected: Bool { didSet { updateAppearance() @@ -96,39 +101,24 @@ class TranslateURLBarButton: UIButton { } } - private lazy var gradientLayer = CAGradientLayer().then { - let gradient = BraveGradient( - stops: [ - .init(color: UIColor(rgb: 0xFA7250), position: 0.0), - .init(color: UIColor(rgb: 0xFF1893), position: 0.43), - .init(color: UIColor(rgb: 0xA78AFF), position: 1.0), - ], - angle: .figmaDegrees(314.42) - ) - - $0.frame = self.bounds - $0.type = gradient.type - $0.colors = gradient.stops.map(\.color.cgColor) - $0.locations = gradient.stops.map({ NSNumber(value: $0.position) }) - $0.startPoint = gradient.startPoint - $0.endPoint = gradient.endPoint - - let mask = CALayer() - mask.contents = imageIcon?.cgImage - mask.frame = $0.bounds - $0.mask = mask - } + private let gradientView = GradientView(braveSystemName: .iconsActive) func setOnboardingState(enabled: Bool) { if enabled { - gradientLayer.frame = imageView?.bounds ?? self.bounds - gradientLayer.mask?.frame = gradientLayer.bounds - - imageView?.layer.addSublayer(gradientLayer) - setImage(nil, for: .normal) + addSubview(gradientView) + gradientView.frame = bounds + gradientView.mask = imageView } else { - gradientLayer.removeFromSuperlayer() + if let imageView = imageView { + // gradientView.mask = imageView automatically removes the imageView from the button :o! + // So we have to add it back lol + addSubview(imageView) + } + + gradientView.mask = nil + gradientView.removeFromSuperview() setImage(imageIcon, for: .normal) + updateIconSize() } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/BravePopup.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/BravePopup.swift new file mode 100644 index 000000000000..0db6af497293 --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/BravePopup.swift @@ -0,0 +1,122 @@ +// Copyright 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveUI +import Foundation +import Shared +import SwiftUI +import UIKit + +struct BravePopupViewModifier: ViewModifier +where PopupContent: View { + @Binding var isPresented: Bool + let content: () -> PopupContent + + func body(content: Content) -> some View { + content + .background( + BravePopupView( + isPresented: self.$isPresented, + content: self.content + ) + ) + } +} + +extension View { + func bravePopup( + isPresented: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View where Content: View { + self.modifier( + BravePopupViewModifier( + isPresented: isPresented, + content: content + ) + ) + } +} + +struct BravePopupView: UIViewControllerRepresentable { + @Binding var isPresented: Bool + private var content: Content + + init( + isPresented: Binding, + @ViewBuilder content: () -> Content + ) { + self._isPresented = isPresented + self.content = content() + } + + func makeUIViewController(context: Context) -> UIViewController { + .init() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + if isPresented { + guard uiViewController.presentedViewController == nil + else { + // The system dismissed our Popup automatically, but never updated our presentation state + // It usually does this if you present another Popup or sheet + // Manually update it + if let controller = context.coordinator.presentedViewController?.value + as? PopupViewController + { + DispatchQueue.main.async { + controller.dismiss(animated: true) { + context.coordinator.presentedViewController = nil + self.isPresented = false + } + } + } else if context.coordinator.presentedViewController != nil { + DispatchQueue.main.async { + isPresented = false + } + } + return + } + + if let parent = uiViewController.parent, !parent.isBeingDismissed { + let controller = PopupViewController(rootView: content, isDismissable: true) + context.coordinator.presentedViewController = .init(controller) + + DispatchQueue.main.async { + if KeyboardHelper.defaultHelper.currentState != nil { + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, + from: nil, + for: nil + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + uiViewController.present(controller, animated: true) + } + } else { + uiViewController.present(controller, animated: true) + } + } + } + } else { + if let presentedViewController = context.coordinator.presentedViewController?.value, + presentedViewController == uiViewController.presentedViewController + { + uiViewController.presentedViewController?.dismiss(animated: true) { + context.coordinator.presentedViewController = nil + self.isPresented = false + } + } + } + } + + class Coordinator { + var presentedViewController: WeakRef? + } + + func makeCoordinator() -> Coordinator { + Coordinator() + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/TranslateToast.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/TranslateToast.swift index 74cbf3681a07..9f8e5eec9d44 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/TranslateToast.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Translate/TranslateToast.swift @@ -4,12 +4,228 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import BraveUI +import DesignSystem import SwiftUI +private struct TranslationOptionsView: View { + @Environment(\.dismiss) + private var dismiss + + @State + private var searchText = "" + + @State + private var timer: Timer? + + @Binding + var language: Locale.Language? + + var body: some View { + NavigationStack { + List { + ForEach(languages, id: \.self) { language in + Button( + action: { + self.language = language + dismiss() + }, + label: { + Text(languageName(for: language)) + } + ) + } + } + .listStyle(.plain) + .navigationTitle("Select Language") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + } + .searchable( + text: $searchText, + placement: .navigationBarDrawer(displayMode: .always), + prompt: "Search" + ) + .onChange(of: searchText) { searchText in + self.timer?.invalidate() + self.timer = Timer.scheduledTimer( + withTimeInterval: 0.1, + repeats: false, + block: { timer in + timer.invalidate() + // TODO: Filter here + } + ) + } + } + + // TODO: Take from Brave-Core's list + // TODO: Take from Apple's list + private var languages: [Locale.Language] { + return [ + "af", // Afrikaans + "ak", // Twi + "am", // Amharic + "ar", // Arabic + "as", // Assamese + "ay", // Aymara + "az", // Azerbaijani + "be", // Belarusian + "bg", // Bulgarian + "bho", // Bhojpuri + "bm", // Bambara + "bn", // Bengali + "bs", // Bosnian + "ca", // Catalan + "ceb", // Cebuano + "ckb", // Kurdish (Sorani) + "co", // Corsican + "cs", // Czech + "cy", // Welsh + "da", // Danish + "de", // German + "doi", // Dogri + "dv", // Dhivehi + "ee", // Ewe + "el", // Greek + "en", // English + "eo", // Esperanto + "es", // Spanish + "et", // Estonian + "eu", // Basque + "fa", // Persian + "fi", // Finnish + "fr", // French + "fy", // Frisian + "ga", // Irish + "gd", // Scots Gaelic + "gl", // Galician + "gom", // Konkani + "gu", // Gujarati + "ha", // Hausa + "haw", // Hawaiian + "hi", // Hindi + "hmn", // Hmong + "hr", // Croatian + "ht", // Haitian Creole + "hu", // Hungarian + "hy", // Armenian + "id", // Indonesian + "ig", // Igbo + "ilo", // Ilocano + "is", // Icelandic + "it", // Italian + "iw", // Hebrew - Chrome uses "he" + "ja", // Japanese + "jw", // Javanese - Chrome uses "jv" + "ka", // Georgian + "kk", // Kazakh + "km", // Khmer + "kn", // Kannada + "ko", // Korean + "kri", // Krio + "ku", // Kurdish + "ky", // Kyrgyz + "la", // Latin + "lb", // Luxembourgish + "lg", // Luganda + "ln", // Lingala + "lo", // Lao + "lt", // Lithuanian + "lus", // Mizo + "lv", // Latvian + "mai", // Maithili + "mg", // Malagasy + "mi", // Maori + "mk", // Macedonian + "ml", // Malayalam + "mn", // Mongolian + "mni-Mtei", // Manipuri (Meitei Mayek) + "mr", // Marathi + "ms", // Malay + "mt", // Maltese + "my", // Burmese + "ne", // Nepali + "nl", // Dutch + "no", // Norwegian - Chrome uses "nb" + "nso", // Sepedi + "ny", // Nyanja + "om", // Oromo + "or", // Odia (Oriya) + "pa", // Punjabi + "pl", // Polish + "ps", // Pashto + "pt", // Portuguese + "qu", // Quechua + "ro", // Romanian + "ru", // Russian + "rw", // Kinyarwanda + "sa", // Sanskrit + "sd", // Sindhi + "si", // Sinhala + "sk", // Slovak + "sl", // Slovenian + "sm", // Samoan + "sn", // Shona + "so", // Somali + "sq", // Albanian + "sr", // Serbian + "st", // Southern Sotho + "su", // Sundanese + "sv", // Swedish + "sw", // Swahili + "ta", // Tamil + "te", // Telugu + "tg", // Tajik + "th", // Thai + "ti", // Tigrinya + "tk", // Turkmen + "tl", // Tagalog - Chrome uses "fil" + "tr", // Turkish + "ts", // Tsonga + "tt", // Tatar + "ug", // Uyghur + "uk", // Ukrainian + "ur", // Urdu + "uz", // Uzbek + "vi", // Vietnamese + "xh", // Xhosa + "yi", // Yiddish + "yo", // Yoruba + "zh-CN", // Chinese (Simplified) + "zh-TW", // Chinese (Traditional) + "zu", // Zulu + ].map({ + Locale.Language.init(identifier: $0) + }) + } + + private func languageName(for language: Locale.Language) -> String { + if let languageCode = language.languageCode?.identifier, + let languageName = Locale.current.localizedString(forLanguageCode: languageCode) + { + return languageName + } + return "Unknown Language" + } +} + struct TranslateToast: View { @Environment(\.dismiss) private var dismiss + @State + private var showSourceLanguageSelection: Bool = false + + @State + private var showTargetLanguageSelection: Bool = false + + @ObservedObject var languageInfo: BraveTranslateLanguageInfo var currentLanguageName: String { @@ -35,31 +251,90 @@ struct TranslateToast: View { Image(braveSystemName: "leo.product.translate") .symbolRenderingMode(.monochrome) .foregroundStyle( - LinearGradient( - braveGradient: .init( - stops: [ - .init(color: UIColor(rgb: 0xFA7250), position: 0.0), - .init(color: UIColor(rgb: 0xFF1893), position: 0.43), - .init(color: UIColor(rgb: 0xA78AFF), position: 1.0), - ], - angle: .figmaDegrees(314.42) - ) - ) + LinearGradient(braveSystemName: .iconsActive) ) .padding(.trailing) - VStack { - Text("Page Translations") + VStack(alignment: .leading) { + Text("Page Translated") .font(.callout.weight(.semibold)) .foregroundColor(Color(braveSystemName: .textPrimary)) + .padding(.bottom, 8.0) - Text( - "\(pageLanguageName) To \(currentLanguageName)" - ) - .font(.callout) - .foregroundColor(Color(braveSystemName: .textSecondary)) + HStack { + Button { + showSourceLanguageSelection = true + } label: { + Text( + "\(pageLanguageName)" + ) + .font(.callout) + .foregroundColor(Color(braveSystemName: .textSecondary)) + .padding([.top, .bottom], 4.0) + .padding([.leading, .trailing], 8.0) + .background( + ContainerRelativeShape() + .fill(Color(braveSystemName: .pageBackground)) + ) + .containerShape(RoundedRectangle(cornerRadius: 4.0, style: .continuous)) + } + + Text( + "To" + ) + .font(.callout) + .foregroundColor(Color(braveSystemName: .textSecondary)) + + Button { + showTargetLanguageSelection = true + } label: { + Text( + "\(currentLanguageName)" + ) + .font(.callout) + .foregroundColor(Color(braveSystemName: .textSecondary)) + .padding([.top, .bottom], 4.0) + .padding([.leading, .trailing], 8.0) + .background( + ContainerRelativeShape() + .fill(Color(braveSystemName: .pageBackground)) + ) + .containerShape(RoundedRectangle(cornerRadius: 4.0, style: .continuous)) + } + } } } .padding() + .frame(alignment: .leading) + .bravePopup(isPresented: $showTargetLanguageSelection) { + TranslationOptionsView( + language: Binding( + get: { + languageInfo.pageLanguage + }, + set: { + languageInfo.pageLanguage = $0 + } + ) + ) + .onDisappear { + showTargetLanguageSelection = false + } + } + .bravePopup(isPresented: $showSourceLanguageSelection) { + TranslationOptionsView( + language: Binding( + get: { + languageInfo.currentLanguage + }, + set: { + languageInfo.currentLanguage = $0 ?? Locale.current.language + } + ) + ) + .onDisappear { + showSourceLanguageSelection = false + } + } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveTranslateScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveTranslateScriptHandler.swift index 45b6ff47589a..427d8d691180 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveTranslateScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/BraveTranslateScriptHandler.swift @@ -152,6 +152,19 @@ class BraveTranslateScriptHandler: NSObject, TabContentScript { return nil } + // return WKUserScript( + // source: secureScript( + // handlerNamesMap: ["$": messageHandlerName, + // "$": kBraveServicesKey, + // "$": namespace], + // securityToken: scriptId, + // script: script + // ), + // injectionTime: .atDocumentEnd, + // forMainFrameOnly: true, + // in: scriptSandbox + // ) + // HACKS! Need a better way to do this. // Chromium Scripts do NOT have a secure message handler and cannot be sandboxed the same way! return WKUserScript( @@ -421,10 +434,6 @@ class BraveTranslateScriptHandler: NSObject, TabContentScript { } } - if body["command"] as? String == "status" { - print(body) - } - replyHandler(nil, nil) } }