mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-21 03:43:16 +09:00
364 lines
15 KiB
Swift
364 lines
15 KiB
Swift
/* 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 http://mozilla.org/MPL/2.0/. */
|
|
|
|
import Foundation
|
|
import Shared
|
|
import SnapKit
|
|
import UIKit
|
|
import Storage
|
|
|
|
private struct HomePanelViewControllerUX {
|
|
// Height of the top panel switcher button toolbar.
|
|
static let ButtonContainerHeight: CGFloat = 40
|
|
static let ButtonContainerBorderColor = UIColor.black.withAlphaComponent(0.1)
|
|
static let BackgroundColorPrivateMode = UIConstants.PrivateModeAssistantToolbarBackgroundColor
|
|
static let ToolbarButtonDeselectedColorNormalMode = UIColor(white: 0.2, alpha: 0.5)
|
|
static let ToolbarButtonDeselectedColorPrivateMode = UIColor(white: 0.9, alpha: 1)
|
|
static let ButtonHighlightLineHeight: CGFloat = 2
|
|
static let ButtonSelectionAnimationDuration = 0.2
|
|
}
|
|
|
|
protocol HomePanelViewControllerDelegate: class {
|
|
func homePanelViewController(_ homePanelViewController: HomePanelViewController, didSelectURL url: URL, visitType: VisitType)
|
|
func homePanelViewController(_ HomePanelViewController: HomePanelViewController, didSelectPanel panel: Int)
|
|
func homePanelViewControllerDidRequestToSignIn(_ homePanelViewController: HomePanelViewController)
|
|
func homePanelViewControllerDidRequestToCreateAccount(_ homePanelViewController: HomePanelViewController)
|
|
func homePanelViewControllerDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool)
|
|
}
|
|
|
|
protocol HomePanel: class {
|
|
weak var homePanelDelegate: HomePanelDelegate? { get set }
|
|
}
|
|
|
|
struct HomePanelUX {
|
|
static let EmptyTabContentOffset = -180
|
|
}
|
|
|
|
protocol HomePanelDelegate: class {
|
|
func homePanelDidRequestToSignIn(_ homePanel: HomePanel)
|
|
func homePanelDidRequestToCreateAccount(_ homePanel: HomePanel)
|
|
func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool)
|
|
func homePanel(_ homePanel: HomePanel, didSelectURL url: URL, visitType: VisitType)
|
|
func homePanel(_ homePanel: HomePanel, didSelectURLString url: String, visitType: VisitType)
|
|
}
|
|
|
|
struct HomePanelState {
|
|
var selectedIndex: Int = 0
|
|
}
|
|
|
|
enum HomePanelType: Int {
|
|
case topSites = 0
|
|
case bookmarks = 1
|
|
case history = 2
|
|
case readingList = 3
|
|
|
|
var localhostURL: URL {
|
|
return URL(string: "#panel=\(self.rawValue)", relativeTo: UIConstants.AboutHomePage as URL)!
|
|
}
|
|
}
|
|
|
|
class HomePanelViewController: UIViewController, UITextFieldDelegate, HomePanelDelegate {
|
|
static let Themes: [String: Theme] = {
|
|
var themes = [String: Theme]()
|
|
var theme = Theme()
|
|
theme.backgroundColor = UIConstants.AppBackgroundColor
|
|
theme.buttonTintColor = UIColor(rgb: 0x7e7e7f)
|
|
theme.highlightButtonColor = UIConstants.HighlightBlue
|
|
themes[Theme.PrivateMode] = theme
|
|
|
|
theme = Theme()
|
|
theme.backgroundColor = UIConstants.AppBackgroundColor
|
|
theme.buttonTintColor = UIColor(rgb: 0x7e7e7f)
|
|
theme.highlightButtonColor = UIConstants.HighlightBlue
|
|
themes[Theme.NormalMode] = theme
|
|
|
|
return themes
|
|
}()
|
|
|
|
var profile: Profile!
|
|
var notificationToken: NSObjectProtocol!
|
|
var panels: [HomePanelDescriptor]!
|
|
var url: URL?
|
|
weak var delegate: HomePanelViewControllerDelegate?
|
|
|
|
fileprivate var buttonContainerView = UIStackView()
|
|
fileprivate var buttonContainerBottomBorderView: UIView!
|
|
fileprivate var controllerContainerView: UIView!
|
|
fileprivate var buttons: [UIButton] = []
|
|
fileprivate var highlightLine = UIView() //The line underneath a panel button that shows which one is selected
|
|
|
|
fileprivate var buttonTintColor: UIColor?
|
|
fileprivate var buttonSelectedTintColor: UIColor?
|
|
|
|
var homePanelState: HomePanelState {
|
|
return HomePanelState(selectedIndex: selectedPanel?.rawValue ?? 0)
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
view.backgroundColor = UIConstants.AppBackgroundColor
|
|
|
|
buttonContainerView.axis = .horizontal
|
|
buttonContainerView.alignment = .fill
|
|
buttonContainerView.distribution = .fillEqually
|
|
buttonContainerView.spacing = 14
|
|
buttonContainerView.clipsToBounds = true
|
|
buttonContainerView.accessibilityNavigationStyle = .combined
|
|
buttonContainerView.accessibilityLabel = NSLocalizedString("Panel Chooser", comment: "Accessibility label for the Home panel's top toolbar containing list of the home panels (top sites, bookmarsk, history, remote tabs, reading list).")
|
|
view.addSubview(buttonContainerView)
|
|
buttonContainerView.addSubview(highlightLine)
|
|
|
|
self.buttonContainerBottomBorderView = UIView()
|
|
self.view.addSubview(buttonContainerBottomBorderView)
|
|
buttonContainerBottomBorderView.backgroundColor = HomePanelViewControllerUX.ButtonContainerBorderColor
|
|
|
|
controllerContainerView = UIView()
|
|
view.addSubview(controllerContainerView)
|
|
|
|
buttonContainerView.snp.makeConstraints { make in
|
|
make.top.equalTo(self.view)
|
|
make.leading.trailing.equalTo(self.view).inset(14)
|
|
make.height.equalTo(HomePanelViewControllerUX.ButtonContainerHeight)
|
|
}
|
|
|
|
buttonContainerBottomBorderView.snp.makeConstraints { make in
|
|
make.top.equalTo(self.buttonContainerView.snp.bottom).offset(-1)
|
|
make.bottom.equalTo(self.buttonContainerView)
|
|
make.leading.trailing.equalToSuperview()
|
|
}
|
|
|
|
controllerContainerView.snp.makeConstraints { make in
|
|
make.top.equalTo(self.buttonContainerView.snp.bottom)
|
|
make.left.right.bottom.equalTo(self.view)
|
|
}
|
|
|
|
self.panels = HomePanels().enabledPanels
|
|
updateButtons()
|
|
|
|
// Gesture recognizer to dismiss the keyboard in the URLBarView when the buttonContainerView is tapped
|
|
let dismissKeyboardGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(HomePanelViewController.dismissKeyboard(_:)))
|
|
dismissKeyboardGestureRecognizer.cancelsTouchesInView = false
|
|
buttonContainerView.addGestureRecognizer(dismissKeyboardGestureRecognizer)
|
|
}
|
|
|
|
func dismissKeyboard(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
view.window?.rootViewController?.view.endEditing(true)
|
|
}
|
|
|
|
var selectedPanel: HomePanelType? = nil {
|
|
didSet {
|
|
if oldValue == selectedPanel {
|
|
// Prevent flicker, allocations, and disk access: avoid duplicate view controllers.
|
|
return
|
|
}
|
|
|
|
if let index = oldValue?.rawValue {
|
|
if index < buttons.count {
|
|
let currentButton = buttons[index]
|
|
currentButton.isSelected = false
|
|
currentButton.isUserInteractionEnabled = true
|
|
}
|
|
}
|
|
|
|
hideCurrentPanel()
|
|
|
|
if let index = selectedPanel?.rawValue {
|
|
if index < buttons.count {
|
|
let newButton = buttons[index]
|
|
newButton.isSelected = true
|
|
newButton.isUserInteractionEnabled = false
|
|
}
|
|
|
|
if index < panels.count {
|
|
let panel = self.panels[index].makeViewController(profile)
|
|
let accessibilityLabel = self.panels[index].accessibilityLabel
|
|
if let panelController = panel as? UINavigationController,
|
|
let rootPanel = panelController.viewControllers.first {
|
|
setupHomePanel(rootPanel, accessibilityLabel: accessibilityLabel)
|
|
self.showPanel(panelController)
|
|
} else {
|
|
setupHomePanel(panel, accessibilityLabel: accessibilityLabel)
|
|
self.showPanel(panel)
|
|
}
|
|
}
|
|
}
|
|
self.updateButtonTints()
|
|
}
|
|
}
|
|
|
|
func setupHomePanel(_ panel: UIViewController, accessibilityLabel: String) {
|
|
(panel as? HomePanel)?.homePanelDelegate = self
|
|
panel.view.accessibilityNavigationStyle = .combined
|
|
panel.view.accessibilityLabel = accessibilityLabel
|
|
}
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return UIStatusBarStyle.lightContent
|
|
}
|
|
|
|
fileprivate func hideCurrentPanel() {
|
|
if let panel = childViewControllers.first {
|
|
panel.willMove(toParentViewController: nil)
|
|
panel.beginAppearanceTransition(false, animated: false)
|
|
panel.view.removeFromSuperview()
|
|
panel.endAppearanceTransition()
|
|
panel.removeFromParentViewController()
|
|
}
|
|
}
|
|
|
|
fileprivate func showPanel(_ panel: UIViewController) {
|
|
addChildViewController(panel)
|
|
panel.beginAppearanceTransition(true, animated: false)
|
|
controllerContainerView.addSubview(panel.view)
|
|
panel.endAppearanceTransition()
|
|
panel.view.snp.makeConstraints { make in
|
|
make.top.equalTo(self.buttonContainerView.snp.bottom)
|
|
make.left.right.bottom.equalTo(self.view)
|
|
}
|
|
panel.didMove(toParentViewController: self)
|
|
}
|
|
|
|
func tappedButton(_ sender: UIButton!) {
|
|
for (index, button) in buttons.enumerated() where button == sender {
|
|
selectedPanel = HomePanelType(rawValue: index)
|
|
delegate?.homePanelViewController(self, didSelectPanel: index)
|
|
if selectedPanel == .bookmarks {
|
|
UnifiedTelemetry.recordEvent(category: .action, method: .view, object: .bookmarksPanel, value: .homePanelTabButton)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
fileprivate func updateButtons() {
|
|
for panel in panels {
|
|
let button = UIButton()
|
|
button.addTarget(self, action: #selector(HomePanelViewController.tappedButton(_:)), for: .touchUpInside)
|
|
if let image = UIImage.templateImageNamed("panelIcon\(panel.imageName)") {
|
|
button.setImage(image, for: UIControlState.normal)
|
|
}
|
|
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 4, right: 0)
|
|
button.accessibilityLabel = panel.accessibilityLabel
|
|
button.accessibilityIdentifier = panel.accessibilityIdentifier
|
|
buttons.append(button)
|
|
self.buttonContainerView.addArrangedSubview(button)
|
|
}
|
|
}
|
|
|
|
func updateButtonTints() {
|
|
var selectedbutton: UIView?
|
|
for (index, button) in self.buttons.enumerated() {
|
|
if index == self.selectedPanel?.rawValue {
|
|
button.tintColor = self.buttonSelectedTintColor
|
|
selectedbutton = button
|
|
} else {
|
|
button.tintColor = self.buttonTintColor
|
|
}
|
|
}
|
|
guard let button = selectedbutton else {
|
|
return
|
|
}
|
|
|
|
// Calling this before makes sure that only the highlightline animates and not the homepanels
|
|
self.view.setNeedsUpdateConstraints()
|
|
self.view.layoutIfNeeded()
|
|
UIView.animate(withDuration: HomePanelViewControllerUX.ButtonSelectionAnimationDuration, delay: 0.0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.0, options: [], animations: { _ in
|
|
self.highlightLine.snp.remakeConstraints { make in
|
|
make.leading.equalTo(button.snp.leading)
|
|
make.trailing.equalTo(button.snp.trailing)
|
|
make.bottom.equalToSuperview()
|
|
make.height.equalTo(HomePanelViewControllerUX.ButtonHighlightLineHeight)
|
|
}
|
|
self.view.setNeedsUpdateConstraints()
|
|
self.view.layoutIfNeeded()
|
|
}, completion: nil)
|
|
}
|
|
|
|
func homePanel(_ homePanel: HomePanel, didSelectURLString url: String, visitType: VisitType) {
|
|
// If we can't get a real URL out of what should be a URL, we let the user's
|
|
// default search engine give it a shot.
|
|
// Typically we'll be in this state if the user has tapped a bookmarked search template
|
|
// (e.g., "http://foo.com/bar/?query=%s"), and this will get them the same behavior as if
|
|
// they'd copied and pasted into the URL bar.
|
|
// See BrowserViewController.urlBar:didSubmitText:.
|
|
guard let url = URIFixup.getURL(url) ?? profile.searchEngines.defaultEngine.searchURLForQuery(url) else {
|
|
Logger.browserLogger.warning("Invalid URL, and couldn't generate a search URL for it.")
|
|
return
|
|
}
|
|
|
|
return self.homePanel(homePanel, didSelectURL: url, visitType: visitType)
|
|
}
|
|
|
|
func homePanel(_ homePanel: HomePanel, didSelectURL url: URL, visitType: VisitType) {
|
|
delegate?.homePanelViewController(self, didSelectURL: url, visitType: visitType)
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
func homePanelDidRequestToCreateAccount(_ homePanel: HomePanel) {
|
|
delegate?.homePanelViewControllerDidRequestToCreateAccount(self)
|
|
}
|
|
|
|
func homePanelDidRequestToSignIn(_ homePanel: HomePanel) {
|
|
delegate?.homePanelViewControllerDidRequestToSignIn(self)
|
|
}
|
|
|
|
func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool) {
|
|
delegate?.homePanelViewControllerDidRequestToOpenInNewTab(url, isPrivate: isPrivate)
|
|
}
|
|
}
|
|
|
|
// MARK: UIAppearance
|
|
extension HomePanelViewController: Themeable {
|
|
func applyTheme(_ themeName: String) {
|
|
guard let theme = HomePanelViewController.Themes[themeName] else {
|
|
fatalError("Theme not found")
|
|
}
|
|
|
|
highlightLine.backgroundColor = theme.highlightButtonColor
|
|
buttonContainerView.backgroundColor = theme.backgroundColor
|
|
self.view.backgroundColor = theme.backgroundColor
|
|
buttonTintColor = theme.buttonTintColor
|
|
buttonSelectedTintColor = theme.highlightButtonColor
|
|
updateButtonTints()
|
|
}
|
|
}
|
|
|
|
protocol HomePanelContextMenu {
|
|
func getSiteDetails(for indexPath: IndexPath) -> Site?
|
|
func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]?
|
|
func presentContextMenu(for indexPath: IndexPath)
|
|
func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?)
|
|
}
|
|
|
|
extension HomePanelContextMenu {
|
|
func presentContextMenu(for indexPath: IndexPath) {
|
|
guard let site = getSiteDetails(for: indexPath) else { return }
|
|
|
|
presentContextMenu(for: site, with: indexPath, completionHandler: {
|
|
return self.contextMenu(for: site, with: indexPath)
|
|
})
|
|
}
|
|
|
|
func contextMenu(for site: Site, with indexPath: IndexPath) -> PhotonActionSheet? {
|
|
guard let actions = self.getContextMenuActions(for: site, with: indexPath) else { return nil }
|
|
|
|
let contextMenu = PhotonActionSheet(site: site, actions: actions)
|
|
contextMenu.modalPresentationStyle = .overFullScreen
|
|
contextMenu.modalTransitionStyle = .crossDissolve
|
|
|
|
return contextMenu
|
|
}
|
|
|
|
func getDefaultContextMenuActions(for site: Site, homePanelDelegate: HomePanelDelegate?) -> [PhotonActionSheetItem]? {
|
|
guard let siteURL = URL(string: site.url) else { return nil }
|
|
|
|
let openInNewTabAction = PhotonActionSheetItem(title: Strings.OpenInNewTabContextMenuTitle, iconString: "quick_action_new_tab") { action in
|
|
homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: false)
|
|
}
|
|
|
|
let openInNewPrivateTabAction = PhotonActionSheetItem(title: Strings.OpenInNewPrivateTabContextMenuTitle, iconString: "quick_action_new_private_tab") { action in
|
|
homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: true)
|
|
}
|
|
|
|
return [openInNewTabAction, openInNewPrivateTabAction]
|
|
}
|
|
}
|