Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View file

@ -0,0 +1,42 @@
/* 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 UIKit
extension CGRect {
var center: CGPoint {
get {
return CGPoint(x: size.width / 2, y: size.height / 2)
}
set {
self.origin = CGPoint(x: newValue.x - size.width / 2, y: newValue.y - size.height / 2)
}
}
}
extension UIEdgeInsets {
init(equalInset inset: CGFloat) {
top = inset
left = inset
right = inset
bottom = inset
}
}
/**
Generates the affine transform for transforming the first CGRect into the second one
- parameter frame: CGRect to transform from
- parameter toFrame: CGRect to transform to
- returns: CGAffineTransform that transforms the first CGRect into the second
*/
func CGAffineTransformMakeRectToRect(_ frame: CGRect, toFrame: CGRect) -> CGAffineTransform {
let scale = toFrame.size.width / frame.size.width
let tx = toFrame.origin.x + toFrame.width / 2 - (frame.origin.x + frame.width / 2)
let ty = toFrame.origin.y - frame.origin.y * scale * 2
let translation = CGAffineTransform(translationX: tx, y: ty)
let scaledAndTranslated = translation.scaledBy(x: scale, y: scale)
return scaledAndTranslated
}

View file

@ -0,0 +1,13 @@
/* 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
// MARK: - Common UITableView text styling
extension NSAttributedString {
static func tableRowTitle(_ string: String, enabled: Bool) -> NSAttributedString {
let color = enabled ? [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor] : [NSForegroundColorAttributeName: UIConstants.TableViewDisabledRowTextColor]
return NSAttributedString(string: string, attributes: color)
}
}

View file

@ -0,0 +1,55 @@
/* 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
/**
* Data structure containing metadata associated with a mailto: link. For additional details,
* see RFC 2368 https://tools.ietf.org/html/rfc2368
*/
public struct MailToMetadata {
public let to: String
public let headers: [String: String]
}
public extension URL {
/**
Extracts the metadata associated with a mailto: URL according to RFC 2368
https://tools.ietf.org/html/rfc2368
*/
func mailToMetadata() -> MailToMetadata? {
guard scheme == "mailto" else {
return nil
}
let urlString = absoluteString
// Extract 'to' value
let toStart = urlString.characters.index(urlString.startIndex, offsetBy: "mailto:".characters.count)
let toEnd = urlString.characters.index(of: "?") ?? urlString.endIndex
let to = urlString.substring(with: toStart..<toEnd)
guard toEnd != urlString.endIndex else {
return MailToMetadata(to: to, headers: [String: String]())
}
// Extract headers
let headersString = urlString.substring(with: urlString.index(toEnd, offsetBy: 1)..<urlString.endIndex)
var headers = [String: String]()
let headerComponents = headersString.components(separatedBy: "&")
headerComponents.forEach { headerPair in
let components = headerPair.components(separatedBy: "=")
guard components.count == 2 else {
return
}
let (hname, hvalue) = (components[0], components[1])
headers[hname] = hvalue
}
return MailToMetadata(to: to, headers: headers)
}
}

View file

