mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-19 23:07:33 +09:00
496 lines
20 KiB
Swift
496 lines
20 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 Storage
|
|
import SnapKit
|
|
import Shared
|
|
|
|
private struct PhotonActionSheetUX {
|
|
static let MaxWidth: CGFloat = 414
|
|
static let Padding: CGFloat = 10
|
|
static let SectionVerticalPadding: CGFloat = 13
|
|
static let HeaderHeight: CGFloat = 80
|
|
static let RowHeight: CGFloat = 44
|
|
static let LabelColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x353535)
|
|
static let DescriptionLabelColor = UIColor(rgb: 0x919191)
|
|
static let PlaceholderImage = UIImage(named: "defaultTopSiteIcon")
|
|
static let BorderWidth: CGFloat = 0.5
|
|
static let BorderColor = UIColor(white: 0, alpha: 0.1)
|
|
static let CornerRadius: CGFloat = 10
|
|
static let SiteImageViewSize = 52
|
|
static let IconSize = CGSize(width: 24, height: 24)
|
|
static let HeaderName = "PhotonActionSheetHeaderView"
|
|
static let CellName = "PhotonActionSheetCell"
|
|
static let CancelButtonHeight: CGFloat = 56
|
|
static let TablePadding: CGFloat = 6
|
|
}
|
|
|
|
public struct PhotonActionSheetItem {
|
|
public fileprivate(set) var title: String
|
|
public fileprivate(set) var iconString: String
|
|
public fileprivate(set) var isEnabled: Bool // Used by toggles like nightmode to switch tint color
|
|
public fileprivate(set) var handler: ((PhotonActionSheetItem) -> Void)?
|
|
|
|
init(title: String, iconString: String, isEnabled: Bool = false, handler: ((PhotonActionSheetItem) -> Void)?) {
|
|
self.title = title
|
|
self.iconString = iconString
|
|
self.isEnabled = isEnabled
|
|
self.handler = handler
|
|
}
|
|
}
|
|
|
|
private enum PresentationStyle {
|
|
case centered // used in the home panels
|
|
case bottom // used to display the menu
|
|
}
|
|
|
|
class PhotonActionSheet: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate {
|
|
fileprivate(set) var actions: [[PhotonActionSheetItem]]
|
|
|
|
private var site: Site?
|
|
private let style: PresentationStyle
|
|
private lazy var showCancelButton: Bool = {
|
|
return self.style == .bottom && self.modalPresentationStyle != .popover
|
|
}()
|
|
var tableView = UITableView(frame: CGRect.zero, style: .grouped)
|
|
private var tintColor = UIColor(rgb: 0x272727)
|
|
private var outerScrollView = UIScrollView()
|
|
|
|
lazy var tapRecognizer: UITapGestureRecognizer = {
|
|
let tapRecognizer = UITapGestureRecognizer()
|
|
tapRecognizer.addTarget(self, action: #selector(PhotonActionSheet.dismiss(_:)))
|
|
tapRecognizer.numberOfTapsRequired = 1
|
|
tapRecognizer.cancelsTouchesInView = false
|
|
tapRecognizer.delegate = self
|
|
return tapRecognizer
|
|
}()
|
|
|
|
lazy var cancelButton: UIButton = {
|
|
let button = UIButton()
|
|
button.setTitle(Strings.CancelButtonTitle, for: .normal)
|
|
button.backgroundColor = UIConstants.AppBackgroundColor
|
|
button.setTitleColor(UIConstants.SystemBlueColor, for: .normal)
|
|
button.layer.cornerRadius = PhotonActionSheetUX.CornerRadius
|
|
button.titleLabel?.font = DynamicFontHelper.defaultHelper.DeviceFontExtraLargeBold
|
|
button.addTarget(self, action: #selector(PhotonActionSheet.dismiss(_:)), for: .touchUpInside)
|
|
button.accessibilityIdentifier = "PhotonMenu.cancel"
|
|
return button
|
|
}()
|
|
|
|
init(site: Site, actions: [PhotonActionSheetItem]) {
|
|
self.site = site
|
|
self.actions = [actions]
|
|
self.style = .centered
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
init(actions: [[PhotonActionSheetItem]]) {
|
|
self.actions = actions
|
|
self.style = .bottom
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
var photonTransitionDelegate: UIViewControllerTransitioningDelegate? {
|
|
didSet {
|
|
self.transitioningDelegate = photonTransitionDelegate
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
if style == .centered {
|
|
applyBackgroundBlur()
|
|
self.tintColor = UIConstants.SystemBlueColor
|
|
}
|
|
view.addGestureRecognizer(tapRecognizer)
|
|
view.addSubview(tableView)
|
|
|
|
view.accessibilityIdentifier = "Action Sheet"
|
|
tableView.bounces = false
|
|
tableView.delegate = self
|
|
tableView.dataSource = self
|
|
tableView.sectionFooterHeight = 0
|
|
tableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.onDrag
|
|
tableView.register(PhotonActionSheetCell.self, forCellReuseIdentifier: PhotonActionSheetUX.CellName)
|
|
tableView.register(PhotonActionSheetHeaderView.self, forHeaderFooterViewReuseIdentifier: PhotonActionSheetUX.HeaderName)
|
|
tableView.register(PhotonActionSheetSeparator.self, forHeaderFooterViewReuseIdentifier: "SeparatorSectionHeader")
|
|
tableView.isScrollEnabled = true
|
|
tableView.showsVerticalScrollIndicator = false
|
|
tableView.layer.cornerRadius = PhotonActionSheetUX.CornerRadius
|
|
tableView.separatorStyle = .none
|
|
tableView.cellLayoutMarginsFollowReadableWidth = false
|
|
tableView.accessibilityIdentifier = "Context Menu"
|
|
let footer = UIView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: tableView.frame.width, height: PhotonActionSheetUX.TablePadding)))
|
|
tableView.tableFooterView = footer
|
|
tableView.tableHeaderView = footer.clone()
|
|
|
|
// In a popover the popover provides the blur background
|
|
// Not using a background color allows the view to style correctly with the popover arrow
|
|
if self.popoverPresentationController == nil {
|
|
tableView.backgroundColor = UIConstants.AppBackgroundColor.withAlphaComponent(0.7)
|
|
let blurEffect = UIBlurEffect(style: .light)
|
|
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
|
tableView.backgroundView = blurEffectView
|
|
} else {
|
|
tableView.backgroundColor = .clear
|
|
}
|
|
|
|
let width = min(self.view.frame.size.width, PhotonActionSheetUX.MaxWidth) - (PhotonActionSheetUX.Padding * 2)
|
|
let height = actionSheetHeight()
|
|
|
|
if self.modalPresentationStyle == .popover {
|
|
self.preferredContentSize = CGSize(width: width, height: height)
|
|
}
|
|
|
|
if self.showCancelButton {
|
|
self.view.addSubview(cancelButton)
|
|
cancelButton.snp.makeConstraints { make in
|
|
make.centerX.equalTo(self.view.snp.centerX)
|
|
make.width.equalTo(width)
|
|
make.height.equalTo(PhotonActionSheetUX.CancelButtonHeight)
|
|
if #available(iOS 11, *) {
|
|
let bottomPad: CGFloat
|
|
if let window = UIApplication.shared.keyWindow, window.safeAreaInsets.bottom != 0 {
|
|
// for iPhone X and similar
|
|
bottomPad = 0
|
|
} else {
|
|
bottomPad = PhotonActionSheetUX.Padding
|
|
}
|
|
make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-bottomPad)
|
|
} else {
|
|
make.bottom.equalTo(self.view.snp.bottom).offset(-PhotonActionSheetUX.Padding)
|
|
}
|
|
}
|
|
}
|
|
|
|
if style == .bottom && self.modalPresentationStyle == .popover {
|
|
// We are showing the menu in a popOver
|
|
self.actions = actions.map({ $0.reversed() }).reversed()
|
|
tableView.frame = CGRect(origin: CGPoint.zero, size: self.preferredContentSize)
|
|
return
|
|
}
|
|
|
|
tableView.snp.makeConstraints { make in
|
|
make.centerX.equalTo(self.view.snp.centerX)
|
|
switch style {
|
|
case .bottom:
|
|
make.bottom.equalTo(cancelButton.snp.top).offset(-PhotonActionSheetUX.Padding)
|
|
case .centered:
|
|
make.centerY.equalTo(self.view.snp.centerY)
|
|
}
|
|
make.width.equalTo(width)
|
|
make.height.equalTo(min(height, view.bounds.height - PhotonActionSheetUX.CancelButtonHeight - (PhotonActionSheetUX.Padding * 6)))
|
|
}
|
|
}
|
|
|
|
private func applyBackgroundBlur() {
|
|
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
|
if let screenshot = appDelegate.window?.screenshot() {
|
|
let blurredImage = screenshot.applyBlur(withRadius: 5,
|
|
blurType: BOXFILTER,
|
|
tintColor: UIColor.black.withAlphaComponent(0.2),
|
|
saturationDeltaFactor: 1.8,
|
|
maskImage: nil)
|
|
let imageView = UIImageView(image: blurredImage)
|
|
view.addSubview(imageView)
|
|
}
|
|
}
|
|
|
|
fileprivate func actionSheetHeight() -> CGFloat {
|
|
let count = actions.reduce(0) { $1.count + $0 }
|
|
let headerHeight = (style == .centered) ? PhotonActionSheetUX.HeaderHeight : 0
|
|
let separatorHeight = actions.count > 1 ? (actions.count - 1) * Int(PhotonActionSheetUX.SectionVerticalPadding) : 0
|
|
return CGFloat(separatorHeight) + headerHeight + ( PhotonActionSheetUX.TablePadding * 2) + CGFloat(count) * PhotonActionSheetUX.RowHeight
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
}
|
|
|
|
func dismiss(_ gestureRecognizer: UIGestureRecognizer?) {
|
|
self.dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
deinit {
|
|
tableView.dataSource = nil
|
|
tableView.delegate = nil
|
|
}
|
|
|
|
override func updateViewConstraints() {
|
|
if !self.showCancelButton {
|
|
tableView.frame = CGRect(origin: CGPoint.zero, size: self.preferredContentSize)
|
|
}
|
|
super.updateViewConstraints()
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
if self.traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass
|
|
|| self.traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
|
|
updateViewConstraints()
|
|
}
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
if tableView.frame.contains(touch.location(in: self.view)) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
return actions.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return actions[section].count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
let action = actions[indexPath.section][indexPath.row]
|
|
guard let handler = action.handler else {
|
|
self.dismiss(nil)
|
|
return
|
|
}
|
|
self.dismiss(nil)
|
|
return handler(action)
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, hasFullWidthSeparatorForRowAtIndexPath indexPath: IndexPath) -> Bool {
|
|
return false
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
return PhotonActionSheetUX.RowHeight
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
// If we have multiple sections show a separator for each one except the first.
|
|
if section > 0 {
|
|
return PhotonActionSheetUX.SectionVerticalPadding
|
|
}
|
|
return self.site != nil ? PhotonActionSheetUX.HeaderHeight : 0
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: PhotonActionSheetUX.CellName, for: indexPath) as! PhotonActionSheetCell
|
|
let action = actions[indexPath.section][indexPath.row]
|
|
|
|
cell.tintColor = action.isEnabled ? UIConstants.SystemBlueColor : self.tintColor
|
|
cell.configureCell(action.title, imageString: action.iconString)
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
// If we have multiple sections show a separator for each one except the first.
|
|
if section > 0 {
|
|
return tableView.dequeueReusableHeaderFooterView(withIdentifier: "SeparatorSectionHeader")
|
|
}
|
|
guard let site = site else {
|
|
return nil
|
|
}
|
|
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: PhotonActionSheetUX.HeaderName) as! PhotonActionSheetHeaderView
|
|
header.tintColor = self.tintColor
|
|
header.configureWithSite(site)
|
|
return header
|
|
}
|
|
}
|
|
|
|
private class PhotonActionSheetHeaderView: UITableViewHeaderFooterView {
|
|
static let Padding: CGFloat = 12
|
|
static let VerticalPadding: CGFloat = 2
|
|
|
|
lazy var titleLabel: UILabel = {
|
|
let titleLabel = UILabel()
|
|
titleLabel.font = DynamicFontHelper.defaultHelper.MediumSizeBoldFontAS
|
|
titleLabel.textAlignment = .left
|
|
titleLabel.numberOfLines = 2
|
|
return titleLabel
|
|
}()
|
|
|
|
lazy var descriptionLabel: UILabel = {
|
|
let titleLabel = UILabel()
|
|
titleLabel.font = DynamicFontHelper.defaultHelper.MediumSizeRegularWeightAS
|
|
titleLabel.textAlignment = .left
|
|
titleLabel.numberOfLines = 1
|
|
return titleLabel
|
|
}()
|
|
|
|
lazy var siteImageView: UIImageView = {
|
|
let siteImageView = UIImageView()
|
|
siteImageView.contentMode = UIViewContentMode.center
|
|
siteImageView.clipsToBounds = true
|
|
siteImageView.layer.cornerRadius = PhotonActionSheetUX.CornerRadius
|
|
siteImageView.layer.borderColor = PhotonActionSheetUX.BorderColor.cgColor
|
|
siteImageView.layer.borderWidth = PhotonActionSheetUX.BorderWidth
|
|
return siteImageView
|
|
}()
|
|
|
|
override init(reuseIdentifier: String?) {
|
|
super.init(reuseIdentifier: reuseIdentifier)
|
|
|
|
self.backgroundView = UIView()
|
|
self.backgroundView?.backgroundColor = .clear
|
|
contentView.addSubview(siteImageView)
|
|
|
|
siteImageView.snp.remakeConstraints { make in
|
|
make.centerY.equalTo(contentView)
|
|
make.leading.equalTo(contentView).offset(PhotonActionSheetHeaderView.Padding)
|
|
make.size.equalTo(PhotonActionSheetUX.SiteImageViewSize)
|
|
}
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel])
|
|
stackView.spacing = PhotonActionSheetHeaderView.VerticalPadding
|
|
stackView.alignment = .leading
|
|
stackView.axis = .vertical
|
|
|
|
contentView.addSubview(stackView)
|
|
|
|
stackView.snp.makeConstraints { make in
|
|
make.leading.equalTo(siteImageView.snp.trailing).offset(PhotonActionSheetHeaderView.Padding)
|
|
make.trailing.equalTo(contentView).inset(PhotonActionSheetHeaderView.Padding)
|
|
make.centerY.equalTo(siteImageView.snp.centerY)
|
|
}
|
|
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
self.siteImageView.image = nil
|
|
self.siteImageView.backgroundColor = UIColor.clear
|
|
}
|
|
|
|
func configureWithSite(_ site: Site) {
|
|
self.siteImageView.setFavicon(forSite: site) { (color, url) in
|
|
self.siteImageView.backgroundColor = color
|
|
self.siteImageView.image = self.siteImageView.image?.createScaled(PhotonActionSheetUX.IconSize)
|
|
}
|
|
self.titleLabel.text = site.title.characters.count <= 1 ? site.url : site.title
|
|
self.descriptionLabel.text = site.tileURL.baseDomain
|
|
}
|
|
}
|
|
|
|
private struct PhotonActionSheetCellUX {
|
|
static let LabelColor = UIConstants.SystemBlueColor
|
|
static let BorderWidth: CGFloat = CGFloat(0.5)
|
|
static let CellSideOffset = 20
|
|
static let TitleLabelOffset = 10
|
|
static let CellTopBottomOffset = 12
|
|
static let StatusIconSize = 24
|
|
static let SelectedOverlayColor = UIColor(white: 0.0, alpha: 0.25)
|
|
static let CornerRadius: CGFloat = 3
|
|
}
|
|
|
|
private class PhotonActionSheetSeparator: UITableViewHeaderFooterView {
|
|
|
|
let separatorLineView = UIView()
|
|
|
|
override init(reuseIdentifier: String?) {
|
|
super.init(reuseIdentifier: reuseIdentifier)
|
|
self.backgroundView = UIView()
|
|
self.backgroundView?.backgroundColor = .clear
|
|
separatorLineView.backgroundColor = UIColor.lightGray
|
|
self.contentView.addSubview(separatorLineView)
|
|
separatorLineView.snp.makeConstraints { make in
|
|
make.leading.trailing.equalTo(self)
|
|
make.centerY.equalTo(self)
|
|
make.height.equalTo(0.5)
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
private class PhotonActionSheetCell: UITableViewCell {
|
|
lazy var titleLabel: UILabel = {
|
|
let titleLabel = UILabel()
|
|
titleLabel.font = DynamicFontHelper.defaultHelper.LargeSizeRegularWeightAS
|
|
titleLabel.minimumScaleFactor = 0.75 // Scale the font if we run out of space
|
|
titleLabel.textColor = PhotonActionSheetCellUX.LabelColor
|
|
titleLabel.textAlignment = .left
|
|
titleLabel.numberOfLines = 1
|
|
titleLabel.adjustsFontSizeToFitWidth = true
|
|
return titleLabel
|
|
}()
|
|
|
|
lazy var statusIcon: UIImageView = {
|
|
let siteImageView = UIImageView()
|
|
siteImageView.contentMode = UIViewContentMode.scaleAspectFit
|
|
siteImageView.clipsToBounds = true
|
|
siteImageView.layer.cornerRadius = PhotonActionSheetCellUX.CornerRadius
|
|
return siteImageView
|
|
}()
|
|
|
|
lazy var selectedOverlay: UIView = {
|
|
let selectedOverlay = UIView()
|
|
selectedOverlay.backgroundColor = PhotonActionSheetCellUX.SelectedOverlayColor
|
|
selectedOverlay.isHidden = true
|
|
return selectedOverlay
|
|
}()
|
|
|
|
override var isSelected: Bool {
|
|
didSet {
|
|
self.selectedOverlay.isHidden = !isSelected
|
|
}
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
self.statusIcon.image = nil
|
|
}
|
|
|
|
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
isAccessibilityElement = true
|
|
|
|
contentView.addSubview(selectedOverlay)
|
|
contentView.addSubview(titleLabel)
|
|
contentView.addSubview(statusIcon)
|
|
backgroundColor = .clear
|
|
|
|
selectedOverlay.snp.makeConstraints { make in
|
|
make.edges.equalTo(contentView)
|
|
}
|
|
|
|
titleLabel.snp.makeConstraints { make in
|
|
make.leading.equalTo(statusIcon.snp.trailing).offset(16)
|
|
make.trailing.equalTo(contentView)
|
|
make.centerY.equalTo(contentView)
|
|
}
|
|
|
|
statusIcon.snp.makeConstraints { make in
|
|
make.size.equalTo(PhotonActionSheetCellUX.StatusIconSize)
|
|
make.leading.equalTo(contentView).offset(16)
|
|
make.centerY.equalTo(contentView)
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configureCell(_ label: String, imageString: String) {
|
|
titleLabel.text = label
|
|
titleLabel.textColor = self.tintColor
|
|
accessibilityIdentifier = imageString
|
|
accessibilityLabel = label
|
|
if let image = UIImage(named: imageString)?.withRenderingMode(.alwaysTemplate) {
|
|
statusIcon.image = image
|
|
statusIcon.tintColor = self.tintColor
|
|
}
|
|
}
|
|
}
|
|
|