Upgrade API to v2 (entities)

This commit is contained in:
Davide De Rosa 2019-04-25 20:40:50 +02:00
parent 2d40213625
commit d9a0ebd923
11 changed files with 115 additions and 210 deletions

View File

@ -169,7 +169,7 @@ extension Infrastructure.Name {
}
}
extension Pool {
extension PoolGroup {
var logo: UIImage? {
return ImageAsset(name: country.lowercased()).image
}

View File

@ -33,19 +33,25 @@ protocol ProviderPoolViewControllerDelegate: class {
class ProviderPoolViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
private var models: [PoolModel] = []
private var categories: [PoolCategory] = []
private var sortedGroupsByCategory: [String: [PoolGroup]] = [:]
private var currentPool: Pool?
weak var delegate: ProviderPoolViewControllerDelegate?
func setModels(_ models: [PoolModel], currentPoolId: String?) {
self.models = models
func setInfrastructure(_ infrastructure: Infrastructure, currentPoolId: String?) {
categories = infrastructure.categories.sorted { $0.name < $1.name }
for c in categories {
sortedGroupsByCategory[c.name] = c.groups.values.sorted()
}
// XXX: uglyyy
for m in models {
for pools in m.poolsByGroup.values {
for p in pools {
for cat in categories {
for group in cat.groups.values {
for p in group.pools {
if p.id == currentPoolId {
currentPool = p
return
@ -78,12 +84,16 @@ class ProviderPoolViewController: UIViewController {
extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate {
private var selectedIndexPath: IndexPath? {
for (i, model) in models.enumerated() {
for entries in model.poolsByGroup.enumerated() {
guard let _ = entries.element.value.firstIndex(where: { $0.id == currentPool?.id }) else {
for (i, cat) in categories.enumerated() {
guard let sortedGroups = sortedGroupsByCategory[cat.name] else {
continue
}
for entries in cat.groups.enumerated() {
let group = entries.element.value
guard let _ = group.pools.firstIndex(where: { $0.id == currentPool?.id }) else {
continue
}
guard let row = model.sortedGroups.firstIndex(of: entries.element.key) else {
guard let row = sortedGroups.firstIndex(where: { $0.country == group.country && $0.area == group.area }) else {
continue
}
return IndexPath(row: row, section: i)
@ -93,34 +103,32 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
func numberOfSections(in tableView: UITableView) -> Int {
return models.count
return categories.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard models.count > 1 else {
guard categories.count > 1 else {
return nil
}
let model = models[section]
return model.isFree ? L10n.Provider.Pool.Sections.Free.header : L10n.Provider.Pool.Sections.Paid.header
let model = categories[section]
return model.name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let model = models[section]
return model.sortedGroups.count
let model = categories[section]
return model.groups.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.first else {
let group = poolGroup(at: indexPath)
guard let pool = group.pools.first else {
fatalError("Empty pools in group \(group)")
}
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.imageView?.image = pool.logo
cell.imageView?.image = group.logo
cell.leftText = pool.localizedCountry
if groupPools.count > 1 {
if group.pools.count > 1 {
cell.rightText = pool.area?.uppercased()
cell.accessoryType = .detailDisclosureButton // no checkmark!
} else {
@ -131,10 +139,8 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.randomElement() else {
let group = poolGroup(at: indexPath)
guard let pool = group.pools.randomElement() else {
fatalError("Empty pools in group \(group)")
}
currentPool = pool
@ -142,25 +148,20 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let model = models[indexPath.section]
let group = model.sortedGroups[indexPath.row]
let groupPools = model.poolsByGroup[group]!
guard let pool = groupPools.first else {
fatalError("Empty pools in group \(group)")
}
guard groupPools.count > 1 else {
let group = poolGroup(at: indexPath)
guard group.pools.count > 1 else {
return
}
let vc = OptionViewController<Pool>()
vc.title = pool.localizedCountry
vc.options = groupPools.sorted {
guard let lnum = $0.num, let ln = Int(lnum) else {
vc.title = group.localizedCountry
vc.options = group.pools.sorted {
guard let lnum = $0.num else {
return true
}
guard let rnum = $1.num, let rn = Int(rnum) else {
guard let rnum = $1.num else {
return false
}
return ln < rn
return lnum < rnum
}
vc.selectedOption = currentPool
vc.descriptionBlock = { $0.areaId ?? "" } // XXX: fail gracefully
@ -170,4 +171,12 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate
}
navigationController?.pushViewController(vc, animated: true)
}
private func poolGroup(at indexPath: IndexPath) -> PoolGroup {
let model = categories[indexPath.section]
guard let sortedGroups = sortedGroupsByCategory[model.name] else {
fatalError("Missing sorted groups for category '\(model.name)'")
}
return sortedGroups[indexPath.row]
}
}

View File

@ -152,7 +152,7 @@ class ServiceViewController: UIViewController, TableModelHost {
case .providerPoolSegueIdentifier:
let vc = destination as? ProviderPoolViewController
vc?.setModels(InfrastructureCache.shared.poolModels(for: uncheckedProviderProfile), currentPoolId: uncheckedProviderProfile.poolId)
vc?.setInfrastructure(uncheckedProviderProfile.infrastructure, currentPoolId: uncheckedProviderProfile.poolId)
vc?.delegate = self
case .endpointSegueIdentifier:
@ -299,9 +299,6 @@ class ServiceViewController: UIViewController, TableModelHost {
}
self.lastInfrastructureUpdate = response.1
self.tableView.reloadData()
// invalidate current pool cache
InfrastructureCache.shared.removePoolModels(for: name)
}
if !isUpdating {
hud.hide()

View File

@ -89,7 +89,7 @@ class ShortcutsConnectToViewController: UITableViewController, ProviderPoolViewC
guard let provider = selectedProfile as? ProviderConnectionProfile else {
return
}
vc.setModels(InfrastructureCache.shared.poolModels(for: provider), currentPoolId: nil)
vc.setInfrastructure(provider.infrastructure, currentPoolId: nil)
vc.delegate = self
}

View File

@ -78,8 +78,7 @@
0E57F64620C83FC7008323CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64420C83FC7008323CF /* LaunchScreen.storyboard */; };
0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; };
0E58BF65224152F9006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; };
0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolModel.swift */; };
0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */; };
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolCategory.swift */; };
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; };
0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; };
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; };
@ -223,8 +222,7 @@
0E5E5DDE215119AF00E318A3 /* VPNStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = "<group>"; };
0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = "<group>"; };
0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = "<group>"; };
0E66A26F225FE25800F9C779 /* PoolModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolModel.swift; sourceTree = "<group>"; };
0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructureCache.swift; sourceTree = "<group>"; };
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.swift; sourceTree = "<group>"; };
0E6BE13920CFB76800A6DD36 /* ApplicationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationError.swift; sourceTree = "<group>"; };
0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogViewController.swift; sourceTree = "<group>"; };
0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; };
@ -484,9 +482,7 @@
isa = PBXGroup;
children = (
0EBE3AA3213DC1B000BFA2F5 /* HostConnectionProfile.swift */,
0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */,
0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */,
0E66A26F225FE25800F9C779 /* PoolModel.swift */,
0E79D14021919F5600BB5FB2 /* ProfileKey.swift */,
0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */,
);
@ -529,6 +525,7 @@
0EBE3A83213C6ADE00BFA2F5 /* InfrastructureFactory.swift */,
0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */,
0ED31C0F20CF09A30027975F /* Pool.swift */,
0E66A26F225FE25800F9C779 /* PoolCategory.swift */,
0E533B152258E03B00EF94FC /* PoolGroup.swift */,
0E39BCEF214B9EF10035E9DE /* WebServices.swift */,
);
@ -1016,7 +1013,7 @@
files = (
0E3152BD223FA03D00F61841 /* GroupConstants.swift in Sources */,
0ECEB10A224FECEA00E9E551 /* DataUnit.swift in Sources */,
0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */,
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */,
0E3152C2223FA04800F61841 /* MockVPNProvider.swift in Sources */,
0E533B162258E03B00EF94FC /* PoolGroup.swift in Sources */,
0E3152D2223FA05400F61841 /* DebugLog.swift in Sources */,
@ -1047,7 +1044,6 @@
0E3152CD223FA05400F61841 /* ConnectionProfile.swift in Sources */,
0E3152BC223FA03D00F61841 /* ApplicationError.swift in Sources */,
0E3152C9223FA04D00F61841 /* InfrastructureFactory.swift in Sources */,
0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */,
0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */,
0E3152D3223FA05400F61841 /* EndpointDataSource.swift in Sources */,
0E3152D4223FA05400F61841 /* Preferences.swift in Sources */,

View File

@ -153,9 +153,6 @@
"account.cells.open_guide.caption" = "See your credentials";
"account.cells.signup.caption" = "Register with %@";
"provider.pool.sections.free.header" = "Free";
"provider.pool.sections.paid.header" = "Paid";
"endpoint.sections.location_addresses.header" = "Addresses";
"endpoint.sections.location_protocols.header" = "Protocols";
"endpoint.cells.any_address.caption" = "Automatic";

View File

@ -1,76 +0,0 @@
//
// InfrastructureCache.swift
// Passepartout
//
// Created by Davide De Rosa on 4/11/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 <http://www.gnu.org/licenses/>.
//
import Foundation
// TODO: retain max N pool models at a time (LRU)
public class InfrastructureCache {
public static let shared = InfrastructureCache()
private var poolModelsByName: [Infrastructure.Name: [PoolModel]]
private init() {
poolModelsByName = [:]
}
public func poolModels(for provider: ProviderConnectionProfile) -> [PoolModel] {
if let models = poolModelsByName[provider.name] {
return models
}
let freeModel = PoolModel(isFree: true)
let paidModel = PoolModel(isFree: false)
for p in provider.infrastructure.pools {
if p.isFree ?? false {
freeModel.addPool(p)
} else {
paidModel.addPool(p)
}
// if p.id == currentPoolId {
// currentPool = p
// }
}
freeModel.sort()
paidModel.sort()
var models: [PoolModel] = []
if !freeModel.isEmpty {
models.append(freeModel)
}
if !paidModel.isEmpty {
models.append(paidModel)
}
poolModelsByName[provider.name] = models
return models
}
public func removePoolModels(for name: Infrastructure.Name? = nil) {
if let name = name {
poolModelsByName.removeValue(forKey: name)
return
}
poolModelsByName.removeAll()
}
}

View File

@ -1,5 +1,5 @@
//
// PoolModel.swift
// PoolCategory.swift
// Passepartout
//
// Created by Davide De Rosa on 4/11/19.
@ -25,34 +25,8 @@
import Foundation
public class PoolModel {
public let isFree: Bool
public struct PoolCategory: Codable {
public let name: String
public var poolsByGroup: [PoolGroup: [Pool]]
public private(set) var sortedGroups: [PoolGroup]
public init(isFree: Bool) {
self.isFree = isFree
poolsByGroup = [:]
sortedGroups = []
}
public var isEmpty: Bool {
return sortedGroups.isEmpty
}
public func addPool(_ p: Pool) {
let group = p.group()
if var existingPools = poolsByGroup[group] {
existingPools.append(p)
poolsByGroup[group] = existingPools
} else {
poolsByGroup[group] = [p]
}
}
public func sort() {
sortedGroups = poolsByGroup.keys.sorted()
}
public let groups: [String: PoolGroup]
}

View File

@ -53,7 +53,7 @@ public struct Infrastructure: Codable {
public let name: Name
public let pools: [Pool]
public let categories: [PoolCategory]
public let presets: [InfrastructurePreset]
@ -69,11 +69,27 @@ public struct Infrastructure: Codable {
}
public func pool(for identifier: String) -> Pool? {
return pools.first { $0.id == identifier }
for cat in categories {
for group in cat.groups.values {
guard let found = group.pools.first(where: { $0.id == identifier }) else {
continue
}
return found
}
}
return nil
}
public func pool(withPrefix prefix: String) -> Pool? {
return pools.first { $0.id.hasPrefix(prefix) }
for cat in categories {
for group in cat.groups.values {
guard let found = group.pools.first(where: { $0.id.hasPrefix(prefix) }) else {
continue
}
return found
}
}
return nil
}
public func preset(for identifier: String) -> InfrastructurePreset? {

View File

@ -26,17 +26,17 @@
import Foundation
import TunnelKit
public struct Pool: Codable, Hashable, CustomStringConvertible {
public struct Pool: Codable, Hashable {
public enum CodingKeys: String, CodingKey {
case id
case country
case area
case num
case isFree = "free"
case tags
// case location
@ -48,26 +48,12 @@ public struct Pool: Codable, Hashable, CustomStringConvertible {
public let id: String
public let country: String
public let area: String?
public let num: Int?
public let num: String?
public var areaId: String? {
let id: String
if let area = area, let num = num {
id = "\(area) #\(num)"
} else if let area = area {
id = area
} else if let num = num {
id = "#\(num)"
} else {
return nil
}
return id.uppercased()
}
public let isFree: Bool?
public let tags: [String]?
// public let location: (Double, Double)
@ -94,21 +80,11 @@ public struct Pool: Codable, Hashable, CustomStringConvertible {
return addrs
}
public func group() -> PoolGroup {
return PoolGroup(country: country, area: area)
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
// MARK: CustomStringConvertible
public var description: String {
return "{[\(id)] \"\(localizedCountry)\"}"
}
}
extension Pool {
@ -132,4 +108,18 @@ extension Pool {
}
return String.init(format: Pool.localizedFormat, countryString, zone.uppercased())
}
public var areaId: String? {
let id: String
if let area = area, let num = num {
id = "\(area) #\(num)"
} else if let area = area {
id = area
} else if let num = num {
id = "#\(num)"
} else {
return nil
}
return id.uppercased()
}
}

View File

@ -25,31 +25,27 @@
import Foundation
public struct PoolGroup: Hashable, Comparable, CustomStringConvertible {
public struct PoolGroup: Codable, Hashable, Comparable, CustomStringConvertible {
public let country: String
public let area: String?
private let id: String
public let pools: [Pool]
private let localizedId: String
public init(country: String, area: String?) {
self.country = country
self.area = area
private var id: String {
var id = country
var localizedId = Utils.localizedCountry(country)
if let area = area {
id += area
localizedId += area
}
self.id = id
self.localizedId = localizedId
return id
}
public func contains(_ pool: Pool) -> Bool {
return (pool.country == country) && (pool.area == area)
private var localizedId: String {
var localizedId = Utils.localizedCountry(country)
if let area = area {
localizedId += area
}
return localizedId
}
// MARK: Hashable
@ -70,3 +66,9 @@ public struct PoolGroup: Hashable, Comparable, CustomStringConvertible {
return "{\(country), \(area ?? "--")}"
}
}
extension PoolGroup {
public var localizedCountry: String {
return Utils.localizedCountry(country)
}
}