Add "Edit" item to rename host profile

Disabled in network profiles. Reuse same title style/constraints
and message strings in host wizard.

For consistency, rename activate() to activateProfile(). And it's
not even an IBAction.
This commit is contained in:
Davide De Rosa 2018-11-02 11:19:59 +01:00
parent 56c0a1a15e
commit b051f8118f
8 changed files with 92 additions and 16 deletions

View File

@ -36,15 +36,16 @@ class Macros {
} }
extension UIAlertController { extension UIAlertController {
func addDefaultAction(_ title: String, handler: @escaping () -> Void) { @discardableResult func addDefaultAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
let action = UIAlertAction(title: title, style: .default) { (action) in let action = UIAlertAction(title: title, style: .default) { (action) in
handler() handler()
} }
addAction(action) addAction(action)
preferredAction = action preferredAction = action
return action
} }
func addCancelAction(_ title: String, handler: (() -> Void)? = nil) { @discardableResult func addCancelAction(_ title: String, handler: (() -> Void)? = nil) -> UIAlertAction {
let action = UIAlertAction(title: title, style: .cancel) { (action) in let action = UIAlertAction(title: title, style: .cancel) { (action) in
handler?() handler?()
} }
@ -52,20 +53,23 @@ extension UIAlertController {
if actions.count == 1 { if actions.count == 1 {
preferredAction = action preferredAction = action
} }
return action
} }
func addAction(_ title: String, handler: @escaping () -> Void) { @discardableResult func addAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
let action = UIAlertAction(title: title, style: .default) { (action) in let action = UIAlertAction(title: title, style: .default) { (action) in
handler() handler()
} }
addAction(action) addAction(action)
return action
} }
func addDestructiveAction(_ title: String, handler: @escaping () -> Void) { @discardableResult func addDestructiveAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
let action = UIAlertAction(title: title, style: .destructive) { (action) in let action = UIAlertAction(title: title, style: .destructive) { (action) in
handler() handler()
} }
addAction(action) addAction(action)
preferredAction = action preferredAction = action
return action
} }
} }

View File