@ -0,0 +1,190 @@
/* 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 UIKit
typealias UIAlertActionCallback = (UIAlertAction) -> Void
// MARK: - Extension methods for building specific UIAlertController instances used across the app
extension UIAlertController {
/**
Builds the Alert view that asks the user if they wish to opt into crash reporting.
- parameter sendReportCallback: Send report option handler
- parameter alwaysSendCallback: Always send option handler
- parameter dontSendCallback: Dont send option handler
- parameter neverSendCallback: Never send option handler
- returns: UIAlertController for opting into crash reporting after a crash occurred
*/
class func crashOptInAlert(
_ sendReportCallback: @escaping UIAlertActionCallback,
alwaysSendCallback: @escaping UIAlertActionCallback,
dontSendCallback: @escaping UIAlertActionCallback) -> UIAlertController {
let alert = UIAlertController(
title: NSLocalizedString("Oops! Firefox crashed", comment: "Title for prompt displayed to user after the app crashes"),
message: NSLocalizedString("Send a crash report so Mozilla can fix the problem?", comment: "Message displayed in the crash dialog above the buttons used to select when sending reports"),
preferredStyle: UIAlertControllerStyle.alert
)
let sendReport = UIAlertAction(
title: NSLocalizedString("Send Report", comment: "Used as a button label for crash dialog prompt"),
style: UIAlertActionStyle.default,
handler: sendReportCallback
)
let alwaysSend = UIAlertAction(
title: NSLocalizedString("Always Send", comment: "Used as a button label for crash dialog prompt"),
style: UIAlertActionStyle.default,
handler: alwaysSendCallback
)
let dontSend = UIAlertAction(
title: NSLocalizedString("Dont Send", comment: "Used as a button label for crash dialog prompt"),
style: UIAlertActionStyle.default,
handler: dontSendCallback
)
alert.addAction(sendReport)
alert.addAction(alwaysSend)
alert.addAction(dontSend)
return alert
}
/**
Builds the Alert view that asks the user if they wish to restore their tabs after a crash.
- parameter okayCallback: Okay option handler
- parameter noCallback: No option handler
- returns: UIAlertController for asking the user to restore tabs after a crash
*/
class func restoreTabsAlert(okayCallback: @escaping UIAlertActionCallback, noCallback: @escaping UIAlertActionCallback) -> UIAlertController {
let alert = UIAlertController(
title: NSLocalizedString("Well, this is embarrassing.", comment: "Restore Tabs Prompt Title"),
message: NSLocalizedString("Looks like Firefox crashed previously. Would you like to restore your tabs?", comment: "Restore Tabs Prompt Description"),
preferredStyle: UIAlertControllerStyle.alert
)
let noOption = UIAlertAction(
title: NSLocalizedString("No", comment: "Restore Tabs Negative Action"),
style: UIAlertActionStyle.cancel,
handler: noCallback
)
let okayOption = UIAlertAction(
title: NSLocalizedString("Okay", comment: "Restore Tabs Affirmative Action"),
style: UIAlertActionStyle.default,
handler: okayCallback
)
alert.addAction(okayOption)
alert.addAction(noOption)
return alert
}
class func clearPrivateDataAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController {
let alert = UIAlertController(
title: "",
message: NSLocalizedString("This action will clear all of your private data. It cannot be undone.", tableName: "ClearPrivateDataConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear their private data."),
preferredStyle: UIAlertControllerStyle.alert
)
let noOption = UIAlertAction(
title: NSLocalizedString("Cancel", tableName: "ClearPrivateDataConfirm", comment: "The cancel button when confirming clear private data."),
style: UIAlertActionStyle.cancel,
handler: nil
)
let okayOption = UIAlertAction(
title: NSLocalizedString("OK", tableName: "ClearPrivateDataConfirm", comment: "The button that clears private data."),
style: UIAlertActionStyle.destructive,
handler: okayCallback
)
alert.addAction(okayOption)
alert.addAction(noOption)
return alert
}
/**
Builds the Alert view that asks if the users wants to also delete history stored on their other devices.
- parameter okayCallback: Okay option handler.
- returns: UIAlertController for asking the user to restore tabs after a crash
*/
class func clearSyncedHistoryAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController {
let alert = UIAlertController(
title: "",
message: NSLocalizedString("This action will clear all of your private data, including history from your synced devices.", tableName: "ClearHistoryConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear history that's synced to another device."),
preferredStyle: UIAlertControllerStyle.alert
)
let noOption = UIAlertAction(
title: NSLocalizedString("Cancel", tableName: "ClearHistoryConfirm", comment: "The cancel button when confirming clear history."),
style: UIAlertActionStyle.cancel,
handler: nil
)
let okayOption = UIAlertAction(
title: NSLocalizedString("OK", tableName: "ClearHistoryConfirm", comment: "The confirmation button that clears history even when Sync is connected."),
style: UIAlertActionStyle.destructive,
handler: okayCallback
)
alert.addAction(okayOption)
alert.addAction(noOption)
return alert
}
/**
Creates an alert view to warn the user that their logins will either be completely deleted in the
case of local-only logins or deleted across synced devices in synced account logins.
- parameter deleteCallback: Block to run when delete is tapped.
- parameter hasSyncedLogins: Boolean indicating the user has logins that have been synced.
- returns: UIAlertController instance
*/
class func deleteLoginAlertWithDeleteCallback(
_ deleteCallback: @escaping UIAlertActionCallback,
hasSyncedLogins: Bool) -> UIAlertController {
let areYouSureTitle = NSLocalizedString("Are you sure?",
tableName: "LoginManager",
comment: "Prompt title when deleting logins")
let deleteLocalMessage = NSLocalizedString("Logins will be permanently removed.",
tableName: "LoginManager",
comment: "Prompt message warning the user that deleting non-synced logins will permanently remove them")
let deleteSyncedDevicesMessage = NSLocalizedString("Logins will be removed from all connected devices.",
tableName: "LoginManager",
comment: "Prompt message warning the user that deleted logins will remove logins from all connected devices")
let cancelActionTitle = NSLocalizedString("Cancel",
tableName: "LoginManager",
comment: "Prompt option for cancelling out of deletion")
let deleteActionTitle = NSLocalizedString("Delete",
tableName: "LoginManager",
comment: "Label for the button used to delete the current login.")
let deleteAlert: UIAlertController
if hasSyncedLogins {
deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteSyncedDevicesMessage, preferredStyle: .alert)
} else {
deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteLocalMessage, preferredStyle: .alert)
}
let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil)
let deleteAction = UIAlertAction(title: deleteActionTitle, style: .destructive, handler: deleteCallback)
deleteAlert.addAction(cancelAction)
deleteAlert.addAction(deleteAction)
return deleteAlert
}
}

