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:
parent
56c0a1a15e
commit
b051f8118f
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue