diff --git a/CHANGELOG.md b/CHANGELOG.md index 53667753..0425efbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Siri shortcuts. [#41](https://github.com/passepartoutvpn/passepartout-ios/pull/41) + ## 1.1.0 Beta 1393 (2019-03-18) ### Fixed diff --git a/Passepartout-iOS/Global/SwiftGen+Scenes.swift b/Passepartout-iOS/Global/SwiftGen+Scenes.swift index 368d8f0e..4fb8f6f3 100644 --- a/Passepartout-iOS/Global/SwiftGen+Scenes.swift +++ b/Passepartout-iOS/Global/SwiftGen+Scenes.swift @@ -21,6 +21,8 @@ internal enum StoryboardScene { internal static let configurationIdentifier = SceneType(storyboard: Main.self, identifier: "ConfigurationIdentifier") + internal static let providerPoolViewController = SceneType(storyboard: Main.self, identifier: "ProviderPoolViewController") + internal static let serviceIdentifier = SceneType(storyboard: Main.self, identifier: "ServiceIdentifier") } internal enum Organizer: StoryboardType { @@ -32,6 +34,11 @@ internal enum StoryboardScene { internal static let wizardHostIdentifier = SceneType(storyboard: Organizer.self, identifier: "WizardHostIdentifier") } + internal enum Shortcuts: StoryboardType { + internal static let storyboardName = "Shortcuts" + + internal static let initialScene = InitialSceneType(storyboard: Shortcuts.self) + } } // swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name diff --git a/Passepartout-iOS/Global/SwiftGen+Segues.swift b/Passepartout-iOS/Global/SwiftGen+Segues.swift index a329d831..206b7e71 100644 --- a/Passepartout-iOS/Global/SwiftGen+Segues.swift +++ b/Passepartout-iOS/Global/SwiftGen+Segues.swift @@ -26,8 +26,13 @@ internal enum StoryboardSegue { case importHostSegueIdentifier = "ImportHostSegueIdentifier" case selectProfileSegueIdentifier = "SelectProfileSegueIdentifier" case showImportedHostsSegueIdentifier = "ShowImportedHostsSegueIdentifier" + case siriShortcutsSegueIdentifier = "SiriShortcutsSegueIdentifier" case versionSegueIdentifier = "VersionSegueIdentifier" } + internal enum Shortcuts: String, SegueType { + case connectToSegueIdentifier = "ConnectToSegueIdentifier" + case pickLocationSegueIdentifier = "PickLocationSegueIdentifier" + } } // swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift index 14cbc0df..094ef709 100644 --- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift @@ -45,12 +45,19 @@ class OrganizerViewController: UITableViewController, TableModelHost { let model: TableModel = TableModel() model.add(.providers) model.add(.hosts) + if #available(iOS 12, *) { + model.add(.siri) + } model.add(.about) model.add(.destruction) model.setHeader(L10n.Organizer.Sections.Providers.header, for: .providers) model.setHeader(L10n.Organizer.Sections.Hosts.header, for: .hosts) model.setFooter(L10n.Organizer.Sections.Providers.footer, for: .providers) model.setFooter(L10n.Organizer.Sections.Hosts.footer, for: .hosts) + if #available(iOS 12, *) { + model.setHeader(L10n.Organizer.Sections.Siri.header, for: .siri) + model.set([.siriShortcuts], in: .siri) + } model.set([.openAbout], in: .about) model.set([.uninstall], in: .destruction) if AppConstants.Flags.isBeta { @@ -179,6 +186,10 @@ class OrganizerViewController: UITableViewController, TableModelHost { private func addNewHost() { perform(segue: StoryboardSegue.Organizer.showImportedHostsSegueIdentifier) } + + private func addShortcuts() { + perform(segue: StoryboardSegue.Organizer.siriShortcutsSegueIdentifier) + } private func removeProfile(at indexPath: IndexPath) { let sectionObject = model.section(for: indexPath.section) @@ -269,6 +280,8 @@ extension OrganizerViewController { case hosts + case siri + case about case destruction @@ -283,6 +296,8 @@ extension OrganizerViewController { case addHost + case siriShortcuts + case openAbout case uninstall @@ -347,6 +362,12 @@ extension OrganizerViewController { cell.leftText = L10n.Organizer.Cells.AddHost.caption return cell + case .siriShortcuts: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.applyAction(Theme.current) + cell.leftText = L10n.Organizer.Cells.SiriShortcuts.caption + return cell + case .openAbout: let cell = Cells.setting.dequeue(from: tableView, for: indexPath) cell.leftText = L10n.Organizer.Cells.About.caption(GroupConstants.App.name) @@ -381,6 +402,9 @@ extension OrganizerViewController { case .addHost: addNewHost() + case .siriShortcuts: + addShortcuts() + case .openAbout: about() diff --git a/Passepartout-iOS/Scenes/Organizer/ShortcutsConnectToViewController.swift b/Passepartout-iOS/Scenes/Organizer/ShortcutsConnectToViewController.swift new file mode 100644 index 00000000..301e1d3d --- /dev/null +++ b/Passepartout-iOS/Scenes/Organizer/ShortcutsConnectToViewController.swift @@ -0,0 +1,210 @@ +// +// ShortcutsConnectToViewController.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 3/18/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import UIKit +import Intents +import IntentsUI +import Passepartout_Core + +class ShortcutsConnectToViewController: UITableViewController, TableModelHost { + private let service = TransientStore.shared.service + + private var providers: [String] = [] + + private var hosts: [String] = [] + + private var selectedProfile: ConnectionProfile? + + // MARK: TableModelHost + + let model: TableModel = { + let model: TableModel = TableModel() + model.setHeader(L10n.Organizer.Sections.Providers.header, for: .providers) + model.setHeader(L10n.Organizer.Sections.Hosts.header, for: .hosts) + return model + }() + + func reloadModel() { + providers = service.ids(forContext: .provider).sorted() + hosts = service.ids(forContext: .host).sortedCaseInsensitive() + + if !providers.isEmpty { + model.add(.providers) + model.set(.providerShortcut, count: providers.count, in: .providers) + } + if !hosts.isEmpty { + model.add(.hosts) + model.set(.hostShortcut, count: hosts.count, in: .hosts) + } + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + title = L10n.Shortcuts.Cells.Connect.caption + reloadModel() + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + guard identifier == StoryboardSegue.Shortcuts.pickLocationSegueIdentifier.rawValue else { + return false + } + guard let _ = selectedProfile as? ProviderConnectionProfile else { + return false + } + return true + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let vc = segue.destination as? ProviderPoolViewController else { + return + } + guard let provider = selectedProfile as? ProviderConnectionProfile else { + return + } + vc.pools = provider.sortedPools() + vc.delegate = self + } +} + +extension ShortcutsConnectToViewController { + enum SectionType { + case providers + + case hosts + } + + enum RowType { + case providerShortcut + + case hostShortcut + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return model.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return model.header(for: section) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return model.count(for: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.apply(Theme.current) + switch model.row(at: indexPath) { + case .providerShortcut: + cell.leftText = providers[indexPath.row] + + case .hostShortcut: + cell.leftText = hosts[indexPath.row] + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard #available(iOS 12, *) else { + return + } + switch model.row(at: indexPath) { + case .providerShortcut: + selectedProfile = service.profile(withContext: .provider, id: providers[indexPath.row]) + pickProviderLocation() + + case .hostShortcut: + selectedProfile = service.profile(withContext: .host, id: hosts[indexPath.row]) + addConnect() + } + } +} + +// MARK: Actions + +@available(iOS 12, *) +extension ShortcutsConnectToViewController { + private func addConnect() { + guard let host = selectedProfile as? HostConnectionProfile else { + fatalError("Not a HostConnectionProfile") + } + let intent = ConnectVPNIntent() + intent.context = host.context.rawValue + intent.profileId = host.id + addShortcut(with: intent) + } + + private func addMoveToLocation(pool: Pool) { + guard let provider = selectedProfile as? ProviderConnectionProfile else { + fatalError("Not a ProviderConnectionProfile") + } + let intent = MoveToLocationIntent() + intent.providerId = provider.id + intent.poolId = pool.id + intent.poolName = pool.name + addShortcut(with: intent) + } + + private func addShortcut(with intent: INIntent) { + guard let shortcut = INShortcut(intent: intent) else { + return + } + let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut) + vc.delegate = self + present(vc, animated: true, completion: nil) + } + + private func pickProviderLocation() { + perform(segue: StoryboardSegue.Shortcuts.pickLocationSegueIdentifier) + } + + @IBAction private func done() { + dismiss(animated: true, completion: nil) + } +} + +extension ShortcutsConnectToViewController: ProviderPoolViewControllerDelegate { + func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool) { + guard #available(iOS 12, *) else { + return + } + addMoveToLocation(pool: pool) + } +} + +@available(iOS 12, *) +extension ShortcutsConnectToViewController: INUIAddVoiceShortcutViewControllerDelegate { + func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) { + navigationController?.popViewController(animated: true) + dismiss(animated: true, completion: nil) + } + + func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) { + dismiss(animated: true, completion: nil) + } +} diff --git a/Passepartout-iOS/Scenes/Organizer/ShortcutsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ShortcutsViewController.swift new file mode 100644 index 00000000..e1117628 --- /dev/null +++ b/Passepartout-iOS/Scenes/Organizer/ShortcutsViewController.swift @@ -0,0 +1,219 @@ +// +// ShortcutsViewController.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 3/18/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import UIKit +import IntentsUI +import Passepartout_Core + +class ShortcutsViewController: UITableViewController, TableModelHost { + + // MARK: TableModel + + let model: TableModel = { + let model: TableModel = TableModel() + model.add(.vpn) + model.add(.trust) + model.set([.connect, .enableVPN, .disableVPN], in: .vpn) + model.set([.trustWiFi, .untrustWiFi, .trustCellular, .untrustCellular], in: .trust) + model.setHeader(L10n.Shortcuts.Sections.Vpn.header, for: .vpn) + model.setHeader(L10n.Shortcuts.Sections.Trust.header, for: .trust) + return model + }() + + func reloadModel() { + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + title = L10n.Organizer.Cells.SiriShortcuts.caption +// itemNext.title = L10n.Global.next + } +} + +extension ShortcutsViewController { + enum SectionType { + case vpn + + case trust + } + + enum RowType { + case connect // host or provider+location + + case enableVPN + + case disableVPN + + case trustWiFi + + case untrustWiFi + + case trustCellular + + case untrustCellular + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return model.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return model.header(for: section) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return model.count(for: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + switch model.row(at: indexPath) { + case .connect: + cell.leftText = L10n.Shortcuts.Cells.Connect.caption + + case .enableVPN: + cell.leftText = L10n.Shortcuts.Cells.EnableVpn.caption + + case .disableVPN: + cell.leftText = L10n.Shortcuts.Cells.DisableVpn.caption + + case .trustWiFi: + cell.leftText = L10n.Shortcuts.Cells.TrustWifi.caption + + case .untrustWiFi: + cell.leftText = L10n.Shortcuts.Cells.UntrustWifi.caption + + case .trustCellular: + cell.leftText = L10n.Shortcuts.Cells.TrustCellular.caption + + case .untrustCellular: + cell.leftText = L10n.Shortcuts.Cells.UntrustCellular.caption + } + cell.apply(Theme.current) + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard #available(iOS 12, *) else { + return + } + switch model.row(at: indexPath) { + case .connect: + addConnect() + + case .enableVPN: + addEnable() + + case .disableVPN: + addDisable() + + case .trustWiFi: + addTrustWiFi() + + case .untrustWiFi: + addUntrustWiFi() + + case .trustCellular: + addTrustCellular() + + case .untrustCellular: + addUntrustCellular() + } + } +} + +// MARK: Actions + +@available(iOS 12, *) +extension ShortcutsViewController { + private func addConnect() { + guard TransientStore.shared.service.hasProfiles() else { + let alert = Macros.alert( + L10n.Shortcuts.Cells.Connect.caption, + L10n.Shortcuts.Alerts.NoProfiles.message + ) + alert.addAction(L10n.Global.ok) { + if let ip = self.tableView.indexPathForSelectedRow { + self.tableView.deselectRow(at: ip, animated: true) + } + } + present(alert, animated: true, completion: nil) + return + } + perform(segue: StoryboardSegue.Shortcuts.connectToSegueIdentifier) + } + + private func addEnable() { + addShortcut(with: EnableVPNIntent()) + } + + private func addDisable() { + addShortcut(with: DisableVPNIntent()) + } + + private func addTrustWiFi() { + addShortcut(with: TrustCurrentNetworkIntent()) + } + + private func addUntrustWiFi() { + addShortcut(with: UntrustCurrentNetworkIntent()) + } + + private func addTrustCellular() { + addShortcut(with: TrustCellularNetworkIntent()) + } + + private func addUntrustCellular() { + addShortcut(with: UntrustCellularNetworkIntent()) + } + + private func addShortcut(with intent: INIntent) { + guard let shortcut = INShortcut(intent: intent) else { + return + } + let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut) + vc.delegate = self + present(vc, animated: true, completion: nil) + } + + @IBAction private func close() { + dismiss(animated: true, completion: nil) + } +} + +@available(iOS 12, *) +extension ShortcutsViewController: INUIAddVoiceShortcutViewControllerDelegate { + func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) { + tableView.reloadData() + dismiss(animated: true, completion: nil) + } + + func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) { + dismiss(animated: true, completion: nil) + } +} diff --git a/Passepartout-iOS/en.lproj/Main.storyboard b/Passepartout-iOS/en.lproj/Main.storyboard index e4506154..653bed5f 100644 --- a/Passepartout-iOS/en.lproj/Main.storyboard +++ b/Passepartout-iOS/en.lproj/Main.storyboard @@ -50,7 +50,7 @@ - + @@ -59,7 +59,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -128,7 +128,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -161,7 +161,7 @@ - + @@ -242,7 +242,7 @@ - + @@ -251,7 +251,7 @@ - + @@ -303,7 +303,7 @@ - + @@ -312,7 +312,7 @@ - + @@ -363,7 +363,7 @@ - + @@ -422,7 +422,7 @@ - + @@ -431,7 +431,7 @@ - + @@ -483,7 +483,7 @@ - + @@ -492,7 +492,7 @@ - + diff --git a/Passepartout-iOS/en.lproj/Organizer.storyboard b/Passepartout-iOS/en.lproj/Organizer.storyboard index 94f21074..1ddbda57 100644 --- a/Passepartout-iOS/en.lproj/Organizer.storyboard +++ b/Passepartout-iOS/en.lproj/Organizer.storyboard @@ -165,13 +165,13 @@ - + - + - + @@ -294,6 +294,7 @@ + @@ -328,34 +329,34 @@ - + - + - +