Dactyloidae iOS initial commit
14
mobile/ios/Extensions/Entitlements/Fennec.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.mozilla.ios.Fennec</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.mozilla.ios.Fennec</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.mozilla.ios.Fennec.enterprise</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.mozilla.ios.Fennec.enterprise</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
14
mobile/ios/Extensions/Entitlements/Firefox.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.mozilla.ios.Firefox</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.mozilla.ios.Firefox</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
14
mobile/ios/Extensions/Entitlements/FirefoxBeta.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.mozilla.ios.FirefoxBeta</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.mozilla.ios.FirefoxBeta</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/* 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 Shared
|
||||
import Sync
|
||||
|
||||
// This is a cut down version of the Profile.
|
||||
// This will only ever be used in the NotificationService extension.
|
||||
// It allows us to customize the SyncDelegate, and later the SyncManager.
|
||||
class ExtensionProfile: BrowserProfile {
|
||||
init(localName: String) {
|
||||
super.init(localName: localName, clear: false)
|
||||
self.syncManager = ExtensionSyncManager(profile: self)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let extensionSafeNames = Set(["clients"])
|
||||
|
||||
// Mock class required by `BrowserProfile`
|
||||
open class PanelDataObservers {
|
||||
init(profile: Any) {}
|
||||
}
|
||||
|
||||
// Mock class required by `BrowserProfile`
|
||||
open class SearchEngines {
|
||||
init(prefs: Any, files: Any) {}
|
||||
}
|
||||
|
||||
class ExtensionSyncManager: BrowserProfile.BrowserSyncManager {
|
||||
init(profile: ExtensionProfile) {
|
||||
super.init(profile: profile)
|
||||
}
|
||||
|
||||
// We don't want to send ping data at all while we're in the extension.
|
||||
override func canSendUsageData() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// We should probably only want to sync client commands while we're in the extension.
|
||||
override func syncNamedCollections(why: SyncReason, names: [String]) -> Success {
|
||||
let names = names.filter { extensionSafeNames.contains($0) }
|
||||
return super.syncNamedCollections(why: why, names: names)
|
||||
}
|
||||
|
||||
override func takeActionsOnEngineStateChanges<T: EngineStateChanges>(_ changes: T) -> Deferred<Maybe<T>> {
|
||||
return deferMaybe(changes)
|
||||
}
|
||||
}
|
||||
33
mobile/ios/Extensions/NotificationService/Info.plist
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>MozDevelopmentTeam</key>
|
||||
<string>$(DEVELOPMENT_TEAM)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(MOZ_BUNDLE_DISPLAY_NAME)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
/* 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 Shared
|
||||
import Storage
|
||||
import Sync
|
||||
import UserNotifications
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var display: SyncDataDisplay?
|
||||
var profile: ExtensionProfile?
|
||||
|
||||
// This is run when an APNS notification with `mutable-content` is received.
|
||||
// If the app is backgrounded, then the alert notification is displayed.
|
||||
// If the app is foregrounded, then the notification.userInfo is passed straight to
|
||||
// AppDelegate.application(_:didReceiveRemoteNotification:completionHandler:)
|
||||
// Once the notification is tapped, then the same userInfo is passed to the same method in the AppDelegate.
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
let userInfo = request.content.userInfo
|
||||
|
||||
guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
|
||||
return self.didFinish(PushMessage.accountVerified)
|
||||
}
|
||||
|
||||
if self.profile == nil {
|
||||
self.profile = ExtensionProfile(localName: "profile")
|
||||
}
|
||||
|
||||
guard let profile = self.profile else {
|
||||
self.didFinish(with: .noProfile)
|
||||
return
|
||||
}
|
||||
|
||||
let queue = profile.queue
|
||||
let display = SyncDataDisplay(content: content, contentHandler: contentHandler, tabQueue: queue)
|
||||
self.display = display
|
||||
profile.syncDelegate = display
|
||||
|
||||
let handler = FxAPushMessageHandler(with: profile)
|
||||
|
||||
handler.handle(userInfo: userInfo).upon { res in
|
||||
self.didFinish(res.successValue, with: res.failureValue as? PushMessageError)
|
||||
}
|
||||
}
|
||||
|
||||
func didFinish(_ what: PushMessage? = nil, with error: PushMessageError? = nil) {
|
||||
profile?.shutdown()
|
||||
|
||||
guard let display = self.display else {
|
||||
return
|
||||
}
|
||||
|
||||
// We cannot use tabqueue after the profile has shutdown;
|
||||
// however, we can't use weak references, because TabQueue isn't a class.
|
||||
// Rather than changing tabQueue, we manually nil it out here.
|
||||
display.tabQueue = nil
|
||||
display.messageDelivered = false
|
||||
display.displayNotification(what, with: error)
|
||||
if !display.messageDelivered {
|
||||
display.displayUnknownMessageNotification()
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
didFinish(with: .timeout)
|
||||
}
|
||||
}
|
||||
|
||||
class SyncDataDisplay {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)
|
||||
var notificationContent: UNMutableNotificationContent
|
||||
var sentTabs: [SentTab]
|
||||
|
||||
var tabQueue: TabQueue?
|
||||
var messageDelivered: Bool = false
|
||||
|
||||
init(content: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void, tabQueue: TabQueue) {
|
||||
self.contentHandler = contentHandler
|
||||
self.notificationContent = content
|
||||
self.sentTabs = []
|
||||
self.tabQueue = tabQueue
|
||||
}
|
||||
|
||||
func displayNotification(_ message: PushMessage? = nil, with error: PushMessageError? = nil) {
|
||||
guard let message = message, error == nil else {
|
||||
return displayUnknownMessageNotification()
|
||||
}
|
||||
|
||||
switch message {
|
||||
case .accountVerified:
|
||||
displayAccountVerifiedNotification()
|
||||
case .deviceConnected(let deviceName):
|
||||
displayDeviceConnectedNotification(deviceName)
|
||||
case .deviceDisconnected(let deviceName):
|
||||
displayDeviceDisconnectedNotification(deviceName)
|
||||
case .thisDeviceDisconnected:
|
||||
displayThisDeviceDisconnectedNotification()
|
||||
case .collectionChanged(let collections):
|
||||
if collections.contains("clients") {
|
||||
displaySentTabNotification()
|
||||
} else {
|
||||
displayUnknownMessageNotification()
|
||||
}
|
||||
default:
|
||||
displayUnknownMessageNotification()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncDataDisplay {
|
||||
func displayDeviceConnectedNotification(_ deviceName: String) {
|
||||
presentNotification(title: Strings.FxAPush_DeviceConnected_title,
|
||||
body: Strings.FxAPush_DeviceConnected_body,
|
||||
bodyArg: deviceName)
|
||||
}
|
||||
|
||||
func displayDeviceDisconnectedNotification(_ deviceName: String?) {
|
||||
if let deviceName = deviceName {
|
||||
presentNotification(title: Strings.FxAPush_DeviceDisconnected_title,
|
||||
body: Strings.FxAPush_DeviceDisconnected_body,
|
||||
bodyArg: deviceName)
|
||||
} else {
|
||||
// We should never see this branch
|
||||
presentNotification(title: Strings.FxAPush_DeviceDisconnected_title,
|
||||
body: Strings.FxAPush_DeviceDisconnected_UnknownDevice_body)
|
||||
}
|
||||
}
|
||||
|
||||
func displayThisDeviceDisconnectedNotification() {
|
||||
presentNotification(title: Strings.FxAPush_DeviceDisconnected_ThisDevice_title,
|
||||
body: Strings.FxAPush_DeviceDisconnected_ThisDevice_body)
|
||||
}
|
||||
|
||||
func displayAccountVerifiedNotification() {
|
||||
presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body)
|
||||
}
|
||||
|
||||
func displayUnknownMessageNotification() {
|
||||
// if, by any change we haven't dealt with the message, then perhaps we
|
||||
// can recycle it as a sent tab message.
|
||||
if sentTabs.count > 0 {
|
||||
displaySentTabNotification()
|
||||
} else {
|
||||
presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncDataDisplay {
|
||||
func displaySentTabNotification() {
|
||||
// We will need to be more precise about calling these SentTab alerts
|
||||
// once we are a) detecting different types of notifications and b) adding actions.
|
||||
// For now, we need to add them so we can handle zero-tab sent-tab-notifications.
|
||||
notificationContent.categoryIdentifier = "org.mozilla.ios.SentTab.placeholder"
|
||||
|
||||
var userInfo = notificationContent.userInfo
|
||||
|
||||
// Add the tabs we've found to userInfo, so that the AppDelegate
|
||||
// doesn't have to do it again.
|
||||
let serializedTabs = sentTabs.flatMap { t -> NSDictionary? in
|
||||
return [
|
||||
"title": t.title,
|
||||
"url": t.url.absoluteString,
|
||||
"displayURL": t.url.absoluteDisplayExternalString,
|
||||
"deviceName": t.deviceName as Any,
|
||||
] as NSDictionary
|
||||
}
|
||||
|
||||
func present(_ tabs: [NSDictionary]) {
|
||||
if !tabs.isEmpty {
|
||||
userInfo["sentTabs"] = tabs as NSArray
|
||||
}
|
||||
notificationContent.userInfo = userInfo
|
||||
presentSentTabsNotification(tabs)
|
||||
}
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getDeliveredNotifications { notifications in
|
||||
// Let's deal with sent-tab-notifications
|
||||
let sentTabNotifications = notifications.filter {
|
||||
$0.request.content.categoryIdentifier == self.notificationContent.categoryIdentifier
|
||||
}
|
||||
|
||||
// We can delete zero tab sent-tab-notifications
|
||||
let emptyTabNotificationsIds = sentTabNotifications.filter {
|
||||
$0.request.content.userInfo["sentTabs"] == nil
|
||||
}.map { $0.request.identifier }
|
||||
center.removeDeliveredNotifications(withIdentifiers: emptyTabNotificationsIds)
|
||||
|
||||
// The one we've just received (but not delivered) may not have any tabs in it either
|
||||
// e.g. if the previous one consumed two tabs.
|
||||
if serializedTabs.count == 0 {
|
||||
// In that case, we try and recycle an existing notification (one that has a tab in it).
|
||||
if let firstNonEmpty = sentTabNotifications.first(where: { $0.request.content.userInfo["sentTabs"] != nil }),
|
||||
let previouslyDeliveredTabs = firstNonEmpty.request.content.userInfo["sentTabs"] as? [NSDictionary] {
|
||||
center.removeDeliveredNotifications(withIdentifiers: [firstNonEmpty.request.identifier])
|
||||
return present(previouslyDeliveredTabs)
|
||||
}
|
||||
}
|
||||
|
||||
// We have tabs in this notification, or we couldn't recycle an existing one that does.
|
||||
present(serializedTabs)
|
||||
}
|
||||
}
|
||||
|
||||
func presentSentTabsNotification(_ tabs: [NSDictionary]) {
|
||||
let title: String
|
||||
let body: String
|
||||
|
||||
if tabs.count == 0 {
|
||||
title = Strings.SentTab_NoTabArrivingNotification_title
|
||||
body = Strings.SentTab_NoTabArrivingNotification_body
|
||||
} else {
|
||||
let deviceNames = Set(tabs.flatMap { $0["deviceName"] as? String })
|
||||
if let deviceName = deviceNames.first, deviceNames.count == 1 {
|
||||
title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName)
|
||||
} else {
|
||||
title = Strings.SentTab_TabArrivingNotification_NoDevice_title
|
||||
}
|
||||
|
||||
if tabs.count == 1 {
|
||||
// We give the fallback string as the url,
|
||||
// because we have only just introduced "displayURL" as a key.
|
||||
body = (tabs[0]["displayURL"] as? String) ??
|
||||
(tabs[0]["url"] as! String)
|
||||
} else if deviceNames.count == 0 {
|
||||
body = Strings.SentTab_TabArrivingNotification_NoDevice_body
|
||||
} else {
|
||||
body = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_body, AppInfo.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
presentNotification(title: title, body: body)
|
||||
}
|
||||
|
||||
func presentNotification(title: String, body: String, titleArg: String? = nil, bodyArg: String? = nil) {
|
||||
func stringWithOptionalArg(_ s: String, _ a: String?) -> String {
|
||||
if let a = a {
|
||||
return String(format: s, a)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
notificationContent.title = stringWithOptionalArg(title, titleArg)
|
||||
notificationContent.body = stringWithOptionalArg(body, bodyArg)
|
||||
|
||||
// This is the only place we call the contentHandler.
|
||||
contentHandler(notificationContent)
|
||||
// This is the only place we change messageDelivered. We can check if contentHandler hasn't be called because of
|
||||
// our logic (rather than something funny with our environment, or iOS killing us).
|
||||
messageDelivered = true
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncDataDisplay: SyncDelegate {
|
||||
func displaySentTab(for url: URL, title: String, from deviceName: String?) {
|
||||
if url.isWebPage() {
|
||||
sentTabs.append(SentTab(url: url, title: title, deviceName: deviceName))
|
||||
|
||||
let item = ShareItem(url: url.absoluteString, title: title, favicon: nil)
|
||||
_ = tabQueue?.addToQueue(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SentTab {
|
||||
let url: URL
|
||||
let title: String
|
||||
let deviceName: String?
|
||||
}
|
||||
79
mobile/ios/Extensions/SendTo/ActionViewController.swift
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/* 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 Shared
|
||||
import Storage
|
||||
import SnapKit
|
||||
|
||||
/// The ActionViewController is the initial viewcontroller that is presented (full screen) when the share extension
|
||||
/// is activated. Depending on whether the user is logged in or not, this viewcontroller will present either the
|
||||
/// InstructionsVC or the ClientPicker VC.
|
||||
|
||||
@objc(ActionViewController)
|
||||
class ActionViewController: UIViewController, ClientPickerViewControllerDelegate, InstructionsViewControllerDelegate {
|
||||
private var sharedItem: ShareItem?
|
||||
|
||||
override func viewDidLoad() {
|
||||
view.backgroundColor = UIColor.white
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
if !hasAccount() {
|
||||
let instructionsViewController = InstructionsViewController()
|
||||
instructionsViewController.delegate = self
|
||||
let navigationController = UINavigationController(rootViewController: instructionsViewController)
|
||||
present(navigationController, animated: false, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
ExtensionUtils.extractSharedItemFromExtensionContext(self.extensionContext, completionHandler: { (item, error) -> Void in
|
||||
guard let item = item, error == nil, item.isShareable else {
|
||||
let alert = UIAlertController(title: Strings.SendToErrorTitle, message: Strings.SendToErrorMessage, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: Strings.SendToErrorOKButton, style: .default) { _ in self.finish() })
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.sharedItem = item
|
||||
let clientPickerViewController = ClientPickerViewController()
|
||||
clientPickerViewController.clientPickerDelegate = self
|
||||
clientPickerViewController.profile = nil // This means the picker will open and close the default profile
|
||||
let navigationController = UINavigationController(rootViewController: clientPickerViewController)
|
||||
self.present(navigationController, animated: false, completion: nil)
|
||||
})
|
||||
}
|
||||
|
||||
func finish() {
|
||||
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
|
||||
func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) {
|
||||
guard let item = sharedItem else {
|
||||
return finish()
|
||||
}
|
||||
|
||||
let profile = BrowserProfile(localName: "profile")
|
||||
profile.sendItems([item], toClients: clients).uponQueue(DispatchQueue.main) { result in
|
||||
profile.shutdown()
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) {
|
||||
finish()
|
||||
}
|
||||
|
||||
func instructionsViewControllerDidClose(_ instructionsViewController: InstructionsViewController) {
|
||||
finish()
|
||||
}
|
||||
|
||||
private func hasAccount() -> Bool {
|
||||
let profile = BrowserProfile(localName: "profile")
|
||||
defer {
|
||||
profile.shutdown()
|
||||
}
|
||||
return profile.hasAccount()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
"CFBundleDisplayName" = "Send Tab";
|
||||
320
mobile/ios/Extensions/SendTo/ClientPickerViewController.swift
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/* 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 Shared
|
||||
import Storage
|
||||
import SnapKit
|
||||
|
||||
protocol ClientPickerViewControllerDelegate {
|
||||
func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController)
|
||||
func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient])
|
||||
}
|
||||
|
||||
struct ClientPickerViewControllerUX {
|
||||
static let TableHeaderRowHeight = CGFloat(50)
|
||||
static let TableHeaderTextFont = UIFont.systemFont(ofSize: 16)
|
||||
static let TableHeaderTextColor = UIColor.gray
|
||||
static let TableHeaderTextPaddingLeft = CGFloat(20)
|
||||
|
||||
static let DeviceRowTintColor = UIColor(red: 0.427, green: 0.800, blue: 0.102, alpha: 1.0)
|
||||
static let DeviceRowHeight = CGFloat(50)
|
||||
static let DeviceRowTextFont = UIFont.systemFont(ofSize: 16)
|
||||
static let DeviceRowTextPaddingLeft = CGFloat(72)
|
||||
static let DeviceRowTextPaddingRight = CGFloat(50)
|
||||
}
|
||||
|
||||
/// The ClientPickerViewController displays a list of clients associated with the provided Account.
|
||||
/// The user can select a number of devices and hit the Send button.
|
||||
/// This viewcontroller does not implement any specific business logic that needs to happen with the selected clients.
|
||||
/// That is up to it's delegate, who can listen for cancellation and success events.
|
||||
|
||||
class ClientPickerViewController: UITableViewController {
|
||||
var profile: Profile?
|
||||
var profileNeedsShutdown = true
|
||||
|
||||
var clientPickerDelegate: ClientPickerViewControllerDelegate?
|
||||
|
||||
var reloading = true
|
||||
var clients: [RemoteClient] = []
|
||||
var selectedClients = NSMutableSet()
|
||||
|
||||
// ShareItem has been added as we are now using this class outside of the ShareTo extension to provide Share To functionality
|
||||
// And in this case we need to be able to store the item we are sharing as we may not have access to the
|
||||
// url later. Currently used only when sharing an item from the Tab Tray from a Preview Action.
|
||||
var shareItem: ShareItem?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = NSLocalizedString("Send Tab", tableName: "SendTo", comment: "Title of the dialog that allows you to send a tab to a different device")
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl?.addTarget(self, action: #selector(ClientPickerViewController.refresh), for: UIControlEvents.valueChanged)
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
title: Strings.SendToCancelButton,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(ClientPickerViewController.cancel)
|
||||
)
|
||||
|
||||
tableView.register(ClientPickerTableViewHeaderCell.self, forCellReuseIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier)
|
||||
tableView.register(ClientPickerTableViewCell.self, forCellReuseIdentifier: ClientPickerTableViewCell.CellIdentifier)
|
||||
tableView.register(ClientPickerNoClientsTableViewCell.self, forCellReuseIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier)
|
||||
tableView.tableFooterView = UIView(frame: CGRect.zero)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if let refreshControl = refreshControl {
|
||||
refreshControl.beginRefreshing()
|
||||
let height = -(refreshControl.bounds.size.height + (self.navigationController?.navigationBar.bounds.size.height ?? 0))
|
||||
self.tableView.contentOffset = CGPoint(x: 0, y: height)
|
||||
}
|
||||
reloadClients()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
if clients.count == 0 {
|
||||
return 1
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if clients.count == 0 {
|
||||
return 1
|
||||
} else {
|
||||
if section == 0 {
|
||||
return 1
|
||||
} else {
|
||||
return clients.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UITableViewCell
|
||||
|
||||
if clients.count > 0 {
|
||||
if indexPath.section == 0 {
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewHeaderCell
|
||||
} else {
|
||||
let clientCell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewCell
|
||||
clientCell.nameLabel.text = clients[indexPath.row].name
|
||||
clientCell.clientType = clients[indexPath.row].type == "mobile" ? ClientType.Mobile : ClientType.Desktop
|
||||
clientCell.checked = selectedClients.contains(indexPath)
|
||||
cell = clientCell
|
||||
}
|
||||
} else {
|
||||
if reloading == false {
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerNoClientsTableViewCell
|
||||
} else {
|
||||
cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "ClientCell")
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
return indexPath.section != 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if clients.count > 0 && indexPath.section == 1 {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
if selectedClients.contains(indexPath) {
|
||||
selectedClients.remove(indexPath)
|
||||
} else {
|
||||
selectedClients.add(indexPath)
|
||||
}
|
||||
|
||||
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
|
||||
|
||||
navigationItem.rightBarButtonItem?.isEnabled = (selectedClients.count != 0)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
if clients.count > 0 {
|
||||
if indexPath.section == 0 {
|
||||
return ClientPickerViewControllerUX.TableHeaderRowHeight
|
||||
} else {
|
||||
return ClientPickerViewControllerUX.DeviceRowHeight
|
||||
}
|
||||
} else {
|
||||
return tableView.frame.height
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func reloadClients() {
|
||||
// If we were not given a profile, open the default profile. This happens in case we are called from an app
|
||||
// extension. That also means that we need to shut down the profile, otherwise the app extension will be
|
||||
// terminated when it goes into the background.
|
||||
|
||||
if self.profile == nil {
|
||||
self.profile = BrowserProfile(localName: "profile")
|
||||
self.profileNeedsShutdown = true
|
||||
}
|
||||
|
||||
guard let profile = self.profile else {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-open the profile it was shutdown. This happens when we run from an app extension, where we must
|
||||
// make sure that the profile is only open for brief moments of time.
|
||||
|
||||
if profile.isShutdown {
|
||||
profile.reopen()
|
||||
}
|
||||
|
||||
reloading = true
|
||||
profile.getClients().upon({ result in
|
||||
withExtendedLifetime(profile) {
|
||||
// If we are running from an app extension then make sure we shut down the profile as soon as we are
|
||||
// done with it.
|
||||
|
||||
if self.profileNeedsShutdown {
|
||||
profile.shutdown()
|
||||
}
|
||||
|
||||
self.reloading = false
|
||||
guard let c = result.successValue else {
|
||||
return
|
||||
}
|
||||
|
||||
self.clients = c
|
||||
DispatchQueue.main.async {
|
||||
if self.clients.count == 0 {
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Send", tableName: "SendTo", comment: "Navigation bar button to Send the current page to a device"), style: UIBarButtonItemStyle.done, target: self, action: #selector(ClientPickerViewController.send))
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = false
|
||||
}
|
||||
self.selectedClients.removeAllObjects()
|
||||
self.tableView.reloadData()
|
||||
self.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
reloadClients()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
clientPickerDelegate?.clientPickerViewControllerDidCancel(self)
|
||||
}
|
||||
|
||||
func send() {
|
||||
var clients = [RemoteClient]()
|
||||
for indexPath in selectedClients {
|
||||
clients.append(self.clients[(indexPath as AnyObject).row])
|
||||
}
|
||||
clientPickerDelegate?.clientPickerViewController(self, didPickClients: clients)
|
||||
|
||||
// Replace the Send button with a loading indicator since it takes a while to sync
|
||||
// up our changes to the server.
|
||||
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 25, height: 25))
|
||||
loadingIndicator.color = .darkGray
|
||||
loadingIndicator.startAnimating()
|
||||
let customBarButton = UIBarButtonItem(customView: loadingIndicator)
|
||||
self.navigationItem.rightBarButtonItem = customBarButton
|
||||
}
|
||||
}
|
||||
|
||||
class ClientPickerTableViewHeaderCell: UITableViewCell {
|
||||
static let CellIdentifier = "ClientPickerTableViewSectionHeader"
|
||||
let nameLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
addSubview(nameLabel)
|
||||
nameLabel.font = ClientPickerViewControllerUX.TableHeaderTextFont
|
||||
nameLabel.text = NSLocalizedString("Available devices:", tableName: "SendTo", comment: "Header for the list of devices table")
|
||||
nameLabel.textColor = ClientPickerViewControllerUX.TableHeaderTextColor
|
||||
|
||||
nameLabel.snp.makeConstraints { (make) -> Void in
|
||||
make.left.equalTo(ClientPickerViewControllerUX.TableHeaderTextPaddingLeft)
|
||||
make.centerY.equalTo(self)
|
||||
make.right.equalTo(self)
|
||||
}
|
||||
|
||||
preservesSuperviewLayoutMargins = false
|
||||
layoutMargins = UIEdgeInsets.zero
|
||||
separatorInset = UIEdgeInsets.zero
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public enum ClientType: String {
|
||||
case Mobile = "deviceTypeMobile"
|
||||
case Desktop = "deviceTypeDesktop"
|
||||
}
|
||||
|
||||
class ClientPickerTableViewCell: UITableViewCell {
|
||||
static let CellIdentifier = "ClientPickerTableViewCell"
|
||||
|
||||
var nameLabel: UILabel
|
||||
var checked: Bool = false {
|
||||
didSet {
|
||||
self.accessoryType = checked ? UITableViewCellAccessoryType.checkmark : UITableViewCellAccessoryType.none
|
||||
}
|
||||
}
|
||||
|
||||
var clientType: ClientType = ClientType.Mobile {
|
||||
didSet {
|
||||
self.imageView?.image = UIImage(named: clientType.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
nameLabel = UILabel()
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(nameLabel)
|
||||
nameLabel.font = ClientPickerViewControllerUX.DeviceRowTextFont
|
||||
nameLabel.numberOfLines = 2
|
||||
nameLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
|
||||
self.tintColor = ClientPickerViewControllerUX.DeviceRowTintColor
|
||||
self.preservesSuperviewLayoutMargins = false
|
||||
self.selectionStyle = UITableViewCellSelectionStyle.none
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
nameLabel.snp.makeConstraints { (make) -> Void in
|
||||
make.left.equalTo(ClientPickerViewControllerUX.DeviceRowTextPaddingLeft)
|
||||
make.centerY.equalTo(self.snp.centerY)
|
||||
make.right.equalTo(self.snp.right).offset(-ClientPickerViewControllerUX.DeviceRowTextPaddingRight)
|
||||
}
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ClientPickerNoClientsTableViewCell: UITableViewCell {
|
||||
static let CellIdentifier = "ClientPickerNoClientsTableViewCell"
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupHelpView(contentView,
|
||||
introText: NSLocalizedString("You don’t have any other devices connected to this Firefox Account available to sync.", tableName: "SendTo", comment: "Error message shown in the remote tabs panel"),
|
||||
showMeText: "") // TODO We used to have a 'show me how to ...' text here. But, we cannot open web pages from the extension. So this is clear for now until we decide otherwise.
|
||||
// Move the separator off screen
|
||||
separatorInset = UIEdgeInsets(top: 0, left: 1000, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "send-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "send-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "send-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "send-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "send-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
23
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "deviceTypeDesktop.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "deviceTypeDesktop@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "deviceTypeDesktop@3x.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png
vendored
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 435 B |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 769 B |
23
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "deviceTypeMobile.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "deviceTypeMobile@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "deviceTypeMobile@3x.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png
vendored
Normal file
|
After Width: | Height: | Size: 312 B |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 471 B |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 868 B |
23
mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "emptySync.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "emptySync@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "emptySync@3x.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
48
mobile/ios/Extensions/SendTo/Info.plist
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>MozDevelopmentTeam</key>
|
||||
<string>$(DEVELOPMENT_TEAM)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>CFBundleDisplayName</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict/>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict/>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>FiraSans-Regular.ttf</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.ui-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>ActionViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/* 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 SnapKit
|
||||
|
||||
struct InstructionsViewControllerUX {
|
||||
static let TopPadding = CGFloat(20)
|
||||
static let TextFont = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
||||
static let TextColor = UIColor(rgb: 0x555555)
|
||||
static let LinkColor = UIColor.blue
|
||||
}
|
||||
|
||||
protocol InstructionsViewControllerDelegate: class {
|
||||
func instructionsViewControllerDidClose(_ instructionsViewController: InstructionsViewController)
|
||||
}
|
||||
|
||||
private func highlightLink(_ s: NSString, withColor color: UIColor) -> NSAttributedString {
|
||||
let start = s.range(of: "<")
|
||||
if start.location == NSNotFound {
|
||||
return NSAttributedString(string: s as String)
|
||||
}
|
||||
|
||||
var s: NSString = s.replacingCharacters(in: start, with: "") as NSString
|
||||
let end = s.range(of: ">")
|
||||
s = s.replacingCharacters(in: end, with: "") as NSString
|
||||
let a = NSMutableAttributedString(string: s as String)
|
||||
let r = NSRange(location: start.location, length: end.location-start.location)
|
||||
a.addAttribute(NSForegroundColorAttributeName, value: color, range: r)
|
||||
return a
|
||||
}
|
||||
|
||||
func setupHelpView(_ view: UIView, introText: String, showMeText: String) {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(named: "emptySync")
|
||||
view.addSubview(imageView)
|
||||
imageView.snp.makeConstraints { (make) -> Void in
|
||||
make.top.equalTo(view).offset(InstructionsViewControllerUX.TopPadding)
|
||||
make.centerX.equalTo(view)
|
||||
}
|
||||
|
||||
let label1 = UILabel()
|
||||
view.addSubview(label1)
|
||||
label1.text = introText
|
||||
label1.numberOfLines = 0
|
||||
label1.lineBreakMode = NSLineBreakMode.byWordWrapping
|
||||
label1.font = InstructionsViewControllerUX.TextFont
|
||||
label1.textColor = InstructionsViewControllerUX.TextColor
|
||||
label1.textAlignment = NSTextAlignment.center
|
||||
label1.snp.makeConstraints { (make) -> Void in
|
||||
make.width.equalTo(250)
|
||||
make.top.equalTo(imageView.snp.bottom).offset(InstructionsViewControllerUX.TopPadding)
|
||||
make.centerX.equalTo(view)
|
||||
}
|
||||
|
||||
let label2 = UILabel()
|
||||
view.addSubview(label2)
|
||||
label2.numberOfLines = 0
|
||||
label2.lineBreakMode = NSLineBreakMode.byWordWrapping
|
||||
label2.font = InstructionsViewControllerUX.TextFont
|
||||
label2.textColor = InstructionsViewControllerUX.TextColor
|
||||
label2.textAlignment = NSTextAlignment.center
|
||||
label2.attributedText = highlightLink(showMeText as NSString, withColor: InstructionsViewControllerUX.LinkColor)
|
||||
label2.snp.makeConstraints { (make) -> Void in
|
||||
make.width.equalTo(250)
|
||||
make.top.equalTo(label1.snp.bottom).offset(InstructionsViewControllerUX.TopPadding)
|
||||
make.centerX.equalTo(view)
|
||||
}
|
||||
}
|
||||
|
||||
class InstructionsViewController: UIViewController {
|
||||
weak var delegate: InstructionsViewControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
edgesForExtendedLayout = []
|
||||
view.backgroundColor = UIColor.white
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Close", tableName: "SendTo", comment: "Close button in top navigation bar"), style: UIBarButtonItemStyle.done, target: self, action: #selector(InstructionsViewController.close))
|
||||
navigationItem.leftBarButtonItem?.accessibilityIdentifier = "InstructionsViewController.navigationItem.leftBarButtonItem"
|
||||
|
||||
setupHelpView(view,
|
||||
introText: NSLocalizedString("You are not signed in to your Firefox Account.", tableName: "SendTo", comment: "See http://mzl.la/1ISlXnU"),
|
||||
showMeText: NSLocalizedString("Please open Firefox, go to Settings and sign in to continue.", tableName: "SendTo", comment: "See http://mzl.la/1ISlXnU"))
|
||||
}
|
||||
|
||||
func close() {
|
||||
delegate?.instructionsViewControllerDidClose(self)
|
||||
}
|
||||
|
||||
func showMeHow() {
|
||||
print("Show me how") // TODO Not sure what to do or if to keep this. Waiting for UX feedback.
|
||||
}
|
||||
}
|
||||
24
mobile/ios/Extensions/SendTo/MainInterface.storyboard
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14B25" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="ObA-dk-sSI">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Send To-->
|
||||
<scene sceneID="7MM-of-jgj">
|
||||
<objects>
|
||||
<viewController title="Send To" id="ObA-dk-sSI" customClass="ActionViewController" customModule="SendTo" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<size key="freeformSize" width="320" height="528"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="252" y="-124"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
5
mobile/ios/Extensions/SendTo/en.lproj/InfoPlist.strings
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
"CFBundleDisplayName" = "Send Tab";
|
||||
23
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "menu-Bookmark.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "menu-Bookmark@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "menu-Bookmark@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark.png
vendored
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
23
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "reading-list-add.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "reading-list-add@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "reading-list-add@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add.png
vendored
Normal file
|
After Width: | Height: | Size: 316 B |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 695 B |
23
mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
48
mobile/ios/Extensions/ShareTo/Info.plist
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>MozDevelopmentTeam</key>
|
||||
<string>$(DEVELOPMENT_TEAM)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(MOZ_BUNDLE_DISPLAY_NAME)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>InitialViewController</string>
|
||||
</dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>FiraSans-Regular.ttf</string>
|
||||
<string>FiraSans-SemiBold.ttf</string>
|
||||
<string>FiraSans-Light.ttf</string>
|
||||
<string>FiraSans-UltraLight.ttf</string>
|
||||
<string>FiraSans-Medium.ttf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
136
mobile/ios/Extensions/ShareTo/InitialViewController.swift
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/* 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 Shared
|
||||
import Storage
|
||||
|
||||
private let LastUsedShareDestinationsKey = "LastUsedShareDestinations"
|
||||
|
||||
@objc(InitialViewController)
|
||||
class InitialViewController: UIViewController, ShareControllerDelegate {
|
||||
var shareDialogController: ShareDialogController!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.view.backgroundColor = UIColor(white: 0.0, alpha: 0.66) // TODO: Is the correct color documented somewhere?
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
ExtensionUtils.extractSharedItemFromExtensionContext(self.extensionContext, completionHandler: { (item, error) -> Void in
|
||||
if let item = item, error == nil {
|
||||
DispatchQueue.main.async {
|
||||
guard item.isShareable else {
|
||||
let alert = UIAlertController(title: Strings.SendToErrorTitle, message: Strings.SendToErrorMessage, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: Strings.SendToErrorOKButton, style: .default) { _ in self.finish() })
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.presentShareDialog(item)
|
||||
}
|
||||
} else {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func shareControllerDidCancel(_ shareController: ShareDialogController) {
|
||||
UIView.animate(withDuration: 0.25, animations: { () -> Void in
|
||||
self.shareDialogController.view.alpha = 0.0
|
||||
}, completion: { (Bool) -> Void in
|
||||
self.dismissShareDialog()
|
||||
self.finish()
|
||||
})
|
||||
}
|
||||
|
||||
func finish() {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
func shareController(_ shareController: ShareDialogController, didShareItem item: ShareItem, toDestinations destinations: NSSet) {
|
||||
setLastUsedShareDestinations(destinations)
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: { () -> Void in
|
||||
self.shareDialogController.view.alpha = 0.0
|
||||
}, completion: { (Bool) -> Void in
|
||||
self.dismissShareDialog()
|
||||
|
||||
let profile = BrowserProfile(localName: "profile")
|
||||
|
||||
if destinations.contains(ShareDestinationReadingList) {
|
||||
profile.readingList?.createRecordWithURL(item.url, title: item.title ?? "", addedBy: UIDevice.current.name)
|
||||
}
|
||||
|
||||
if destinations.contains(ShareDestinationBookmarks) {
|
||||
_ = profile.bookmarks.shareItem(item).value // Blocks until database has settled
|
||||
}
|
||||
|
||||
profile.shutdown()
|
||||
|
||||
self.finish()
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// TODO: use Set.
|
||||
func getLastUsedShareDestinations() -> NSSet {
|
||||
if let destinations = UserDefaults.standard.object(forKey: LastUsedShareDestinationsKey) as? NSArray {
|
||||
return NSSet(array: destinations as [AnyObject])
|
||||
}
|
||||
return NSSet(object: ShareDestinationBookmarks)
|
||||
}
|
||||
|
||||
func setLastUsedShareDestinations(_ destinations: NSSet) {
|
||||
UserDefaults.standard.set(destinations.allObjects, forKey: LastUsedShareDestinationsKey)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
func presentShareDialog(_ item: ShareItem) {
|
||||
shareDialogController = ShareDialogController()
|
||||
shareDialogController.delegate = self
|
||||
shareDialogController.item = item
|
||||
shareDialogController.initialShareDestinations = getLastUsedShareDestinations()
|
||||
|
||||
self.addChildViewController(shareDialogController)
|
||||
shareDialogController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(shareDialogController.view)
|
||||
shareDialogController.didMove(toParentViewController: self)
|
||||
|
||||
// Setup constraints for the dialog. We keep the dialog centered with 16 points of padding on both
|
||||
// sides. The dialog grows to max 380 points wide so that it does not look too big on landscape or
|
||||
// iPad devices.
|
||||
|
||||
let views: NSDictionary = ["dialog": shareDialogController.view]
|
||||
|
||||
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(16@751)-[dialog(<=380@1000)]-(16@751)-|",
|
||||
options: NSLayoutFormatOptions(), metrics: nil, views: (views as? [String: AnyObject])!))
|
||||
|
||||
let cx = NSLayoutConstraint(item: shareDialogController.view, attribute: NSLayoutAttribute.centerX,
|
||||
relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 0)
|
||||
cx.priority = 1000 // TODO: Why does UILayoutPriorityRequired give a linker error? SDK Bug?
|
||||
view.addConstraint(cx)
|
||||
|
||||
view.addConstraint(NSLayoutConstraint(item: shareDialogController.view, attribute: NSLayoutAttribute.centerY,
|
||||
relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0))
|
||||
|
||||
// Fade the dialog in
|
||||
|
||||
shareDialogController.view.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.25, animations: { () -> Void in
|
||||
self.shareDialogController.view.alpha = 1.0
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
func dismissShareDialog() {
|
||||
shareDialogController.willMove(toParentViewController: nil)
|
||||
shareDialogController.view.removeFromSuperview()
|
||||
shareDialogController.removeFromParentViewController()
|
||||
}
|
||||
}
|
||||
245
mobile/ios/Extensions/ShareTo/ShareViewController.swift
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
/* 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 Shared
|
||||
import Storage
|
||||
|
||||
struct ShareDestination {
|
||||
let code: String
|
||||
let name: String
|
||||
let image: String
|
||||
}
|
||||
|
||||
// TODO: See if we can do this with an Enum instead. Previous attempts failed because for example NSSet does not take (string) enum values.
|
||||
let ShareDestinationBookmarks: String = "Bookmarks"
|
||||
let ShareDestinationReadingList: String = "ReadingList"
|
||||
|
||||
let ShareDestinations = [
|
||||
ShareDestination(code: ShareDestinationReadingList, name: NSLocalizedString("Add to Reading List", tableName: "ShareTo", comment: "On/off toggle to select adding this url to your reading list"), image: "AddToReadingList"),
|
||||
ShareDestination(code: ShareDestinationBookmarks, name: NSLocalizedString("Add to Bookmarks", tableName: "ShareTo", comment: "On/off toggle to select adding this url to your bookmarks"), image: "AddToBookmarks")
|
||||
]
|
||||
|
||||
protocol ShareControllerDelegate {
|
||||
func shareControllerDidCancel(_ shareController: ShareDialogController)
|
||||
func shareController(_ shareController: ShareDialogController, didShareItem item: ShareItem, toDestinations destinations: NSSet)
|
||||
}
|
||||
|
||||
private struct ShareDialogControllerUX {
|
||||
static let CornerRadius: CGFloat = 4 // Corner radius of the dialog
|
||||
|
||||
static let NavigationBarTintColor = UIColor(rgb: 0xf37c00) // Tint color changes the text color in the navigation bar
|
||||
static let NavigationBarCancelButtonFont = UIFont.systemFont(ofSize: UIFont.buttonFontSize) // System default
|
||||
static let NavigationBarAddButtonFont = UIFont.boldSystemFont(ofSize: UIFont.buttonFontSize) // System default
|
||||
static let NavigationBarIconSize = 40 // Width and height of the icon
|
||||
static let NavigationBarBottomPadding = 12
|
||||
|
||||
static let ItemTitleFontMedium = UIFont.systemFont(ofSize: 15, weight: UIFontWeightMedium)
|
||||
static let ItemTitleFont = UIFont.systemFont(ofSize: 15)
|
||||
static let ItemTitleMaxNumberOfLines = 2
|
||||
static let ItemTitleLeftPadding = 44
|
||||
static let ItemTitleRightPadding = 44
|
||||
static let ItemTitleBottomPadding = 12
|
||||
|
||||
static let ItemLinkFont = UIFont.systemFont(ofSize: 12)
|
||||
static let ItemLinkMaxNumberOfLines = 3
|
||||
static let ItemLinkLeftPadding = 44
|
||||
static let ItemLinkRightPadding = 44
|
||||
static let ItemLinkBottomPadding = 14
|
||||
|
||||
static let DividerColor = UIColor.lightGray // Divider between the item and the table with destinations
|
||||
static let DividerHeight = 0.5
|
||||
|
||||
static let TableRowHeight: CGFloat = 44 // System default
|
||||
static let TableRowFont = UIFont.systemFont(ofSize: 14)
|
||||
static let TableRowFontMinScale: CGFloat = 0.8
|
||||
static let TableRowTintColor = UIColor(red: 0.427, green: 0.800, blue: 0.102, alpha: 1.0) // Green tint for the checkmark
|
||||
static let TableRowTextColor = UIColor(rgb: 0x555555)
|
||||
|
||||
static let TableHeight = 88 // Height of 2 standard 44px cells
|
||||
}
|
||||
|
||||
class ShareDialogController: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
var delegate: ShareControllerDelegate!
|
||||
var item: ShareItem!
|
||||
var initialShareDestinations: NSSet = NSSet(object: ShareDestinationBookmarks)
|
||||
|
||||
var selectedShareDestinations: NSMutableSet = NSMutableSet()
|
||||
var navBar: UINavigationBar!
|
||||
var navItem: UINavigationItem!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
selectedShareDestinations = NSMutableSet(set: initialShareDestinations)
|
||||
|
||||
self.view.backgroundColor = UIColor.white
|
||||
self.view.layer.cornerRadius = ShareDialogControllerUX.CornerRadius
|
||||
self.view.clipsToBounds = true
|
||||
|
||||
// Setup the NavigationBar
|
||||
|
||||
navBar = UINavigationBar()
|
||||
navBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
navBar.tintColor = ShareDialogControllerUX.NavigationBarTintColor
|
||||
navBar.isTranslucent = false
|
||||
self.view.addSubview(navBar)
|
||||
|
||||
// Setup the NavigationItem
|
||||
|
||||
navItem = UINavigationItem()
|
||||
navItem.leftBarButtonItem = UIBarButtonItem(
|
||||
title: Strings.ShareToCancelButton,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(ShareDialogController.cancel)
|
||||
)
|
||||
navItem.leftBarButtonItem?.setTitleTextAttributes([NSFontAttributeName: ShareDialogControllerUX.NavigationBarCancelButtonFont], for: UIControlState())
|
||||
navItem.leftBarButtonItem?.accessibilityIdentifier = "ShareDialogController.navigationItem.leftBarButtonItem"
|
||||
|
||||
navItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Add", tableName: "ShareTo", comment: "Add button in the share dialog"), style: UIBarButtonItemStyle.done, target: self, action: #selector(ShareDialogController.add))
|
||||
navItem.rightBarButtonItem?.setTitleTextAttributes([NSFontAttributeName: ShareDialogControllerUX.NavigationBarAddButtonFont], for: UIControlState())
|
||||
|
||||
let logo = UIImageView(image: UIImage(named: "Icon-Small"))
|
||||
logo.contentMode = UIViewContentMode.scaleAspectFit // TODO Can go away if icon is provided in correct size
|
||||
navItem.titleView = logo
|
||||
|
||||
navBar.pushItem(navItem, animated: false)
|
||||
|
||||
// Setup the title view
|
||||
|
||||
let titleView = UILabel()
|
||||
titleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleView.numberOfLines = ShareDialogControllerUX.ItemTitleMaxNumberOfLines
|
||||
titleView.lineBreakMode = NSLineBreakMode.byTruncatingTail
|
||||
titleView.text = item.title
|
||||
titleView.font = ShareDialogControllerUX.ItemTitleFontMedium
|
||||
view.addSubview(titleView)
|
||||
|
||||
// Setup the link view
|
||||
|
||||
let linkView = UILabel()
|
||||
linkView.translatesAutoresizingMaskIntoConstraints = false
|
||||
linkView.numberOfLines = ShareDialogControllerUX.ItemLinkMaxNumberOfLines
|
||||
linkView.lineBreakMode = NSLineBreakMode.byTruncatingTail
|
||||
linkView.text = item.url
|
||||
linkView.font = ShareDialogControllerUX.ItemLinkFont
|
||||
view.addSubview(linkView)
|
||||
|
||||
// Setup the icon
|
||||
|
||||
let iconView = UIImageView()
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconView.image = UIImage(named: "defaultFavicon")
|
||||
view.addSubview(iconView)
|
||||
|
||||
// Setup the divider
|
||||
|
||||
let dividerView = UIView()
|
||||
dividerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dividerView.backgroundColor = ShareDialogControllerUX.DividerColor
|
||||
view.addSubview(dividerView)
|
||||
|
||||
// Setup the table with destinations
|
||||
|
||||
let tableView = UITableView()
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.separatorInset = UIEdgeInsets.zero
|
||||
tableView.layoutMargins = UIEdgeInsets.zero
|
||||
tableView.isUserInteractionEnabled = true
|
||||
tableView.delegate = self
|
||||
tableView.allowsSelection = true
|
||||
tableView.dataSource = self
|
||||
tableView.isScrollEnabled = false
|
||||
view.addSubview(tableView)
|
||||
|
||||
// Setup constraints
|
||||
|
||||
let views = [
|
||||
"nav": navBar,
|
||||
"title": titleView,
|
||||
"link": linkView,
|
||||
"icon": iconView,
|
||||
"divider": dividerView,
|
||||
"table": tableView
|
||||
]
|
||||
|
||||
// TODO See Bug 1102516 - Use Snappy to define share extension layout constraints
|
||||
|
||||
let constraints = [
|
||||
"H:|[nav]|",
|
||||
"V:|[nav]",
|
||||
|
||||
"H:|-\(ShareDialogControllerUX.ItemTitleLeftPadding)-[title]-\(ShareDialogControllerUX.ItemTitleRightPadding)-|",
|
||||
"V:[nav]-\(ShareDialogControllerUX.NavigationBarBottomPadding)-[title]",
|
||||
|
||||
"H:|-\(ShareDialogControllerUX.ItemLinkLeftPadding)-[link]-\(ShareDialogControllerUX.ItemLinkLeftPadding)-|",
|
||||
"V:[title]-\(ShareDialogControllerUX.ItemTitleBottomPadding)-[link]",
|
||||
|
||||
"H:|[divider]|",
|
||||
"V:[divider(\(ShareDialogControllerUX.DividerHeight))]",
|
||||
"V:[link]-\(ShareDialogControllerUX.ItemLinkBottomPadding)-[divider]",
|
||||
|
||||
"H:|[table]|",
|
||||
"V:[divider][table]",
|
||||
"V:[table(\(ShareDialogControllerUX.TableHeight))]|"
|
||||
]
|
||||
|
||||
for constraint in constraints {
|
||||
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: constraint, options: NSLayoutFormatOptions(), metrics: nil, views: views))
|
||||
}
|
||||
}
|
||||
|
||||
// UITabBarItem Actions that map to our delegate methods
|
||||
|
||||
func cancel() {
|
||||
delegate?.shareControllerDidCancel(self)
|
||||
}
|
||||
|
||||
func add() {
|
||||
delegate?.shareController(self, didShareItem: item, toDestinations: NSSet(set: selectedShareDestinations))
|
||||
}
|
||||
|
||||
// UITableView Delegate and DataSource
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return ShareDestinations.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return ShareDialogControllerUX.TableRowHeight
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.textColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.darkGray : ShareDialogControllerUX.TableRowTextColor
|
||||
cell.textLabel?.font = ShareDialogControllerUX.TableRowFont
|
||||
cell.accessoryType = selectedShareDestinations.contains(ShareDestinations[indexPath.row].code) ? UITableViewCellAccessoryType.checkmark : UITableViewCellAccessoryType.none
|
||||
cell.tintColor = ShareDialogControllerUX.TableRowTintColor
|
||||
cell.layoutMargins = UIEdgeInsets.zero
|
||||
cell.textLabel?.text = ShareDestinations[indexPath.row].name
|
||||
cell.textLabel?.adjustsFontSizeToFitWidth = true
|
||||
cell.textLabel?.minimumScaleFactor = ShareDialogControllerUX.TableRowFontMinScale
|
||||
cell.imageView?.image = UIImage(named: ShareDestinations[indexPath.row].image)
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
|
||||
let code = ShareDestinations[indexPath.row].code
|
||||
if selectedShareDestinations.contains(code) {
|
||||
selectedShareDestinations.remove(code)
|
||||
} else {
|
||||
selectedShareDestinations.add(code)
|
||||
}
|
||||
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
|
||||
|
||||
navItem.rightBarButtonItem?.isEnabled = (selectedShareDestinations.count != 0)
|
||||
}
|
||||
}
|
||||
23
mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "todayCopiedLink.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "todayCopiedLink@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "todayCopiedLink@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink.png
vendored
Normal file
|
After Width: | Height: | Size: 461 B |
BIN
mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 806 B |
BIN
mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTabHighlight.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTabHighlight@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTabHighlight@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
23
mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTab.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTab@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newPrivateTab@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
23
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTabHighlight.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTabHighlight@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTabHighlight@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 5 KiB |
23
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTab.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTab@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newTab@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
37
mobile/ios/Extensions/Today/Info.plist
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>MozDevelopmentTeam</key>
|
||||
<string>$(DEVELOPMENT_TEAM)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(MOZ_BUNDLE_DISPLAY_NAME)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widget-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>TodayViewController</string>
|
||||
</dict>
|
||||
<key>MozInternalURLScheme</key>
|
||||
<string>$(MOZ_INTERNAL_URL_SCHEME)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
298
mobile/ios/Extensions/Today/TodayViewController.swift
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/* 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 NotificationCenter
|
||||
import Shared
|
||||
import SnapKit
|
||||
import XCGLogger
|
||||
|
||||
private let log = Logger.browserLogger
|
||||
|
||||
struct TodayStrings {
|
||||
static let NewPrivateTabButtonLabel = NSLocalizedString("TodayWidget.NewPrivateTabButtonLabel", tableName: "Today", value: "New Private Tab", comment: "New Private Tab button label")
|
||||
static let NewTabButtonLabel = NSLocalizedString("TodayWidget.NewTabButtonLabel", tableName: "Today", value: "New Tab", comment: "New Tab button label")
|
||||
static let GoToCopiedLinkLabel = NSLocalizedString("TodayWidget.GoToCopiedLinkLabel", tableName: "Today", value: "Go to copied link", comment: "Go to link on clipboard")
|
||||
}
|
||||
|
||||
struct TodayUX {
|
||||
static let privateBrowsingColor = UIColor(rgb: 0xCE6EFC)
|
||||
static let backgroundHightlightColor = UIColor(white: 216.0/255.0, alpha: 44.0/255.0)
|
||||
static let linkTextSize: CGFloat = 10.0
|
||||
static let labelTextSize: CGFloat = 14.0
|
||||
static let imageButtonTextSize: CGFloat = 14.0
|
||||
static let copyLinkImageWidth: CGFloat = 23
|
||||
static let margin: CGFloat = 8
|
||||
static let buttonsHorizontalMarginPercentage: CGFloat = 0.1
|
||||
static let iOS9LeftMargin: CGFloat = 40
|
||||
}
|
||||
|
||||
@objc (TodayViewController)
|
||||
class TodayViewController: UIViewController, NCWidgetProviding {
|
||||
|
||||
var copiedURL: URL?
|
||||
|
||||
fileprivate lazy var newTabButton: ImageButtonWithLabel = {
|
||||
let imageButton = ImageButtonWithLabel()
|
||||
imageButton.addTarget(self, action: #selector(onPressNewTab), forControlEvents: .touchUpInside)
|
||||
imageButton.label.text = TodayStrings.NewTabButtonLabel
|
||||
|
||||
let button = imageButton.button
|
||||
|
||||
button.setImage(UIImage(named: "new_tab_button_normal")?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
button.setImage(UIImage(named: "new_tab_button_highlight")?.withRenderingMode(.alwaysTemplate), for: .highlighted)
|
||||
|
||||
let label = imageButton.label
|
||||
label.font = UIFont.systemFont(ofSize: TodayUX.imageButtonTextSize)
|
||||
|
||||
imageButton.sizeToFit()
|
||||
return imageButton
|
||||
}()
|
||||
|
||||
fileprivate lazy var newPrivateTabButton: ImageButtonWithLabel = {
|
||||
let imageButton = ImageButtonWithLabel()
|
||||
imageButton.addTarget(self, action: #selector(onPressNewPrivateTab), forControlEvents: .touchUpInside)
|
||||
imageButton.label.text = TodayStrings.NewPrivateTabButtonLabel
|
||||
|
||||
let button = imageButton.button
|
||||
button.setImage(UIImage(named: "new_private_tab_button_normal"), for: .normal)
|
||||
button.setImage(UIImage(named: "new_private_tab_button_highlight"), for: .highlighted)
|
||||
|
||||
let label = imageButton.label
|
||||
label.tintColor = TodayUX.privateBrowsingColor
|
||||
label.textColor = TodayUX.privateBrowsingColor
|
||||
label.font = UIFont.systemFont(ofSize: TodayUX.imageButtonTextSize)
|
||||
imageButton.sizeToFit()
|
||||
return imageButton
|
||||
}()
|
||||
|
||||
fileprivate lazy var openCopiedLinkButton: ButtonWithSublabel = {
|
||||
let button = ButtonWithSublabel()
|
||||
|
||||
button.setTitle(TodayStrings.GoToCopiedLinkLabel, for: .normal)
|
||||
button.addTarget(self, action: #selector(onPressOpenClibpoard), for: .touchUpInside)
|
||||
|
||||
// We need to set the background image/color for .Normal, so the whole button is tappable.
|
||||
button.setBackgroundColor(UIColor.clear, forState: .normal)
|
||||
button.setBackgroundColor(TodayUX.backgroundHightlightColor, forState: .highlighted)
|
||||
|
||||
button.setImage(UIImage(named: "copy_link_icon")?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
|
||||
button.label.font = UIFont.systemFont(ofSize: TodayUX.labelTextSize)
|
||||
button.subtitleLabel.font = UIFont.systemFont(ofSize: TodayUX.linkTextSize)
|
||||
return button
|
||||
}()
|
||||
|
||||
fileprivate lazy var widgetStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = TodayUX.margin / 2
|
||||
stackView.distribution = UIStackViewDistribution.fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: TodayUX.margin, left: TodayUX.margin, bottom: TodayUX.margin, right: TodayUX.margin)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
return stackView
|
||||
}()
|
||||
|
||||
fileprivate lazy var buttonStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = 0
|
||||
stackView.distribution = UIStackViewDistribution.fillEqually
|
||||
let edge = self.view.frame.size.width * TodayUX.buttonsHorizontalMarginPercentage
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 0, left: edge, bottom: 0, right: edge)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
return stackView
|
||||
}()
|
||||
|
||||
fileprivate var scheme: String {
|
||||
guard let string = Bundle.main.object(forInfoDictionaryKey: "MozInternalURLScheme") as? String else {
|
||||
// Something went wrong/weird, but we should fallback to the public one.
|
||||
return "firefox"
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let widgetView: UIView!
|
||||
self.extensionContext?.widgetLargestAvailableDisplayMode = .compact
|
||||
let effectView = UIVisualEffectView(effect: UIVibrancyEffect.widgetPrimary())
|
||||
self.view.addSubview(effectView)
|
||||
effectView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self.view)
|
||||
}
|
||||
widgetView = effectView.contentView
|
||||
buttonStackView.addArrangedSubview(newTabButton)
|
||||
buttonStackView.addArrangedSubview(newPrivateTabButton)
|
||||
|
||||
widgetStackView.addArrangedSubview(buttonStackView)
|
||||
widgetStackView.addArrangedSubview(openCopiedLinkButton)
|
||||
|
||||
widgetView.addSubview(widgetStackView)
|
||||
widgetStackView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(widgetView)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updateCopiedLink()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
let edge = size.width * TodayUX.buttonsHorizontalMarginPercentage
|
||||
buttonStackView.layoutMargins = UIEdgeInsets(top: 0, left: edge, bottom: 0, right: edge)
|
||||
}
|
||||
|
||||
func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
|
||||
return UIEdgeInsets.zero
|
||||
}
|
||||
|
||||
func updateCopiedLink() {
|
||||
UIPasteboard.general.asyncURL().uponQueue(.main) { res in
|
||||
if let copiedURL: URL? = res.successValue,
|
||||
let url = copiedURL {
|
||||
self.openCopiedLinkButton.isHidden = false
|
||||
self.openCopiedLinkButton.subtitleLabel.isHidden = SystemUtils.isDeviceLocked()
|
||||
self.openCopiedLinkButton.subtitleLabel.text = url.absoluteDisplayString
|
||||
self.copiedURL = url
|
||||
} else {
|
||||
self.openCopiedLinkButton.isHidden = true
|
||||
self.copiedURL = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Button behaviour
|
||||
@objc func onPressNewTab(_ view: UIView) {
|
||||
openContainingApp()
|
||||
}
|
||||
|
||||
@objc func onPressNewPrivateTab(_ view: UIView) {
|
||||
openContainingApp("?private=true")
|
||||
}
|
||||
|
||||
fileprivate func openContainingApp(_ urlSuffix: String = "") {
|
||||
let urlString = "\(scheme)://open-url\(urlSuffix)"
|
||||
self.extensionContext?.open(URL(string: urlString)!) { success in
|
||||
log.info("Extension opened containing app: \(success)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func onPressOpenClibpoard(_ view: UIView) {
|
||||
if let url = copiedURL,
|
||||
let encodedString = url.absoluteString.escape() {
|
||||
openContainingApp("?url=\(encodedString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIButton {
|
||||
func setBackgroundColor(_ color: UIColor, forState state: UIControlState) {
|
||||
let colorView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
|
||||
colorView.backgroundColor = color
|
||||
|
||||
UIGraphicsBeginImageContext(colorView.bounds.size)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
colorView.layer.render(in: context)
|
||||
}
|
||||
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
self.setBackgroundImage(colorImage, for: state)
|
||||
}
|
||||
}
|
||||
|
||||
class ImageButtonWithLabel: UIView {
|
||||
|
||||
lazy var button = UIButton()
|
||||
lazy var label = UILabel()
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
performLayout()
|
||||
}
|
||||
|
||||
func performLayout() {
|
||||
addSubview(button)
|
||||
addSubview(label)
|
||||
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.left.centerX.equalTo(self)
|
||||
}
|
||||
|
||||
label.snp.makeConstraints { make in
|
||||
make.top.equalTo(button.snp.bottom)
|
||||
make.leading.trailing.bottom.equalTo(self)
|
||||
}
|
||||
|
||||
label.numberOfLines = 1
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.textAlignment = .center
|
||||
label.textColor = UIColor.white
|
||||
}
|
||||
|
||||
func addTarget(_ target: AnyObject?, action: Selector, forControlEvents events: UIControlEvents) {
|
||||
button.addTarget(target, action: action, for: events)
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonWithSublabel: UIButton {
|
||||
lazy var subtitleLabel: UILabel = UILabel()
|
||||
lazy var label: UILabel = UILabel()
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: CGRect.zero)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
performLayout()
|
||||
}
|
||||
|
||||
fileprivate func performLayout() {
|
||||
let titleLabel = self.label
|
||||
|
||||
self.titleLabel?.removeFromSuperview()
|
||||
addSubview(titleLabel)
|
||||
|
||||
let imageView = self.imageView!
|
||||
let subtitleLabel = self.subtitleLabel
|
||||
subtitleLabel.textColor = UIColor.lightGray
|
||||
self.addSubview(subtitleLabel)
|
||||
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.centerY.left.equalTo(self)
|
||||
make.width.equalTo(TodayUX.copyLinkImageWidth)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(imageView.snp.right).offset(TodayUX.margin)
|
||||
make.trailing.top.equalTo(self)
|
||||
}
|
||||
|
||||
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(self)
|
||||
make.top.equalTo(titleLabel.snp.bottom)
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
}
|
||||
}
|
||||
|
||||
override func setTitle(_ text: String?, for state: UIControlState) {
|
||||
self.label.text = text
|
||||
super.setTitle(text, for: state)
|
||||
}
|
||||
}
|
||||
24
mobile/ios/Extensions/ViewLater/ActionRequestHandler.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* 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 MobileCoreServices
|
||||
|
||||
import Shared
|
||||
import Storage
|
||||
|
||||
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
public func beginRequest(with context: NSExtensionContext) {
|
||||
ExtensionUtils.extractSharedItemFromExtensionContext(context, completionHandler: { (item, error) -> Void in
|
||||
if let item = item, error == nil && item.isShareable {
|
||||
let profile = BrowserProfile(localName: "profile")
|
||||
profile.queue.addToQueue(item).uponQueue(DispatchQueue.main) { _ in
|
||||
profile.shutdown()
|
||||
context.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
"CFBundleDisplayName" = "View Later";
|
||||
44
mobile/ios/Extensions/ViewLater/Info.plist
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>MozDevelopmentTeam</key>
|
||||
<string>$(DEVELOPMENT_TEAM)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>CFBundleDisplayName</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict/>
|
||||
<key>CFBundleIcons~ipad</key>
|
||||
<dict/>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ActionRequestHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "view-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "view-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "view-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "view-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "view-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "24x24",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "27.5x27.5",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "44x44",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "longLook",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "86x86",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "98x98",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 877 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
"CFBundleDisplayName" = "View Later";
|
||||