Merge branch 'favorite-locations'

This commit is contained in:
Davide De Rosa 2019-11-21 15:47:39 +01:00
commit 2c4e065baf
8 changed files with 175 additions and 17 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 1.10.0 Beta 2181 (2019-11-21) ## 1.10.0 Beta 2181 (2019-11-21)
### Added
- Favorite provider locations. [#118](https://github.com/passepartoutvpn/passepartout-ios/issues/118)
### Changed ### Changed
- "Trusted networks" settings are now saved per profile. [#114](https://github.com/passepartoutvpn/passepartout-ios/issues/114) - "Trusted networks" settings are now saved per profile. [#114](https://github.com/passepartoutvpn/passepartout-ios/issues/114)

View File

@ -69,6 +69,20 @@ internal enum L10n {
} }
} }
internal enum Provider { internal enum Provider {
internal enum Pool {
internal enum Actions {
/// Favorite
internal static let favorite = L10n.tr("App", "provider.pool.actions.favorite")
/// Unfavorite
internal static let unfavorite = L10n.tr("App", "provider.pool.actions.unfavorite")
}
internal enum Sections {
internal enum EmptyFavorites {
/// Swipe left on a location to add or remove it from Favorites.
internal static let footer = L10n.tr("App", "provider.pool.sections.empty_favorites.footer")
}
}
}
internal enum Preset { internal enum Preset {
internal enum Cells { internal enum Cells {
internal enum TechDetails { internal enum TechDetails {

View File

@ -160,6 +160,22 @@ extension UIActivityIndicatorView {
} }
} }
extension UIBarButtonItem {
func apply(_ theme: Theme) {
tintColor = nil
}
func applyAccent(_ theme: Theme) {
tintColor = theme.palette.accent1
}
}
extension UIContextualAction {
func applyNormal(_ theme: Theme) {
backgroundColor = theme.palette.primaryBackground
}
}
// XXX: status bar is broken // XXX: status bar is broken
extension MFMailComposeViewController { extension MFMailComposeViewController {
func apply(_ theme: Theme) { func apply(_ theme: Theme) {

View File

@ -48,6 +48,10 @@
"endpoint.sections.location_addresses.header" = "Addresses"; "endpoint.sections.location_addresses.header" = "Addresses";
"endpoint.sections.location_protocols.header" = "Protocols"; "endpoint.sections.location_protocols.header" = "Protocols";
"provider.pool.actions.favorite" = "Favorite";
"provider.pool.actions.unfavorite" = "Unfavorite";
"provider.pool.sections.empty_favorites.footer" = "Swipe left on a location to add or remove it from Favorites.";
"provider.preset.cells.tech_details.caption" = "Technical details"; "provider.preset.cells.tech_details.caption" = "Technical details";
"network_settings.cells.add_dns_server.caption" = "Add address"; "network_settings.cells.add_dns_server.caption" = "Add address";

View File

@ -29,28 +29,40 @@ import Convenience
protocol ProviderPoolViewControllerDelegate: class { protocol ProviderPoolViewControllerDelegate: class {
func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool) func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool)
func providerPoolController(_: ProviderPoolViewController, didUpdateFavoriteGroups favoriteGroupIds: [String])
} }
class ProviderPoolViewController: UIViewController { class ProviderPoolViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var tableView: UITableView!
private var categories: [PoolCategory] = [] private var allCategories: [PoolCategory] = []
private var sortedGroupsByCategory: [String: [PoolGroup]] = [:] private var favoriteCategories: [PoolCategory] = []
private var currentPool: Pool? private var currentPool: Pool?
private var isShowingFavorites = false
var favoriteGroupIds: [String] = []
var isReadonly = false
weak var delegate: ProviderPoolViewControllerDelegate? weak var delegate: ProviderPoolViewControllerDelegate?
func setInfrastructure(_ infrastructure: Infrastructure, currentPoolId: String?) { func setInfrastructure(_ infrastructure: Infrastructure, currentPoolId: String?) {
categories = infrastructure.categories.sorted { $0.name.lowercased() < $1.name.lowercased() } let sortedCategories = infrastructure.categories.sorted { $0.name.lowercased() < $1.name.lowercased() }
allCategories = []
for c in categories { for c in sortedCategories {
sortedGroupsByCategory[c.name] = c.groups.sorted() allCategories.append(PoolCategory(
name: c.name,
groups: c.groups.sorted(),
presets: c.presets
))
} }
// XXX: uglyyy // XXX: uglyyy
for cat in categories { for cat in allCategories {
for group in cat.groups { for group in cat.groups {
for p in group.pools { for p in group.pools {
if p.id == currentPoolId { if p.id == currentPoolId {
@ -72,6 +84,56 @@ class ProviderPoolViewController: UIViewController {
if let ip = selectedIndexPath { if let ip = selectedIndexPath {
tableView.selectRowAsync(at: ip) tableView.selectRowAsync(at: ip)
} }
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: self, action: #selector(toggleFavorites))
}
// MARK: Actions
@objc private func toggleFavorites() {
isShowingFavorites = !isShowingFavorites
if isShowingFavorites {
reloadFavorites()
navigationItem.rightBarButtonItem?.applyAccent(.current)
} else {
navigationItem.rightBarButtonItem?.apply(.current)
}
tableView.reloadData()
}
private func favoriteGroup(withId groupId: String) {
favoriteGroupIds.append(groupId)
delegate?.providerPoolController(self, didUpdateFavoriteGroups: favoriteGroupIds)
}
private func unfavoriteGroup(in category: PoolCategory, withId groupId: String, deletingRowAt indexPath: IndexPath?) {
favoriteGroupIds.removeAll(where: { $0 == groupId })
if let indexPath = indexPath {
reloadFavorites()
if category.groups.count == 1 {
tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic)
} else {
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
delegate?.providerPoolController(self, didUpdateFavoriteGroups: favoriteGroupIds)
}
private func reloadFavorites() {
favoriteCategories = []
for c in allCategories {
let favoriteGroups = c.groups.filter {
return favoriteGroupIds.contains($0.uniqueId(in: c))
}
guard !favoriteGroups.isEmpty else {
continue
}
favoriteCategories.append(PoolCategory(
name: c.name,
groups: favoriteGroups,
presets: c.presets
))
}
} }
} }
@ -80,10 +142,7 @@ class ProviderPoolViewController: UIViewController {
extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate { extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate {
private var selectedIndexPath: IndexPath? { private var selectedIndexPath: IndexPath? {
for (i, cat) in categories.enumerated() { for (i, cat) in categories.enumerated() {
guard let sortedGroups = sortedGroupsByCategory[cat.name] else { for (j, group) in cat.groups.enumerated() {
continue
}
for (j, group) in sortedGroups.enumerated() {
guard let _ = group.pools.firstIndex(where: { $0.id == currentPool?.id }) else { guard let _ = group.pools.firstIndex(where: { $0.id == currentPool?.id }) else {
continue continue
} }
@ -94,18 +153,34 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
} }
func numberOfSections(in tableView: UITableView) -> Int { func numberOfSections(in tableView: UITableView) -> Int {
if isShowingEmptyFavorites {
return 1
}
return categories.count return categories.count
} }
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard categories.count > 1 else { if isShowingEmptyFavorites {
return nil
}
if categories.count == 1 && categories.first?.name == "" {
return nil return nil
} }
let model = categories[section] let model = categories[section]
return model.name return model.name
} }
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
if isShowingEmptyFavorites {
return L10n.App.Provider.Pool.Sections.EmptyFavorites.footer
}
return nil
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isShowingEmptyFavorites {
return 0
}
let model = categories[section] let model = categories[section]
return model.groups.count return model.groups.count
} }
@ -168,11 +243,48 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
navigationController?.pushViewController(vc, animated: true) navigationController?.pushViewController(vc, animated: true)
} }
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return !isReadonly
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let category = categories[indexPath.section]
let group = poolGroup(at: indexPath)
let groupId = group.uniqueId(in: category)
let action: UIContextualAction
if favoriteGroupIds.contains(groupId) {
action = UIContextualAction(style: .destructive, title: L10n.App.Provider.Pool.Actions.unfavorite) {
self.unfavoriteGroup(in: category, withId: groupId, deletingRowAt: self.isShowingFavorites ? indexPath : nil)
$2(true)
}
} else if !isShowingFavorites {
action = UIContextualAction(style: .normal, title: L10n.App.Provider.Pool.Actions.favorite) {
self.favoriteGroup(withId: groupId)
$2(true)
}
action.applyNormal(.current)
} else {
return nil
}
let cfg = UISwipeActionsConfiguration(actions: [action])
cfg.performsFirstActionWithFullSwipe = false
return cfg
}
// MARK: Helpers
private func poolGroup(at indexPath: IndexPath) -> PoolGroup { private func poolGroup(at indexPath: IndexPath) -> PoolGroup {
let model = categories[indexPath.section] let model = categories[indexPath.section]
guard let sortedGroups = sortedGroupsByCategory[model.name] else { return model.groups[indexPath.row]
fatalError("Missing sorted groups for category '\(model.name)'") }
}
return sortedGroups[indexPath.row] private var categories: [PoolCategory] {
return isShowingFavorites ? favoriteCategories : allCategories
}
private var isShowingEmptyFavorites: Bool {
return isShowingFavorites && favoriteGroupIds.isEmpty
} }
} }

View File

@ -162,6 +162,7 @@ class ServiceViewController: UIViewController, StrongTableHost {
case .providerPoolSegueIdentifier: case .providerPoolSegueIdentifier:
let vc = destination as? ProviderPoolViewController let vc = destination as? ProviderPoolViewController
vc?.setInfrastructure(uncheckedProviderProfile.infrastructure, currentPoolId: uncheckedProviderProfile.poolId) vc?.setInfrastructure(uncheckedProviderProfile.infrastructure, currentPoolId: uncheckedProviderProfile.poolId)
vc?.favoriteGroupIds = uncheckedProviderProfile.favoriteGroupIds ?? []
vc?.delegate = self vc?.delegate = self
case .endpointSegueIdentifier: case .endpointSegueIdentifier:
@ -1425,6 +1426,10 @@ extension ServiceViewController: ProviderPoolViewControllerDelegate {
IntentDispatcher.donateConnection(with: uncheckedProviderProfile) IntentDispatcher.donateConnection(with: uncheckedProviderProfile)
} }
} }
func providerPoolController(_: ProviderPoolViewController, didUpdateFavoriteGroups favoriteGroupIds: [String]) {
uncheckedProviderProfile.favoriteGroupIds = favoriteGroupIds
}
} }
extension ServiceViewController: ProviderPresetViewControllerDelegate { extension ServiceViewController: ProviderPresetViewControllerDelegate {

View File

@ -173,4 +173,7 @@ class ShortcutsConnectToViewController: UITableViewController, ProviderPoolViewC
func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool) { func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool) {
addMoveToLocation(pool: pool) addMoveToLocation(pool: pool)
} }
func providerPoolController(_: ProviderPoolViewController, didUpdateFavoriteGroups favoriteGroupIds: [String]) {
}
} }

@ -1 +1 @@
Subproject commit 2b6edafb0cd34c331aa0a7040571ef82a552ad97 Subproject commit 79cc4a739978b6fc98e910c65d7913431cb41915