View file

@ -0,0 +1,87 @@
/* 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 UIKit
import Storage
import SDWebImage
import Shared
public extension UIImageView {
public func setIcon(_ icon: Favicon?, forURL url: URL?, completed completionBlock: ((UIColor, URL?) -> Void)? = nil ) {
if let url = url, let defaultIcon = FaviconFetcher.getDefaultIconForURL(url: url) {
self.image = UIImage(contentsOfFile: defaultIcon.url)
self.backgroundColor = defaultIcon.color
completionBlock?(defaultIcon.color, url)
} else {
let imageURL = URL(string: icon?.url ?? "")
let defaults = defaultFavicon(url)
self.sd_setImage(with: imageURL, placeholderImage: defaults.image, options: []) {(img, err, _, _) in
guard let image = img, let dUrl = url, err == nil else {
self.backgroundColor = defaults.color
completionBlock?(defaults.color, url)
return
}
self.color(forImage: image, andURL: dUrl, completed: completionBlock)
}
}
}
/*
* Fetch a background color for a specfic favicon UIImage. It uses the URL to store the UIColor in memory for subsequent requests.
*/
private func color(forImage image: UIImage, andURL url: URL, completed completionBlock: ((UIColor, URL?) -> Void)? = nil) {
guard let domain = url.baseDomain else {
self.backgroundColor = .gray
completionBlock?(UIColor.gray, url)
return
}
if let color = FaviconFetcher.colors[domain] {
self.backgroundColor = color
completionBlock?(color, url)
} else {
image.getColors(scaleDownSize: CGSize(width: 25, height: 25)) {colors in
let isSame = [colors.primary, colors.secondary, colors.detail].every { $0 == colors.primary }
if isSame {
completionBlock?(UIColor.white, url)
FaviconFetcher.colors[domain] = UIColor.white
} else {
completionBlock?(colors.background, url)
FaviconFetcher.colors[domain] = colors.background
}
}
}
}
public func setFavicon(forSite site: Site, onCompletion completionBlock: ((UIColor, URL?) -> Void)? = nil ) {
self.setIcon(site.icon, forURL: site.tileURL, completed: completionBlock)
}
private func defaultFavicon(_ url: URL?) -> (image: UIImage, color: UIColor) {
if let url = url {
return (FaviconFetcher.getDefaultFavicon(url), FaviconFetcher.getDefaultColor(url))
} else {
return (FaviconFetcher.defaultFavicon, .white)
}
}
}
open class ImageOperation: NSObject, SDWebImageOperation {
open var cacheOperation: Operation?
var cancelled: Bool {
if let cacheOperation = cacheOperation {
return cacheOperation.isCancelled
}
return false
}
@objc open func cancel() {
if let cacheOperation = cacheOperation {
cacheOperation.cancel()
}
}
}