@ -125,6 +125,17 @@ extension UIButton {
} }
} }
extension UITextField {
func applyProfileId(_ theme: Theme) {
placeholder = L10n.Global.Host.TitleInput.placeholder
clearButtonMode = .always
keyboardType = .asciiCapable
returnKeyType = .done
autocapitalizationType = .none
autocorrectionType = .no
}
}
// XXX: status bar is broken // XXX: status bar is broken
extension MFMailComposeViewController { extension MFMailComposeViewController {
func apply(_ theme: Theme) { func apply(_ theme: Theme) {

View File

@ -445,7 +445,7 @@ extension OrganizerViewController: ConnectionServiceDelegate {
perform(segue: StoryboardSegue.Organizer.selectProfileSegueIdentifier, sender: profile) perform(segue: StoryboardSegue.Organizer.selectProfileSegueIdentifier, sender: profile)
} }
func connectionService(didRename profile: ConnectionProfile) { func connectionService(didRename oldProfile: ConnectionProfile, to newProfile: ConnectionProfile) {
TransientStore.shared.serialize() // rename TransientStore.shared.serialize() // rename
reloadModel() reloadModel()

View File

@ -51,6 +51,7 @@ class WizardHostViewController: UITableViewController, TableModelHost {
lazy var model: TableModel<SectionType, RowType> = { lazy var model: TableModel<SectionType, RowType> = {
let model: TableModel<SectionType, RowType> = TableModel() let model: TableModel<SectionType, RowType> = TableModel()
model.add(.meta) model.add(.meta)
model.setFooter(L10n.Global.Host.TitleInput.message, for: .meta)
if !existingHosts.isEmpty { if !existingHosts.isEmpty {
model.add(.existing) model.add(.existing)
model.setHeader(L10n.Wizards.Host.Sections.Existing.header, for: .existing) model.setHeader(L10n.Wizards.Host.Sections.Existing.header, for: .existing)
@ -185,6 +186,10 @@ extension WizardHostViewController {
return model.header(for: section) return model.header(for: section)
} }
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return model.footer(for: section)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count(for: section) return model.count(for: section)
} }
@ -196,9 +201,7 @@ extension WizardHostViewController {
cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption
cell.captionWidth = 100.0 cell.captionWidth = 100.0
cell.allowedCharset = .filename cell.allowedCharset = .filename
cell.field.placeholder = L10n.Wizards.Host.Cells.TitleInput.placeholder cell.field.applyProfileId(Theme.current)
cell.field.clearButtonMode = .always
cell.field.returnKeyType = .done
cell.delegate = self cell.delegate = self
return cell return cell

View File

@ -38,6 +38,7 @@ class ServiceViewController: UIViewController, TableModelHost {
var profile: ConnectionProfile? { var profile: ConnectionProfile? {
didSet { didSet {
title = profile?.id title = profile?.id
navigationItem.rightBarButtonItem?.isEnabled = (profile?.context == .host)
reloadModel() reloadModel()
updateViewsIfNeeded() updateViewsIfNeeded()
} }
@ -47,6 +48,8 @@ class ServiceViewController: UIViewController, TableModelHost {
private lazy var vpn = GracefulVPN(service: service) private lazy var vpn = GracefulVPN(service: service)
private weak var pendingRenameAction: UIAlertAction?
private var lastInfrastructureUpdate: Date? private var lastInfrastructureUpdate: Date?
// MARK: Table // MARK: Table
@ -179,7 +182,7 @@ class ServiceViewController: UIViewController, TableModelHost {
viewWelcome?.isHidden = (profile != nil) viewWelcome?.isHidden = (profile != nil)
} }
@IBAction private func activate() { private func activateProfile() {
service.activateProfile(uncheckedProfile) service.activateProfile(uncheckedProfile)
TransientStore.shared.serialize() // activate TransientStore.shared.serialize() // activate
@ -189,6 +192,28 @@ class ServiceViewController: UIViewController, TableModelHost {
vpn.disconnect(completionHandler: nil) vpn.disconnect(completionHandler: nil)
} }
@IBAction private func renameProfile() {
let alert = Macros.alert(L10n.Service.Alerts.Rename.title, L10n.Global.Host.TitleInput.message)
alert.addTextField { (field) in
field.text = self.profile?.id
field.applyProfileId(Theme.current)
field.delegate = self
}
pendingRenameAction = alert.addDefaultAction(L10n.Global.ok) {
guard let newId = alert.textFields?.first?.text else {
return
}
self.doRenameCurrentProfile(to: newId)
}
alert.addCancelAction(L10n.Global.cancel)
pendingRenameAction?.isEnabled = false
present(alert, animated: true, completion: nil)
}
private func doRenameCurrentProfile(to newId: String) {
profile = service.renameProfile(uncheckedHostProfile, to: newId)
}
private func toggleVpnService(cell: ToggleTableViewCell) { private func toggleVpnService(cell: ToggleTableViewCell) {
if cell.isOn { if cell.isOn {
guard !service.needsCredentials(for: uncheckedProfile) else { guard !service.needsCredentials(for: uncheckedProfile) else {
@ -713,7 +738,7 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog
private func handle(row: RowType, cell: UITableViewCell) -> Bool { private func handle(row: RowType, cell: UITableViewCell) -> Bool {
switch row { switch row {
case .useProfile: case .useProfile:
activate() activateProfile()
case .reconnect: case .reconnect:
confirmVpnReconnection() confirmVpnReconnection()
@ -931,6 +956,21 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog
// MARK: - // MARK: -
extension ServiceViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard string.rangeOfCharacter(from: CharacterSet.filename.inverted) == nil else {
return false
}
if let text = textField.text {
let replacement = (text as NSString).replacingCharacters(in: range, with: string)
pendingRenameAction?.isEnabled = (replacement != uncheckedProfile.id)
}
return true
}
}
// MARK: -
extension ServiceViewController: TrustedNetworksModelDelegate { extension ServiceViewController: TrustedNetworksModelDelegate {
func trustedNetworksCouldDisconnect(_: TrustedNetworksModel) -> Bool { func trustedNetworksCouldDisconnect(_: TrustedNetworksModel) -> Bool {
return (service.preferences.trustPolicy == .disconnect) && (vpn.status != .disconnected) return (service.preferences.trustPolicy == .disconnect) && (vpn.status != .disconnected)

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="AAm-3V-G5F"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="AAm-3V-G5F">
<device id="retina4_7" orientation="portrait"> <device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/> <adaptation id="fullscreen"/>
</device> </device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -215,7 +215,13 @@
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/> <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view> </view>
<navigationItem key="navigationItem" id="Vwa-AG-OnN"/> <navigationItem key="navigationItem" id="Vwa-AG-OnN">
<barButtonItem key="rightBarButtonItem" systemItem="edit" id="gbN-fY-AoW">
<connections>
<action selector="renameProfile" destination="BYZ-38-t0r" id="xxl-Nu-VS6"/>
</connections>
</barButtonItem>
</navigationItem>
<connections> <connections>
<outlet property="labelWelcome" destination="jEt-mV-gjN" id="kaN-fX-eRE"/> <outlet property="labelWelcome" destination="jEt-mV-gjN" id="kaN-fX-eRE"/>
<outlet property="tableView" destination="14D-an-pBY" id="qzB-YR-Pss"/> <outlet property="tableView" destination="14D-an-pBY" id="qzB-YR-Pss"/>

View File

@ -26,6 +26,8 @@
"global.ok" = "OK"; "global.ok" = "OK";
"global.cancel" = "Cancel"; "global.cancel" = "Cancel";
"global.next" = "Next"; "global.next" = "Next";
"global.host.title_input.message" = "Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.).";
"global.host.title_input.placeholder" = "My Profile";
"reddit.title" = "Reddit"; "reddit.title" = "Reddit";
"reddit.message" = "Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project."; "reddit.message" = "Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project.";
@ -49,7 +51,6 @@
"account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix."; "account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix.";
"wizards.host.cells.title_input.caption" = "Title"; "wizards.host.cells.title_input.caption" = "Title";
"wizards.host.cells.title_input.placeholder" = "My Profile";
"wizards.host.sections.existing.header" = "Existing profiles"; "wizards.host.sections.existing.header" = "Existing profiles";
"wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?"; "wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?";
@ -105,6 +106,7 @@
"service.cells.debug_log.caption" = "Debug log"; "service.cells.debug_log.caption" = "Debug log";
"service.cells.report_issue.caption" = "Report connectivity issue"; "service.cells.report_issue.caption" = "Report connectivity issue";
"service.alerts.rename.title" = "Rename profile";
"service.alerts.credentials_needed.message" = "You need to enter account credentials first."; "service.alerts.credentials_needed.message" = "You need to enter account credentials first.";
"service.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?"; "service.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?";
"service.alerts.trusted.no_network.message" = "You are not connected to any Wi-Fi network."; "service.alerts.trusted.no_network.message" = "You are not connected to any Wi-Fi network.";

View File

@ -243,6 +243,14 @@ internal enum L10n {
internal static let next = L10n.tr("Localizable", "global.next") internal static let next = L10n.tr("Localizable", "global.next")
/// OK /// OK
internal static let ok = L10n.tr("Localizable", "global.ok") internal static let ok = L10n.tr("Localizable", "global.ok")
internal enum Host {
internal enum TitleInput {
/// Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.).
internal static let message = L10n.tr("Localizable", "global.host.title_input.message")
/// My Profile
internal static let placeholder = L10n.tr("Localizable", "global.host.title_input.placeholder")
}
}
} }
internal enum ImportedHosts { internal enum ImportedHosts {
@ -415,6 +423,10 @@ internal enum L10n {
/// Do you want to reconnect to the VPN? /// Do you want to reconnect to the VPN?
internal static let message = L10n.tr("Localizable", "service.alerts.reconnect_vpn.message") internal static let message = L10n.tr("Localizable", "service.alerts.reconnect_vpn.message")
} }
internal enum Rename {
/// Rename profile
internal static let title = L10n.tr("Localizable", "service.alerts.rename.title")
}
internal enum TestConnectivity { internal enum TestConnectivity {
/// Connectivity /// Connectivity
internal static let title = L10n.tr("Localizable", "service.alerts.test_connectivity.title") internal static let title = L10n.tr("Localizable", "service.alerts.test_connectivity.title")
@ -655,8 +667,6 @@ internal enum L10n {
internal enum TitleInput { internal enum TitleInput {
/// Title /// Title
internal static let caption = L10n.tr("Localizable", "wizards.host.cells.title_input.caption") internal static let caption = L10n.tr("Localizable", "wizards.host.cells.title_input.caption")
/// My Profile
internal static let placeholder = L10n.tr("Localizable", "wizards.host.cells.title_input.placeholder")
} }
} }
internal enum Sections { internal enum Sections {