mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 01:08:39 +09:00
319 lines
11 KiB
Swift
319 lines
11 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 SnapKit
|
|
import Shared
|
|
|
|
class SnackBarUX {
|
|
static var MaxWidth: CGFloat = 400
|
|
}
|
|
|
|
/**
|
|
* A specialized version of UIButton for use in SnackBars. These are displayed evenly
|
|
* spaced in the bottom of the bar. The main convenience of these is that you can pass
|
|
* in a callback in the constructor (although these also style themselves appropriately).
|
|
*
|
|
*``SnackButton(title: "OK", { _ in print("OK", terminator: "\n") })``
|
|
*/
|
|
class SnackButton: UIButton {
|
|
let callback: (_ bar: SnackBar) -> Void
|
|
fileprivate var bar: SnackBar!
|
|
|
|
/**
|
|
* An image to show as the background when a button is pressed. This is currently a 1x1 pixel blue color
|
|
*/
|
|
lazy var highlightImg: UIImage = {
|
|
let size = CGSize(width: 1, height: 1)
|
|
return UIImage.createWithColor(size, color: UIConstants.HighlightColor)
|
|
}()
|
|
|
|
init(title: String, accessibilityIdentifier: String, callback: @escaping (_ bar: SnackBar) -> Void) {
|
|
self.callback = callback
|
|
|
|
super.init(frame: CGRect.zero)
|
|
|
|
setTitle(title, for: UIControlState())
|
|
titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultMediumFont
|
|
setBackgroundImage(highlightImg, for: .highlighted)
|
|
setTitleColor(UIConstants.HighlightText, for: .highlighted)
|
|
|
|
addTarget(self, action: #selector(SnackButton.onClick), for: .touchUpInside)
|
|
|
|
self.accessibilityIdentifier = accessibilityIdentifier
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
self.callback = { bar in }
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func onClick() {
|
|
callback(bar)
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Presents some information to the user. Can optionally include some buttons and an image. Usage:
|
|
*
|
|
* ``let bar = SnackBar(text: "This is some text in the snackbar.",
|
|
* img: UIImage(named: "bookmark"),
|
|
* buttons: [
|
|
* SnackButton(title: "OK", { _ in print("OK", terminator: "\n") }),
|
|
* SnackButton(title: "Cancel", { _ in print("Cancel", terminator: "\n") }),
|
|
* SnackButton(title: "Maybe", { _ in print("Maybe", terminator: "\n") })
|
|
* ]
|
|
* )``
|
|
*/
|
|
class SnackBar: UIView {
|
|
let imageView: UIImageView
|
|
let textLabel: UILabel
|
|
let contentView: UIView
|
|
let backgroundView: UIView
|
|
let buttonsView: Toolbar
|
|
fileprivate var buttons = [SnackButton]()
|
|
// The Constraint for the bottom of this snackbar. We use this to transition it
|
|
var bottom: Constraint?
|
|
|
|
convenience init(text: String, img: UIImage?, buttons: [SnackButton]?) {
|
|
var attributes = [String: AnyObject]()
|
|
attributes[NSFontAttributeName] = DynamicFontHelper.defaultHelper.DefaultMediumFont
|
|
attributes[NSBackgroundColorAttributeName] = UIColor.clear
|
|
let attrText = NSAttributedString(string: text, attributes: attributes)
|
|
self.init(attrText: attrText, img: img, buttons: buttons)
|
|
}
|
|
|
|
init(attrText: NSAttributedString, img: UIImage?, buttons: [SnackButton]?) {
|
|
imageView = UIImageView()
|
|
textLabel = UILabel()
|
|
contentView = UIView()
|
|
buttonsView = Toolbar()
|
|
backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.extraLight))
|
|
|
|
super.init(frame: CGRect.zero)
|
|
|
|
imageView.image = img
|
|
textLabel.attributedText = attrText
|
|
if let buttons = buttons {
|
|
for button in buttons {
|
|
addButton(button)
|
|
}
|
|
}
|
|
setup()
|
|
}
|
|
|
|
fileprivate override init(frame: CGRect) {
|
|
imageView = UIImageView()
|
|
textLabel = UILabel()
|
|
contentView = UIView()
|
|
buttonsView = Toolbar()
|
|
backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.extraLight))
|
|
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
fileprivate func setup() {
|
|
textLabel.backgroundColor = nil
|
|
|
|
addSubview(backgroundView)
|
|
addSubview(contentView)
|
|
contentView.addSubview(imageView)
|
|
contentView.addSubview(textLabel)
|
|
addSubview(buttonsView)
|
|
|
|
self.backgroundColor = UIColor.clear
|
|
buttonsView.drawTopBorder = true
|
|
buttonsView.drawBottomBorder = false
|
|
buttonsView.drawSeperators = true
|
|
|
|
imageView.contentMode = UIViewContentMode.left
|
|
|
|
textLabel.font = DynamicFontHelper.defaultHelper.DefaultMediumFont
|
|
textLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
|
|
textLabel.numberOfLines = 0
|
|
textLabel.backgroundColor = UIColor.clear
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
let imageWidth: CGFloat
|
|
if let img = imageView.image {
|
|
imageWidth = img.size.width + UIConstants.DefaultPadding * 2
|
|
} else {
|
|
imageWidth = 0
|
|
}
|
|
self.textLabel.preferredMaxLayoutWidth = contentView.frame.width - (imageWidth + UIConstants.DefaultPadding)
|
|
super.layoutSubviews()
|
|
}
|
|
|
|
fileprivate func drawLine(_ context: CGContext, start: CGPoint, end: CGPoint) {
|
|
context.setStrokeColor(UIConstants.BorderColor.cgColor)
|
|
context.setLineWidth(1)
|
|
context.move(to: CGPoint(x: start.x, y: start.y))
|
|
context.addLine(to: CGPoint(x: end.x, y: end.y))
|
|
context.strokePath()
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
let context = UIGraphicsGetCurrentContext()
|
|
drawLine(context!, start: CGPoint(x: 0, y: 1), end: CGPoint(x: frame.size.width, y: 1))
|
|
}
|
|
|
|
/**
|
|
* Called to check if the snackbar should be removed or not. By default, Snackbars persist forever.
|
|
* Override this class or use a class like CountdownSnackbar if you want things expire
|
|
* - returns: true if the snackbar should be kept alive
|
|
*/
|
|
func shouldPersist(_ tab: Tab) -> Bool {
|
|
return true
|
|
}
|
|
|
|
override func updateConstraints() {
|
|
super.updateConstraints()
|
|
|
|
backgroundView.snp.remakeConstraints { make in
|
|
make.bottom.left.right.equalTo(self)
|
|
// Offset it by the width of the top border line so we can see the line from the super view
|
|
make.top.equalTo(self).offset(1)
|
|
}
|
|
|
|
contentView.snp.remakeConstraints { make in
|
|
make.top.left.right.equalTo(self).inset(UIEdgeInsets(equalInset: UIConstants.DefaultPadding))
|
|
}
|
|
|
|
if let img = imageView.image {
|
|
imageView.snp.remakeConstraints { make in
|
|
make.left.centerY.equalTo(contentView)
|
|
// To avoid doubling the padding, the textview doesn't have an inset on its left side.
|
|
// Instead, it relies on the imageView to tell it where its left side should be.
|
|
make.width.equalTo(img.size.width + UIConstants.DefaultPadding)
|
|
make.height.equalTo(img.size.height + UIConstants.DefaultPadding)
|
|
}
|
|
} else {
|
|
imageView.snp.remakeConstraints { make in
|
|
make.width.height.equalTo(0)
|
|
make.top.left.equalTo(self)
|
|
make.bottom.lessThanOrEqualTo(contentView.snp.bottom)
|
|
}
|
|
}
|
|
|
|
textLabel.snp.remakeConstraints { make in
|
|
make.top.equalTo(contentView)
|
|
make.left.equalTo(self.imageView.snp.right)
|
|
make.trailing.equalTo(contentView)
|
|
make.bottom.lessThanOrEqualTo(contentView.snp.bottom)
|
|
}
|
|
|
|
buttonsView.snp.remakeConstraints { make in
|
|
make.top.equalTo(contentView.snp.bottom).offset(UIConstants.DefaultPadding)
|
|
make.bottom.equalTo(self.snp.bottom)
|
|
make.left.right.equalTo(self)
|
|
if self.buttonsView.subviews.count > 0 {
|
|
make.height.equalTo(UIConstants.SnackbarButtonHeight)
|
|
} else {
|
|
make.height.equalTo(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
var showing: Bool {
|
|
return alpha != 0 && self.superview != nil
|
|
}
|
|
|
|
/**
|
|
* Helper for animating the Snackbar showing on screen.
|
|
*/
|
|
func show() {
|
|
alpha = 1
|
|
bottom?.update(offset: 0)
|
|
}
|
|
|
|
/**
|
|
* Helper for animating the Snackbar leaving the screen.
|
|
*/
|
|
func hide() {
|
|
alpha = 0
|
|
var h = frame.height
|
|
if h == 0 {
|
|
h = UIConstants.ToolbarHeight
|
|
}
|
|
bottom?.update(offset: h)
|
|
}
|
|
|
|
fileprivate func addButton(_ snackButton: SnackButton) {
|
|
snackButton.bar = self
|
|
buttonsView.addButtons([snackButton])
|
|
buttonsView.setNeedsUpdateConstraints()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A special version of a snackbar that persists for at least a timeout. After that
|
|
* it will dismiss itself on the next page load where this tab isn't showing. As long as
|
|
* you stay on the current tab though, it will persist until you interact with it.
|
|
*/
|
|
class TimerSnackBar: SnackBar {
|
|
fileprivate var prevURL: URL?
|
|
fileprivate var timer: Timer?
|
|
fileprivate var timeout: TimeInterval
|
|
|
|
init(timeout: TimeInterval = 10, attrText: NSAttributedString, img: UIImage?, buttons: [SnackButton]?) {
|
|
self.timeout = timeout
|
|
super.init(attrText: attrText, img: img, buttons: buttons)
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
self.timeout = 0
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
static func showAppStoreConfirmationBar(forTab tab: Tab, appStoreURL: URL) {
|
|
let msg = NSAttributedString(string: Strings.ExternalLinkAppStoreConfirmationTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
|
|
let bar = TimerSnackBar(attrText: msg,
|
|
img: UIImage(named: "defaultFavicon"),
|
|
buttons: [
|
|
SnackButton(title: UIConstants.OKString, accessibilityIdentifier: "ConfirmOpenInAppStore", callback: { bar in
|
|
tab.removeSnackbar(bar)
|
|
UIApplication.shared.openURL(appStoreURL)
|
|
}),
|
|
SnackButton(title: UIConstants.CancelString, accessibilityIdentifier: "CancelOpenInAppStore", callback: { bar in
|
|
tab.removeSnackbar(bar)
|
|
})
|
|
])
|
|
|
|
tab.addSnackbar(bar)
|
|
}
|
|
|
|
override func show() {
|
|
self.timer = Timer(timeInterval: timeout, target: self, selector: #selector(TimerSnackBar.SELTimerDone), userInfo: nil, repeats: false)
|
|
RunLoop.current.add(self.timer!, forMode: RunLoopMode.defaultRunLoopMode)
|
|
super.show()
|
|
}
|
|
|
|
@objc
|
|
func SELTimerDone() {
|
|
self.timer = nil
|
|
}
|
|
|
|
override func shouldPersist(_ tab: Tab) -> Bool {
|
|
if !showing {
|
|
return timer != nil
|
|
}
|
|
|
|
return super.shouldPersist(tab)
|
|
}
|
|
}
|