View file

@ -0,0 +1,63 @@
/* 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 Deferred
import Foundation
import MobileCoreServices
import Shared
import UIKit
extension UIPasteboard {
func addImageWithData(_ data: Data, forURL url: URL) {
let isGIF = UIImage.dataIsGIF(data)
// Setting pasteboard.items allows us to set multiple representations for the same item.
items = [[
kUTTypeURL as String: url,
imageTypeKey(isGIF): data
]]
}
fileprivate func imageTypeKey(_ isGIF: Bool) -> String {
return (isGIF ? kUTTypeGIF : kUTTypePNG) as String
}
private var syncURL: URL? {
if let string = UIPasteboard.general.string,
let url = URL(string: string), url.isWebPage() {
return url
} else {
return nil
}
}
/// Preferred method to get strings out of the clipboard.
/// When iCloud pasteboards are enabled, the usually fast, synchronous calls
/// become slow and synchronous causing very slow start up times.
func asyncString() -> Deferred<Maybe<String?>> {
return fetchAsync() {
return UIPasteboard.general.string
}
}
/// Preferred method to get URLs out of the clipboard.
/// We use Deferred<Maybe<T?>> to fit in to the rest of the Deferred<Maybe> tools
/// we already use; but use optionals instead of errorTypes, because not having a URL
/// on the clipboard isn't an error.
func asyncURL() -> Deferred<Maybe<URL?>> {
return fetchAsync() {
return self.syncURL
}
}
// Converts the potentially long running synchronous operation into an asynchronous one.
private func fetchAsync<T>(getter: @escaping () -> T) -> Deferred<Maybe<T>> {
let deferred = Deferred<Maybe<T>>()
DispatchQueue.global().async {
let value = getter()
deferred.fill(Maybe(success: value))
}
return deferred
}
}

View file

@ -0,0 +1,94 @@
/* 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
extension UIView {
/**
* Takes a screenshot of the view with the given size.
*/
func screenshot(_ size: CGSize, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? {
assert(0...1 ~= quality)
let offset = offset ?? CGPoint(x: 0, y: 0)
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale * quality)
drawHierarchy(in: CGRect(origin: offset, size: frame.size), afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
/**
* Takes a screenshot of the view with the given aspect ratio.
* An aspect ratio of 0 means capture the entire view.
*/
func screenshot(_ aspectRatio: CGFloat = 0, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? {
assert(aspectRatio >= 0)
var size: CGSize
if aspectRatio > 0 {
size = CGSize()
let viewAspectRatio = frame.width / frame.height
if viewAspectRatio > aspectRatio {
size.height = frame.height
size.width = size.height * aspectRatio
} else {
size.width = frame.width
size.height = size.width / aspectRatio
}
} else {
size = frame.size
}
return screenshot(size, offset: offset, quality: quality)
}
/*
* Performs a deep copy of the view. Does not copy constraints.
*/
func clone() -> UIView {
let data = NSKeyedArchiver.archivedData(withRootObject: self)
return NSKeyedUnarchiver.unarchiveObject(with: data) as! UIView
}
/**
* rounds the requested corners of a view with the provided radius
*/
func addRoundedCorners(_ cornersToRound: UIRectCorner, cornerRadius: CGSize, color: UIColor) {
let rect = bounds
let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: cornersToRound, cornerRadii: cornerRadius)
// Create the shape layer and set its path
let maskLayer = CAShapeLayer()
maskLayer.frame = rect
maskLayer.path = maskPath.cgPath
let roundedLayer = CALayer()
roundedLayer.backgroundColor = color.cgColor
roundedLayer.frame = rect
roundedLayer.mask = maskLayer
layer.insertSublayer(roundedLayer, at: 0)
backgroundColor = UIColor.clear
}
/**
This allows us to find the view in a current view hierarchy that is currently the first responder
*/
static func findSubViewWithFirstResponder(_ view: UIView) -> UIView? {
let subviews = view.subviews
if subviews.count == 0 {
return nil
}
for subview: UIView in subviews {
if subview.isFirstResponder {
return subview
}
return findSubViewWithFirstResponder(subview)
}
return nil
}
}